Дата во встроенном языке 1С 8.3: от литерала до производственного календаря
Содержание
В 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);
Если нужен список рабочих дней месяца, проще прочитать регистр данных календаря запросом:
"ВЫБРАТЬ
| Д.ДатаКалендаря
|ИЗ
| РегистрСведений.ДанныеПроизводственногоКалендаря КАК Д
|ГДЕ
| Д.ПроизводственныйКалендарь = &Календарь
| И Д.ДатаКалендаря МЕЖДУ &ДатаНач И &ДатаКон
| И Д.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)"
💡 Ссылку на календарь (&Календарь) берите из реквизита организации или параметра обработки, а не ищите по наименованию: названия у разных баз отличаются. Данные календаря на новый год загружаются обновлением классификаторов — если запрос на январь вернул пусто, сначала проверьте, заполнен ли календарь на этот год.
Ошибки при работе с датами и как их найти
Дата() передали строку не в формате ГГГГММДДччммсс — чаще всего 15.01.2024 из файла или поля ввода. Разберите строку на части через СтрРазделить и соберите Дата(Год, Месяц, День).КонецДня(ДатаКон) или в запросе используйте МЕЖДУ с параметром, уже приведённым к концу дня.Остатки(&Период) считает на начало момента. Передайте Новый Граница(КонецДня(Дата), ВидГраницы.Включая).ТекущаяДата() на сервере в другом часовом поясе. Замените на ТекущаяДатаСеанса() и проверьте пояс сеанса.ДобавитьМесяц от предыдущей: после февраля 29-е так и остаётся 29-м. Считайте каждую дату от исходной (ДобавитьМесяц(Старт, N)) или берите КонецМесяца().mm вместо MM. Месяц — заглавные, минуты — строчные.НачалоДня() перед делением на 86400.Как отладить дату
- 🔎 В отладчике Конфигуратора добавьте переменную в табло — дата показывается с секундами, видно и «лишнее» время, и пустую дату
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?
Формат() параметр ДП='—' или проверяйте ЗначениеЗаполнено() перед выводом.