Как вычислить точное количество месяцев в периоде с учетом дробных частей в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Часто при разработке на платформе 1С возникает задача вычислить количество месяцев между двумя датами. Если мы используем стандартную функцию языка запросов РАЗНОСТЬДАТ(&Дата1, &Дата2, МЕСЯЦ), то получаем в результате только целое число. Однако в реальных бизнес-задачах, например, при расчете среднемесячной оплаты, аренды или KPI, нам необходимо получить точное значение с учетом "хвостика" дней в виде дробной части (например, 8.8 месяцев). Разберем подробнее, как правильно реализовать этот механизм различными способами.

Определение базы для расчета (знаменатель дроби)

Прежде чем приступать к написанию кода, выясним причину возникновения погрешностей при расчете. Главный вопрос: на какое число делить остаток дней, чтобы получить корректную дробную часть месяца? Рассмотрим популярные подходы:

  1. Законодательный коэффициент (29.3): С апреля 2014 года для расчета отпускных применяется коэффициент 29.3 (ранее использовался 29.4). Это среднемесячное число календарных дней.
  2. Метод 30 дней (коммерческий стандарт): В финансовом секторе (учет процентов, кредитов) часто применяется европейский стандарт "30/360", где любой месяц условно принимается за 30 дней.
  3. Точное количество дней в конкретном месяце: Наиболее справедливый метод для точных календарных вычислений. Мы вычисляем "вес" каждого дня в зависимости от того, сколько дней в текущем месяце (28, 29, 30 или 31).
  4. Астрономический год: Для очень длительных периодов применяют коэффициент 30.4375 (с учетом високосных лет).

Проанализируем ситуацию и реализуем расчет на основе фактического количества дней в месяце, так как этот способ дает наименьшую погрешность при привязке к календарю.

Решение задачи с помощью языка запросов 1С

Посмотрим на пример того, как вычислить точное количество месяцев исключительно средствами запроса, не прибегая к постобработке результата циклом. Суть метода заключается в разделении периода на три части: неполный начальный месяц, целые месяцы внутри периода и неполный конечный месяц.

Для каждого "куска" мы будем вычислять долю от общего количества дней в соответствующем месяце. Обратите внимание на использование функции СЕКУНДА вместо простого деления дней, чтобы избежать проблем с границами дат.


ВЫБРАТЬ
    РАЗНОСТЬДАТ(&Дата1, КОНЕЦПЕРИОДА(&Дата1, МЕСЯЦ), ДЕНЬ) / 
    РАЗНОСТЬДАТ(НАЧАЛОПЕРИОДА(&Дата1, МЕСЯЦ), КОНЕЦПЕРИОДА(&Дата1, МЕСЯЦ), ДЕНЬ) 
    + 
    РАЗНОСТЬДАТ(ДОБАВИТЬКДАТЕ(КОНЕЦПЕРИОДА(&Дата1, МЕСЯЦ), СЕКУНДА, 1), НАЧАЛОПЕРИОДА(&Дата2, МЕСЯЦ), МЕСЯЦ) 
    + 
    РАЗНОСТЬДАТ(НАЧАЛОПЕРИОДА(&Дата2, МЕСЯЦ), &Дата2, ДЕНЬ) / 
    РАЗНОСТЬДАТ(НАЧАЛОПЕРИОДА(&Дата2, МЕСЯЦ), КОНЕЦПЕРИОДА(&Дата2, МЕСЯЦ), ДЕНЬ) КАК ТочноеКоличествоМесяцев

Разберем по шагам, что делает этот запрос:

  1. Первый блок вычисляет долю дней в первом (начальном) месяце. Мы находим разность дней от Дата1 до конца месяца и делим на общее число дней в этом месяце.
  2. Второй блок находит количество полностью прошедших целых месяцев между датами. Прибавление одной секунды с помощью ДОБАВИТЬКДАТЕ решает известную проблему смещения границ дат при вызове функции КОНЕЦПЕРИОДА.
  3. Третий блок аналогично первому вычисляет "хвостик" во втором (конечном) месяце.

Универсальная программная функция расчета

Если задача требует программного вычисления на сервере (например, при проведении документа), создадим самодостаточную функцию. Рассмотрим подробнее рекурсивный алгоритм, который позволяет получить точную разность не только в месяцах, но и в днях или годах с заданной точностью округления.


&НаСервере
Функция ДробнаяРазностьДат(НачалоПериода, КонецПериода, Интервал, СДробями = Ложь, Точность = 0, ОкрВверх = Ложь)
    Если СДробями = Ложь Тогда
        Дата1 = ?(НачалоПериода > КонецПериода, КонецПериода, НачалоПериода);
        Дата2 = ?(НачалоПериода > КонецПериода, НачалоПериода, КонецПериода);
        
        Запрос = Новый Запрос;
        Запрос.Текст = "ВЫБРАТЬ РАЗНОСТЬДАТ(&Дата1, &Дата2," + СокрЛП(Интервал) + ") КАК Разность";
        Запрос.УстановитьПараметр("Дата1", Дата1);
        Запрос.УстановитьПараметр("Дата2", Дата2);
        Результат = Запрос.Выполнить().Выбрать();
        Результат.Следующий();
        Разность = ?(ВРег(СокрЛП(Интервал)) = "ДЕНЬ", Результат.Разность + 1, Результат.Разность);
    Иначе
        // Проверяем выбранный интервал
        Если ВРег(СокрЛП(Интервал)) = "МЕСЯЦ" Тогда
            // Обе даты лежат в одном месяце
            Если НачалоМесяца(НачалоПериода) = НачалоМесяца(КонецПериода) Тогда
                СекундВМесяце = КонецМесяца(НачалоПериода) - НачалоМесяца(НачалоПериода) + 1;
                СекундВИнтервале = КонецДня(КонецПериода) - НачалоДня(НачалоПериода) + 1;
                Разность = Окр((СекундВИнтервале / СекундВМесяце) + ?(ОкрВверх, 5 * Pow(10, -Точность - 1), 0), Точность);
            Иначе
                // Считаем дробные части для разных месяцев
                ЕстьДробныйНач = Не (НачалоМесяца(НачалоПериода) = НачалоДня(НачалоПериода));
                ЕстьДробныйКон = Не (КонецМесяца(КонецПериода) = КонецДня(КонецПериода));
                
                Дата1 = ?(ЕстьДробныйНач, КонецМесяца(НачалоПериода) + 1, НачалоДня(НачалоПериода));
                Дата2 = ?(ЕстьДробныйКон, НачалоМесяца(КонецПериода) - 1, КонецДня(КонецПериода));
                
                // Рекурсивный вызов для целой части
                Разность = ДробнаяРазностьДат(Дата1, Дата2, "Месяц", Ложь) + 1;
                
                // Добавляем дробные куски
                СекундВМесяце = ?(ЕстьДробныйНач, КонецМесяца(НачалоПериода) - НачалоМесяца(НачалоПериода) + 1, 0) 
                              + ?(ЕстьДробныйКон, КонецМесяца(КонецПериода) - НачалоМесяца(КонецПериода) + 1, 0);
                СекундВИнтервале = ?(ЕстьДробныйНач, КонецМесяца(НачалоПериода) - НачалоДня(НачалоПериода) + 1, 0) 
                                 + ?(ЕстьДробныйКон, КонецДня(КонецПериода) - НачалоМесяца(КонецПериода) + 1, 0);
                
                Если ЕстьДробныйНач Или ЕстьДробныйКон Тогда
                    Разность = Разность + Окр((СекундВИнтервале / СекундВМесяце) + ?(ОкрВверх, 5 * Pow(10, -Точность - 1), 0), Точность);
                КонецЕсли;
            КонецЕсли;
        КонецЕсли;
    КонецЕсли;
    
    Возврат Разность;
КонецФункции

Важное примечание: В данной функции реализовано принудительное округление вверх, если передан параметр ОкрВверх. Переменные СекундВМесяце и СекундВИнтервале позволяют максимально точно учесть длину каждого конкретного месяца без использования усредненных величин.

Оптимизированный расчет для задач с фиксированным количеством дней в месяце

Если учетная политика вашей организации требует считать каждый месяц равным 30 дням (метод 30/360), алгоритм можно существенно упростить. Рассмотрим короткую функцию, которую легко модифицировать под свои нужды.


Функция РазницаВМесяцах(ДатаНач, ДатаКон)
    Лет = Год(ДатаКон) - Год(ДатаНач);
    Месяцев = Месяц(ДатаКон) - Месяц(ДатаНач);
    
    // Если дата не является началом месяца, берем точный день, иначе условно 30
    Если НачалоДня(ДатаНач) <> НачалоДня(КонецМесяца(ДатаНач)) Тогда
        Дней1 = День(ДатаНач);
    Иначе
        Дней1 = 30;
    КонецЕсли;
    
    Если НачалоДня(ДатаКон) <> НачалоДня(КонецМесяца(ДатаКон)) Тогда
        Дней2 = День(ДатаКон);
    Иначе
        Дней2 = 30;
    КонецЕсли;
    
    Дней = Дней2 - Дней1;
    
    // Возвращаем дробное значение без применения функции Цел()
    Возврат Лет * 12 + Месяцев + (Дней / 30);
КонецФункции

Использование стандартных подсистем (БСП)

Во многих современных конфигурациях 1С (ERP, УТ, КА) не всегда требуется писать свои велосипеды. Обязательно посмотрим в общий модуль ОбщегоНазначенияКлиентСервер. Там часто присутствует функция РазностьДат, которая может закрыть базовые потребности при подсчете целых интервалов.

Кроме того, если расчет связан с рабочими днями (например, учет выработки сотрудников), настоятельно рекомендуем использовать регистр сведений ДанныеПроизводственногоКалендаря. В таком случае расчет дробного месяца будет выглядеть как отношение отработанных дней к общему числу рабочих дней в графике, что исключит искажение данных из-за государственных праздников и выходных.

← На главную