Високосный год 1С-разработчику приходится учитывать везде, где сумма делится на число дней: аренда за неполный месяц, проценты по займу, пени по договору. Готовой функции проверки во встроенном языке платформы нет, поэтому каждый пишет свою — и нередко с ошибкой. Ниже три надёжных способа проверки: по формуле григорианского календаря, через число дней в феврале (День(КонецМесяца(...))) и через длину года (ДеньГода(КонецГода(...))). Там же — количество дней в месяце и в году, те же вычисления в запросе и два расчёта, где 29 февраля меняет итоговую сумму: оплата за неполный месяц и проценты за период, который переходит через Новый год.
Код рассчитан на платформу 8.3 и любую конфигурацию. Функции работы с датами (Год, День, КонецМесяца, ДеньГода, ДобавитьМесяц) доступны и на клиенте, и на сервере, поэтому модуль ниже можно вызывать прямо из формы, без серверного вызова. Запрос, как обычно, выполняется только на сервере.
Правило григорианского календаря: год високосный, если делится на 4, кроме годов, кратных 100; годы, кратные 400, снова високосные. Поэтому 2024 и 2028 — високосные, 1900 и 2100 — нет, 2000 — да. Во встроенном языке правило записывается через оператор остатка от деления %.
Второй способ не требует помнить правило: платформа сама знает календарь, достаточно спросить, сколько дней в феврале нужного года. Третий — сравнить номер последнего дня года с 366. Результат у всех трёх одинаковый, выбор зависит от того, что у вас на входе — номер года или дата.
| Задача | Выражение | Что на входе |
|---|---|---|
| Проверка високосного года по формуле | Г % 4 = 0 И (Г % 100 <> 0 ИЛИ Г % 400 = 0) | Число (номер года) |
| Проверка через февраль | День(КонецМесяца(Дата(Г, 2, 1))) = 29 | Число или Год(Дата) |
| Проверка через длину года | ДеньГода(КонецГода(Д)) = 366 | Дата |
| Количество дней в месяце | День(КонецМесяца(Д)) | Дата |
| Количество дней в году | ДеньГода(КонецГода(Д)) | Дата |
| Дата через год | ДобавитьМесяц(Д, 12) | Дата |
Чтобы не копировать выражения по конфигурации, соберите их в общий модуль, например КалендарныеРасчетыКлиентСервер. Флаги: «Клиент (управляемое приложение)», «Сервер», «Внешнее соединение»; для обычных форм добавьте «Клиент (обычное приложение)». Суффикс КлиентСервер в имени — по стандартам разработки 1С для модулей, доступных в обоих контекстах. Перед созданием убедитесь, что модуля с таким именем в конфигурации ещё нет.
// Общий модуль КалендарныеРасчетыКлиентСервер
// Флаги: «Клиент (управляемое приложение)», «Сервер», «Внешнее соединение».
// Для обычных форм дополнительно отметьте «Клиент (обычное приложение)».
#Область ПрограммныйИнтерфейс
// Проверяет, високосный ли год (правила григорианского календаря).
//
// Параметры:
// ГодИлиДата - Число, Дата - номер года (2024) или любая дата внутри года.
//
// Возвращаемое значение:
// Булево - Истина, если в году 366 дней.
//
Функция ЭтоВисокосныйГод(ГодИлиДата) Экспорт
Если ТипЗнч(ГодИлиДата) = Тип("Дата") Тогда
НомерГода = Год(ГодИлиДата);
Иначе
НомерГода = ГодИлиДата;
КонецЕсли;
Возврат НомерГода % 4 = 0 И (НомерГода % 100 <> 0 ИЛИ НомерГода % 400 = 0);
КонецФункции
// Та же проверка через календарь платформы: в високосном феврале 29 дней.
//
// Параметры:
// Период - Дата - любая дата внутри проверяемого года.
//
// Возвращаемое значение:
// Булево
//
Функция ЭтоВисокосныйГодПоКалендарю(Период) Экспорт
Возврат День(КонецМесяца(Дата(Год(Период), 2, 1))) = 29;
КонецФункции
// Количество дней в месяце, которому принадлежит дата.
Функция КоличествоДнейВМесяце(Период) Экспорт
Возврат День(КонецМесяца(Период));
КонецФункции
// Количество дней в году, которому принадлежит дата: 365 или 366.
Функция КоличествоДнейВГоду(Период) Экспорт
Возврат ДеньГода(КонецГода(Период));
КонецФункции
// Количество дней в каждом месяце года.
//
// Возвращаемое значение:
// Массив - 12 чисел, индекс 0 соответствует январю.
//
Функция ДниПоМесяцам(НомерГода) Экспорт
Результат = Новый Массив;
Для НомерМесяца = 1 По 12 Цикл
Результат.Добавить(День(КонецМесяца(Дата(НомерГода, НомерМесяца, 1))));
КонецЦикла;
Возврат Результат;
КонецФункции
// Календарные дни между датами включительно. Время в датах не учитывается.
Функция КоличествоДнейВПериоде(ДатаНачала, ДатаОкончания) Экспорт
Начало = НачалоДня(ДатаНачала);
Окончание = НачалоДня(ДатаОкончания);
Если Окончание < Начало Тогда
Возврат 0;
КонецЕсли;
Возврат (Окончание - Начало) / 86400 + 1;
КонецФункции
// Сумма за часть месяца пропорционально календарным дням.
// Обе даты должны относиться к одному месяцу, иначе вызывается исключение.
Функция СуммаЗаЧастьМесяца(СуммаЗаМесяц, ДатаНачала, ДатаОкончания) Экспорт
Если НачалоМесяца(ДатаНачала) <> НачалоМесяца(ДатаОкончания) Тогда
ВызватьИсключение НСтр("ru = 'Даты начала и окончания должны относиться к одному месяцу.'");
КонецЕсли;
Дней = КоличествоДнейВПериоде(ДатаНачала, ДатаОкончания);
Возврат Окр(СуммаЗаМесяц * Дней / КоличествоДнейВМесяце(ДатаНачала), 2);
КонецФункции
// Проценты за период, если база - фактическое число дней в году (365 или 366).
// Период, который переходит через 31 декабря, делится на части по годам.
Функция ПроцентыЗаПериод(Сумма, СтавкаГодовых, ДатаНачала, ДатаОкончания) Экспорт
Результат = 0;
НачалоОтрезка = НачалоДня(ДатаНачала);
Окончание = НачалоДня(ДатаОкончания);
Пока НачалоОтрезка <= Окончание Цикл
КонецОтрезка = НачалоДня(КонецГода(НачалоОтрезка));
Если КонецОтрезка > Окончание Тогда
КонецОтрезка = Окончание;
КонецЕсли;
Дней = КоличествоДнейВПериоде(НачалоОтрезка, КонецОтрезка);
Результат = Результат
+ Сумма * СтавкаГодовых / 100 * Дней / КоличествоДнейВГоду(НачалоОтрезка);
НачалоОтрезка = КонецОтрезка + 86400;
КонецЦикла;
Возврат Окр(Результат, 2);
КонецФункции
#КонецОбласти
Примеры вызова — из формы или с сервера, без разницы:
Високосный2024 = КалендарныеРасчетыКлиентСервер.ЭтоВисокосныйГод(2024); // Истина
Високосный1900 = КалендарныеРасчетыКлиентСервер.ЭтоВисокосныйГод(1900); // Ложь
ВисокосныйПоДате = КалендарныеРасчетыКлиентСервер.ЭтоВисокосныйГод('20250610'); // Ложь
ДнейВФеврале = КалендарныеРасчетыКлиентСервер.КоличествоДнейВМесяце('20240215'); // 29
ДнейВГоду = КалендарныеРасчетыКлиентСервер.КоличествоДнейВГоду('20260301'); // 365
ДниМесяцев = КалендарныеРасчетыКлиентСервер.ДниПоМесяцам(2028);
ФевральВ2028 = ДниМесяцев[1]; // 29, индекс 0 - январь
Выражение НомерГода % 4 = 0 И (НомерГода % 100 <> 0 ИЛИ НомерГода % 400 = 0) возвращает Булево, его можно сразу отдавать через Возврат. Скобки здесь для читаемости: условие «кратно 400» и без них попадает в правильную ветку, но со скобками правило читается так же, как формулируется словами.
Проверка ТипЗнч(ГодИлиДата) = Тип("Дата") позволяет передать и число, и дату. Без неё передача даты в формулу приводит к ошибке — подробнее о приёме в статье про тип значения в 1С.
КонецМесяца возвращает последнюю секунду последнего дня месяца — для февраля 2024 года это 29.02.2024 23:59:59. День берёт из этой даты номер дня, он и равен количеству дней в месяце. Ни таблицы «31, 28, 31…», ни отдельной обработки февраля не нужно — календарь платформы уже всё учёл.
ДеньГода возвращает порядковый номер дня в году. Для 31 декабря это 365 или 366 — готовое количество дней в году и заодно третий способ проверки високосного года.
Разность двух дат в 1С — число секунд. Значение типа Дата не хранит часовой пояс, поэтому разность двух начал дня всегда кратна 86400, и високосный день учитывается автоматически. НачалоДня отрезает время: без него документы от 10:15 и 18:30 дают дробное число дней. + 1 делает период включительным — 15 и 29 февраля дают 15 дней, а не 14.
ДниПоМесяцам возвращает массив из 12 чисел. Индексы начинаются с нуля, поэтому февраль — элемент [1]. Удобно, когда нужно разложить годовую сумму по месяцам или вывести календарь в отчёт.
В языке запросов те же вычисления делаются через КОНЕЦПЕРИОДА, ДЕНЬ и ДЕНЬГОДА. Собрать из поля документа дату «1 февраля того же года» через ДАТАВРЕМЯ не выйдет: в запросе эта функция принимает только числовые константы. Можно дойти до февраля через ДОБАВИТЬКДАТЕ от начала года, но проще получить признак високосного года через длину года.
// Серверный код: модуль обработки или общий модуль с флагом «Сервер».
// Документ.РеализацияТоваровУслуг замените на документ своей конфигурации.
Функция ДокументыСЧисломДней(НачалоПериода, КонецПериода) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка,
| Док.Дата КАК Дата,
| ДЕНЬ(КОНЕЦПЕРИОДА(Док.Дата, МЕСЯЦ)) КАК ДнейВМесяце,
| ДЕНЬГОДА(КОНЕЦПЕРИОДА(Док.Дата, ГОД)) КАК ДнейВГоду,
| ВЫБОР
| КОГДА ДЕНЬГОДА(КОНЕЦПЕРИОДА(Док.Дата, ГОД)) = 366
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК ВисокосныйГод
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Дата МЕЖДУ &НачалоПериода И &КонецПериода";
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(НачалоПериода));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(КонецПериода));
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Поля ДнейВМесяце и ДнейВГоду можно сразу использовать как делители в вычисляемых полях — например, чтобы получить среднюю выручку в день в отчёте на СКД.
Аренда — 60 000 в месяц, арендатор въехал 15 февраля. В 2024 году это 15 дней из 29, в 2023 — 14 из 28. Если делитель зашит константой, февраль посчитается неверно: при 30 — каждый год, при 28 — в каждый високосный. Функция СуммаЗаЧастьМесяца берёт делитель из календаря. Период через границу месяца (например, 20.01–10.02) она не принимает и вызывает исключение: у такого периода нет одного делителя, и без проверки функция молча разделила бы 22 дня на 31 день января. Такой период делят по месяцам и считают каждую часть отдельно.
Заём 1 000 000 под 16% годовых с 15.12.2023 по 15.01.2024. При базе «фактическое число дней в году» период делится на два отрезка: 17 дней декабря делятся на 365, 15 дней января — на 366. Получается 14 009,43. Если считать все 32 дня с делителем 365, выйдет 14 027,40 — расхождение небольшое, но на акте сверки оно всплывёт.
// Аренда 60 000 в месяц, въезд 15 февраля, расчёт до конца месяца.
Сумма2024 = КалендарныеРасчетыКлиентСервер.СуммаЗаЧастьМесяца(60000, '20240215', '20240229'); // 31034.48
Сумма2023 = КалендарныеРасчетыКлиентСервер.СуммаЗаЧастьМесяца(60000, '20230215', '20230228'); // 30000
// Заём 1 000 000 под 16% годовых с 15.12.2023 по 15.01.2024 включительно.
Проценты = КалендарныеРасчетыКлиентСервер.ПроцентыЗаПериод(1000000, 16, '20231215', '20240115'); // 14009.43
Какая база верна — 365, 366 или фактическая — и включаются ли в расчёт первый и последний день, определяет договор или учётная политика, а не программист. Функция реализует один вариант; остальные получаются заменой делителя. Там же решается, округлять каждый отрезок или только итог: в коде округляется итог.
ДатаДокумента % 4 падает с ошибкой «Преобразование значения к типу Число не может быть выполнено». Нужен Год(ДатаДокумента) % 4 или проверка типа, как в модуле выше.365 * 86400 секунд даёт сдвиг на день, как только в периоде есть 29 февраля. Календарный год прибавляется через ДобавитьМесяц(Дата, 12). Учтите обратный случай: от 29.02.2024 функция вернёт 28.02.2025 — в следующем году 29 февраля нет.(ДатаОкончания - ДатаНачала) / 86400 по документам с временем даёт дробь, а период из одного дня — ноль. Если такой ноль попадает в делитель, получите «Деление на 0». Приводите даты к НачалоДня и добавляйте единицу для включительного периода.КоличествоДнейВМесяце вернёт 31, а Год — 1: ошибки нет, но результат бессмысленный. Проверяйте ЗначениеЗаполнено до расчёта.Дата(Год, 2, 29) внутри Попытка формально тоже отвечает на вопрос, но строит логику на исключениях: в цикле это медленно, а в Исключение заодно попадают ошибки, которые вы ловить не собирались.// Ошибка: к дате прибавили 365 суток вместо года.
СрокОкончания = '20230301' + 365 * 86400; // 29.02.2024, а не 01.03.2024
// Правильно: календарный год.
СрокОкончания = ДобавитьМесяц('20230301', 12); // 01.03.2024
Во встроенном языке платформы — нет. Служебные функции на эту тему встречаются в модулях конфигураций, но опираться на неописанные функции чужих модулей рискованно: при обновлении их могут переименовать. Три строки в своём общем модуле надёжнее.
Выражением ДЕНЬ(КОНЕЦПЕРИОДА(Поле, МЕСЯЦ)). Для года — ДЕНЬГОДА(КОНЕЦПЕРИОДА(Поле, ГОД)), результат 365 или 366.
Да. Год, День, КонецМесяца, КонецГода, ДеньГода, ДобавитьМесяц и Массив доступны в клиентском контексте. Серверный вызов нужен только для запроса к базе.
Вычтите одну дату из другой и разделите на 86400 — платформа сама учитывает 29 февраля. Предварительно приведите обе даты к началу дня и решите, включать ли границы периода.
Если на входе номер года из настройки или реквизита — формулу. Если дата документа — ДеньГода(КонецГода(Дата)) = 366, эта же логика без изменений переносится в запрос. Разницы в результате нет.
Если календарные вычисления — часть задачи побольше (начисление процентов по договорам, график платежей, отчёт с нормированием по дням), её можно передать программисту. Опишите задачу на странице разместить задачу — оценка на площадке бесплатная, ответ в течение рабочего дня. Подробнее о направлении — в разделе отчёты и обработки 1С.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.