Дата во встроенном языке 1С 8.3: от литерала до производственного календаря

· 9 мин

Содержание

В 1С 8.3 дата — это один тип Дата, который всегда хранит и день, и время с точностью до секунды. Задают её литералом '20240115' или конструктором Дата(2024, 1, 15), сдвигают сложением секунд (Дата + 86400 — плюс сутки) и функцией ДобавитьМесяц(), границы периода получают через НачалоМесяца()/КонецМесяца() и их аналоги, а в строку выводят функцией Формат() с параметром ДФ.

Почти все ошибки с датами в 1С сводятся к трём вещам: забытое время (23:59:59 в конце дня), строка в «русском» формате 15.01.2024, которую платформа не понимает напрямую, и путаница между временем сервера, клиента и сеанса. Все три лечатся одной привычкой: явно приводить границы к НачалоДня()/КонецДня(), разбирать чужие строки вручную и брать ТекущаяДатаСеанса() вместо ТекущаяДата().

Как задать дату в коде: литерал, конструктор, пустая дата

Есть три способа получить значение типа Дата без вычислений.

📌 Литерал в одинарных кавычках. Формат — ГГГГММДДччммсс, время можно опустить:

ДатаНачала   = '20240115';          // 15.01.2024 00:00:00
ДатаСобытия  = '20240115103000';    // 15.01.2024 10:30:00
ДатаЧитаемо  = '2024.01.15 10:30:00'; // то же самое: нецифровые символы игнорируются

📌 Конструктор Дата() — удобен, когда год, месяц и день лежат в переменных:

Отчетная = Дата(Год, 3, 31);              // 31 марта
Точно    = Дата(2024, 3, 31, 18, 0, 0);   // 31.03.2024 18:00:00
ИзСтроки = Дата("20240331180000");        // строка только в формате ГГГГММДДччммсс

📌 Пустая дата — '00010101' или Дата(1, 1, 1). Это значение по умолчанию у реквизитов типа «Дата». Проверять её удобнее через ЗначениеЗаполнено(), чем сравнением с литералом:

Если Не ЗначениеЗаполнено(Объект.ДатаОплаты) Тогда
    Сообщить("Не указана дата оплаты");
КонецЕсли;

Допустимый диапазон — от 01.01.0001 до 31.12.3999 23:59:59. Даты до нашей эры и точность меньше секунды тип Дата не поддерживает.

Только дата, только время

Сам тип один, но у реквизита и у описания типов есть квалификатор состава: ЧастиДаты.Дата, ЧастиДаты.Время, ЧастиДаты.ДатаВремя. Он влияет на хранение и на поле ввода, а не на вычисления.

ТипТолькоДата = Новый ОписаниеТипов("Дата", , , Новый КвалификаторыДаты(ЧастиДаты.Дата));

Время без даты хранится как '00010101ччммсс'. Чтобы сравнить «время суток» у двух полных дат, вычтите начало дня: Дата - НачалоДня(Дата) даёт число секунд с полуночи.

Сдвиг даты и границы периодов: функции начала и конца

Арифметика с датой в 1С идёт в секундах. Прибавили число — получили новую дату. Вычли одну дату из другой — получили число секунд.

Завтра        = ТекущаяДатаСеанса() + 86400;      // +1 сутки
ЧерезЧас      = ТекущаяДатаСеанса() + 3600;
НеделяНазад   = НачалоДня(ТекущаяДатаСеанса()) - 7 * 86400;

Для месяцев секунды не годятся: в месяце 28–31 день. Здесь нужна ДобавитьМесяц(Дата, ЧислоМесяцев), число может быть отрицательным:

ЧерезКвартал = ДобавитьМесяц('20240115', 3);    // 15.04.2024
ГодНазад     = ДобавитьМесяц('20240115', -12);  // 15.01.2023

Если в целевом месяце нужного числа нет, функция берёт последний день месяца: ДобавитьМесяц('20240131', 1) вернёт 29.02.2024.

Функции начала и конца периода

ПериодНачалоКонец
МинутаНачалоМинуты()КонецМинуты()
ЧасНачалоЧаса()КонецЧаса()
ДеньНачалоДня()КонецДня()
Неделя (с понедельника)НачалоНедели()КонецНедели()
МесяцНачалоМесяца()КонецМесяца()
КварталНачалоКвартала()КонецКвартала()
ГодНачалоГода()КонецГода()

Функции «Конец…» возвращают последнюю секунду периода: КонецМесяца('20240215') — это 29.02.2024 23:59:59, а не 29.02.2024 00:00:00.

💡 Типовая связка для отчёта «за прошлый месяц»:

ПрошлыйМесяц = ДобавитьМесяц(ТекущаяДатаСеанса(), -1);
ДатаНач = НачалоМесяца(ПрошлыйМесяц);
ДатаКон = КонецМесяца(ПрошлыйМесяц);

Части даты

Год, месяц и прочие компоненты достают отдельными функциями: Год(), Месяц(), День(), Час(), Минута(), Секунда(). Плюс три полезные:

  • 📅 ДеньНедели(Дата) — от 1 (понедельник) до 7 (воскресенье);
  • 📅 ДеньГода(Дата) — порядковый номер дня в году;
  • 📅 НеделяГода(Дата) — номер недели.

Номер квартала отдельной функцией не получают, его считают: Цел((Месяц(Дата) - 1) / 3) + 1.

Перевод даты в строку и строки в дату

Из даты в строку: Формат()

Строка(Дата) выдаёт представление по региональным настройкам сеанса, поэтому для файлов, имён и печати используйте Формат() — результат предсказуем.

ЗадачаФорматная строкаРезультат для 15.01.2024 10:30
Привычная дата"ДФ=dd.MM.yyyy"15.01.2024
Дата и время"ДФ='dd.MM.yyyy HH:mm:ss'"15.01.2024 10:30:00
Для имени файла"ДФ=yyyyMMdd_HHmmss"20240115_103000
Локальный короткий формат"ДЛФ=D"15.01.2024
Месяц словами"ДЛФ=DD"15 января 2024 г.
Только время"ДЛФ=T"10:30:00
Что вывести при пустой дате"ДФ=dd.MM.yyyy; ДП='не указана'"для пустой даты: не указана

Регистр букв в ДФ имеет значение: MM — месяц, mm — минуты; HH — часы от 0 до 23, hh — от 1 до 12. Перепутанные MM и mm — частая причина «странных» дат в именах файлов.

Для обмена с внешними системами строку собирают в ISO-формате: XMLСтрока(Дата) вернёт 2024-01-15T10:30:00.

Из строки в дату

Дата("15.01.2024") упадёт с ошибкой — конструктор понимает только ГГГГММДДччммсс. Строку с точками разбирают на части:

Функция ДатаИзСтрокиДДММГГГГ(Знач Текст)
    Части = СтрРазделить(СокрЛП(Текст), ".", Ложь);
    Если Части.Количество() <> 3 Тогда
        Возврат '00010101';
    КонецЕсли;
    Попытка
        Возврат Дата(Число(Части[2]), Число(Части[1]), Число(Части[0]));
    Исключение
        Возврат '00010101';
    КонецПопытки;
КонецФункции

Попытка здесь не лишняя: строка 31.02.2024 пройдёт проверку на три части, но Дата() на ней выдаст исключение.

Для ISO-строк из XML и веб-сервисов есть обратная функция: XMLЗначение(Тип("Дата"), "2024-01-15T10:30:00"). Для JSON — ПрочитатьДатуJSON(Строка, ФорматДатыJSON.ISO).

Сравнение дат и подсчёт интервалов

Даты сравниваются обычными операторами <, >, =, <=, >=. Пустая дата меньше любой заполненной.

Главная тонкость — время. Документ от 31.01.2024 14:20 не попадёт в условие Дата <= '20240131', потому что литерал означает 31.01.2024 00:00:00. Правая граница периода должна быть концом дня:

Если Док.Дата >= ДатаНач И Док.Дата <= КонецДня(ДатаКон) Тогда
    // документ в периоде
КонецЕсли;

Разница между датами

Разность двух дат — число секунд. Дальше делим:

Секунд = ДатаКон - ДатаНач;
Дней   = (НачалоДня(ДатаКон) - НачалоДня(ДатаНач)) / 86400;
Часов  = Цел(Секунд / 3600);

НачалоДня() перед делением обязательна: без неё у дат с временем получится дробное число дней (например, 2,73), а Цел() отрежет «неполный» день там, где по календарю он полный.

Полных месяцев и лет простым делением не получить. Месяцы считают через номера: (Год(Д2) - Год(Д1)) * 12 + Месяц(Д2) - Месяц(Д1), а затем уменьшают на 1, если День(Д2) < День(Д1). Так же считают полный возраст или стаж в годах.

Замер длительности кода

Для профилирования секунд мало. Используйте ТекущаяУниверсальнаяДатаВМиллисекундах():

Старт = ТекущаяУниверсальнаяДатаВМиллисекундах();
// ... замеряемый код ...
Сообщить("Заняло, мс: " + Формат(ТекущаяУниверсальнаяДатаВМиллисекундах() - Старт, "ЧГ=0"));

Даты в запросах 1С

В языке запросов свои функции, они не совпадают с функциями встроенного языка.

ЗадачаВстроенный языкЯзык запросов
Константа'20240115'ДАТАВРЕМЯ(2024, 1, 15)
Начало периодаНачалоМесяца(Д)НАЧАЛОПЕРИОДА(Д, МЕСЯЦ)
Конец периодаКонецМесяца(Д)КОНЕЦПЕРИОДА(Д, МЕСЯЦ)
СдвигД + 86400, ДобавитьМесяц()ДОБАВИТЬКДАТЕ(Д, ДЕНЬ, 1)
РазницаД2 - Д1 (секунды)РАЗНОСТЬДАТ(Д1, Д2, ДЕНЬ)
ЧастиГод(), Месяц()ГОД(), МЕСЯЦ(), ДЕНЬНЕДЕЛИ()

Даты в текст запроса не вклеивайте строкой — передавайте параметром:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Док.Ссылка,
|    Док.Дата
|ИЗ
|    Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
|    Док.Дата МЕЖДУ &ДатаНач И &ДатаКон";
Запрос.УстановитьПараметр("ДатаНач", НачалоДня(ДатаНач));
Запрос.УстановитьПараметр("ДатаКон", КонецДня(ДатаКон));

Остатки «на дату» с учётом движений этого дня

Виртуальная таблица Остатки(&Период) возвращает остатки на начало переданного момента. Передали '20240131' — движения 31 января не учтутся. Чтобы включить весь день, передайте границу:

Запрос.УстановитьПараметр("Период",
    Новый Граница(КонецДня(ДатаОстатков), ВидГраницы.Включая));

Если нужен остаток на момент конкретного документа, передают Новый Граница(Документ.МоментВремени(), ВидГраницы.Исключая) — остаток до этого документа.

Группировка по периодам

Чтобы получить продажи по месяцам, группируйте по НАЧАЛОПЕРИОДА(Док.Дата, МЕСЯЦ). Нужны строки и за месяцы без продаж — используйте в запросе конструкцию ПЕРИОДАМИ(МЕСЯЦ, &ДатаНач, &ДатаКон) в итогах (ИТОГИ ... ПО Период ПЕРИОДАМИ(...)), она дополнит пропущенные периоды.

Часовые пояса, текущее время и производственный календарь

Какую «текущую дату» брать

  • 🖥️ ТекущаяДата() — время компьютера, на котором выполняется код. На клиенте это часы пользователя, на сервере — часы сервера 1С.
  • 🌍 ТекущаяДатаСеанса() — время с учётом часового пояса сеанса. Для дат документов, сроков и отборов в прикладном коде используйте её.
  • 🕐 ТекущаяУниверсальнаяДата() — время по UTC, удобно для журналов и обмена.

Разница становится видна, когда сервер стоит в Москве, а филиал работает в Новосибирске: ТекущаяДата() на сервере покажет московское время, ТекущаяДатаСеанса() — новосибирское, если для сеанса задан этот пояс.

Связанные функции: ЧасовойПоясСеанса(), УстановитьЧасовойПоясСеанса(), УстановитьЧасовойПоясИнформационнойБазы(), МестноеВремя(Дата, Пояс) и УниверсальноеВремя(Дата, Пояс) для пересчёта между UTC и местным временем.

УстановитьЧасовойПоясИнформационнойБазы() меняет пояс для всех новых сеансов базы. Вызывайте её только по согласованию, иначе у пользователей других городов «поедут» даты новых документов.

Рабочие дни по производственному календарю

Встроенный язык не знает праздников. В типовых конфигурациях на БСП (БП 3.0, ЗУП 3.1, УТ 11) их хранит справочник «Производственные календари», а считать рабочие дни помогает общий модуль КалендарныеГрафики:

// Дата через 5 рабочих дней по календарю
СрокОплаты = КалендарныеГрафики.ДатаПоКалендарю(Календарь, ДатаДокумента, 5);

Если нужен список рабочих дней месяца, проще прочитать регистр данных календаря запросом:

"ВЫБРАТЬ
|    Д.ДатаКалендаря
|ИЗ
|    РегистрСведений.ДанныеПроизводственногоКалендаря КАК Д
|ГДЕ
|    Д.ПроизводственныйКалендарь = &Календарь
|    И Д.ДатаКалендаря МЕЖДУ &ДатаНач И &ДатаКон
|    И Д.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)"

💡 Ссылку на календарь (&Календарь) берите из реквизита организации или параметра обработки, а не ищите по наименованию: названия у разных баз отличаются. Данные календаря на новый год загружаются обновлением классификаторов — если запрос на январь вернул пусто, сначала проверьте, заполнен ли календарь на этот год.

Ошибки при работе с датами и как их найти

Диагностика: выберите симптом

Как отладить дату

  • 🔎 В отладчике Конфигуратора добавьте переменную в табло — дата показывается с секундами, видно и «лишнее» время, и пустую дату 01.01.0001.
  • 🔎 В фоновых заданиях и на сервере без отладчика выведите значение в журнал регистрации: ЗаписьЖурналаРегистрации("Отладка", , , , Формат(Д, "ДФ='dd.MM.yyyy HH:mm:ss'")).
  • 🔎 В запросе проверьте параметры через консоль запросов: подставьте те же значения и посмотрите, что возвращает отбор с концом дня и без него.

Частые вопросы

Как получить дату без времени?
НачалоДня(Дата) — время обнулится до 00:00:00. В запросе — НАЧАЛОПЕРИОДА(Поле, ДЕНЬ).
Как узнать количество дней в месяце?
День(КонецМесяца(Дата)) — номер последнего дня месяца, то есть 28, 29, 30 или 31.
Можно ли хранить дату раньше 01.01.0001 или с миллисекундами?
Нет. Тип Дата охватывает 01.01.0001–31.12.3999 с точностью до секунды. Миллисекунды храните отдельным числом.
Как вывести дату в формате ГГГГ-ММ-ДД?
Формат(Дата, "ДФ=yyyy-MM-dd"). Полная ISO-строка с временем — XMLСтрока(Дата).
Почему пустая дата в печатной форме выводится как 01.01.0001?
Значение выводится без проверки. Добавьте в Формат() параметр ДП='—' или проверяйте ЗначениеЗаполнено() перед выводом.