1C-CodeОсновы 1С → Даты: границы периода и потерянный последний день

Даты: границы периода и потерянный последний день

Тип, на котором ошибаются чаще всего. Здесь — почему отчёт теряет последний день и как это не повторить.

Глава 7 из 36 · чтение примерно 26 минут

Дата — третий базовый тип и самый коварный. Не потому, что сложный, а потому, что дата в 1С — это момент времени, а не календарный день. Из одного этого факта растёт почти вся боль с отчётами.

Эту главу стоит прочесть внимательно даже опытному: ошибка «отчёт теряет последний день периода» живёт в рабочих базах годами и обнаруживается случайно.

Как записать дату

Дата(2024, 3, 7) — год, месяц, день. Именно в этом порядке, от большего к меньшему.

Можно добавить время: Дата(2024, 3, 7, 14, 30, 0) — часы, минуты, секунды. Если время не указано, оно равно полуночи — ноль часов ноль минут. Запомните это: полночь — это начало дня, а не конец.

Дата — это момент

Дата(2024, 3, 7);              // 07.03.2024 00:00:00
Дата(2024, 3, 7, 14, 30, 0);   // 07.03.2024 14:30:00
Дата(2024, 13, 1);             // ОШИБКА: тринадцатого месяца нет
ЧастьЧто означает
порядокгод, месяц, день — от большего к меньшему
без времениподставляется полночь, то есть самое начало суток
13-й месяцплатформа не переносит на следующий год, а прекращает работу
«Месяц + 1»на декабре это даёт 13 и падает — переход через год делают явно

Внутри дата — это секунды

Платформа хранит дату числом секунд. Отсюда вся её арифметика, и она проще, чем кажется:

Упражнение 1. Сколько дней прошло

Напишите функцию ДнейМежду(Начало, Конец), возвращающую число дней между двумя датами. Для 07.03.2024 и 10.03.20243.

Решается в тренажёре: код запускается и проверяется сразу.

Границы периода: главная ошибка учёта

Пусть отчёт строят за март. Пишут отбор: Дата >= Дата(2024,3,1) И Дата <= Дата(2024,3,31). Выглядит безупречно — и теряет весь последний день.

Потому что Дата(2024,3,31) — это 31 марта в полночь, то есть самое начало 31-го. Документ, проведённый 31 марта в 10 утра, имеет время 10:00 — он больше, чем полночь, и в отбор не попадает.

✓ Так теряется день

Отбор.Установить(
  Дата <= Дата(2024, 3, 31));

// 31.03 00:00:00
// всё, что позже полуночи, — мимо

Из отчёта пропадают документы последнего дня. Итог меньше — но правдоподобен.

✕ Так правильно

Отбор.Установить(
  Дата <= КонецДня(Дата(2024, 3, 31)));

// 31.03 23:59:59
// последний день целиком внутри

КонецДня двигает момент на последнюю секунду суток.

Почему эту ошибку не находят

Она не даёт ни ошибки, ни пустого отчёта. Отчёт строится, цифры выглядят разумно, просто они чуть меньше правильных.

Заметят её тогда, когда кто-то сверит отчёт с бухгалтерским и увидит расхождение. К этому моменту по неверным цифрам уже приняли решения — и, что хуже, испорчены все прошлые месяцы, а не только текущий.

Отсюда правило без исключений: верхнюю границу периода всегда заворачивают в КонецДня. Всегда. Даже когда «в этом справочнике времени не бывает» — оно там появится.

Границы периодов

ФункцияЧто возвращаетПример от 17.03.2024
НачалоДня(Д)полночь того же дня17.03.2024 00:00:00
КонецДня(Д)последняя секунда суток17.03.2024 23:59:59
НачалоМесяца(Д)первое число, полночь01.03.2024 00:00:00
КонецМесяца(Д)последняя секунда месяца31.03.2024 23:59:59
НачалоКвартала(Д)первое число квартала01.01.2024 00:00:00
КонецКвартала(Д)последняя секунда квартала31.03.2024 23:59:59
НачалоГода(Д)первое января01.01.2024 00:00:00
КонецГода(Д)последняя секунда года31.12.2024 23:59:59
НачалоНедели(Д)понедельник этой недели11.03.2024 00:00:00
КонецНедели(Д)воскресенье, последняя секунда17.03.2024 23:59:59

Почему не «плюс 86399 секунд»

Соблазн есть: Дата + 86399 тоже даёт конец суток. Но такой код надо читать и разгадывать, а КонецДня(Дата) говорит сам за себя.

И тот же довод про месяц: «последний день марта» — это 31, апреля — 30, февраля — 28 или 29. Считать это руками бессмысленно, когда есть КонецМесяца, который знает и про високосный год тоже.

Упражнение 2. Период за месяц

Напишите функцию ПоследняяСекундаМесяца(Д), возвращающую последний момент того месяца, в который попадает дата. Проверка ждёт для 17.03.2024 ответ 31.03.2024 23:59:59.

Решается в тренажёре: код запускается и проверяется сразу.

Разобрать дату на части

Год(Д), Месяц(Д), День(Д) возвращают числа. Час, Минута, Секунда — время.

ДеньНедели(Д) даёт номер, где понедельник = 1, воскресенье = 7. Это отличается от многих языков, где неделя начинается с воскресенья, — и от этого зависит, попадёт ли ваш отчёт по будням на правильные дни.

Месяц — это номер, а не период

Месяц(Д) возвращает число от 1 до 12. Не «март 2024 года», а просто 3.

Поэтому группировка отчёта по Месяц(Дата) на данных за один год выглядит правильной, а как только в базе появляется второй год — март 2024 склеивается с мартом 2025 в одну строку.

Группировать надо по НачалоМесяца(Дата): это настоящая дата, у неё есть и месяц, и год, и сортируется она правильно.

Упражнение 3. Рабочий день или выходной

Напишите функцию Выходной(Д), возвращающую Истина для субботы и воскресенья. Проверка ждёт для 09.03.2024 (это суббота) ответ Истина.

Решается в тренажёре: код запускается и проверяется сразу.

Сдвиг на месяцы: ДобавитьМесяц

Прибавить месяц — это не прибавить 30 дней. В месяцах разное число дней, поэтому для сдвига есть отдельная команда: ДобавитьМесяц(Дата, Сколько).

Она умеет главное: прижимать к концу месяца. 31 января плюс месяц — это 29 февраля в високосный год и 28 февраля в обычный. Не «2 марта», как вышло бы при прибавлении 31 дня.

Прижатие к последнему дню

ДобавитьМесяц(Дата(2024,1,31), 1);   // 29.02.2024 — високосный
ДобавитьМесяц(Дата(2023,1,31), 1);   // 28.02.2023 — обычный
ДобавитьМесяц(Дата(2024,3,31), 1);   // 30.04.2024 — в апреле 30 дней
ДобавитьМесяц(Дата(2024,3,31), -1);  // 29.02.2024 — назад тоже можно
ЧастьЧто означает
прижатиеесли такого числа в месяце нет, берётся последнее
отрицательноесдвиг назад делают тем же ДобавитьМесяц с минусом
времясохраняется — сдвигается только календарная часть

График платежей: считайте от исходной даты

Из прижатия следует правило, которое узнаю́т, когда график уже уехал.

Строя график из двенадцати платежей, каждую дату считают от исходной: ДобавитьМесяц(Начало, Н). А не от предыдущей: ДобавитьМесяц(ПредыдущаяДата, 1).

Разница появляется после первого же прижатия. Стартовали 31 января: от исходной получится 29 февраля, потом 31 марта, 30 апреля, 31 мая — как и договаривались, «в последний день месяца». А от предыдущей: 29 февраля, потом 29 марта, 29 апреля — и число 31 потеряно навсегда, весь оставшийся график съехал.

Упражнение 4. Дата N-го платежа

Напишите функцию ДатаПлатежа(Начало, Номер): дата платежа с указанным номером, где первый платёж (номер 1) — в дату начала. Считайте от исходной даты. Для 31.01.2024 и номера 2 ожидается 29.02.2024.

Решается в тренажёре: код запускается и проверяется сразу.

Пустая дата

Дата, которую не заполнили, не равна Неопределено. У неё есть своё значение — 01.01.0001, самое начало летоисчисления.

Проверять её сравнением с этой датой неудобно и легко ошибиться. Для этого есть ЗначениеЗаполнено(Д) — она вернёт Ложь для пустой даты и Истину для любой настоящей.

Проверять дату до вычислений обязательно: разность с пустой датой даст около семисот тысяч дней, и это число молча уедет в отчёт как «срок хранения».

Показать дату человеку

Формат(Дата, "ДФ=dd.MM.yyyy") превращает дату в текст по шаблону: dd — день, MM — месяц, yyyy — год, HH:mm:ss — время.

И правило, которое важнее самого шаблона: Формат нужен только для показа. Хранить, сравнивать и складывать надо настоящие даты. Дата, превращённая в строку, сортируется как текст — и 01.12.2024 окажется раньше, чем 05.03.2024.

Разница дат — это секунды

Даты вычитаются: Дата2 - Дата1 даёт число секунд между ними. Не дней и не часов — секунд, потому что внутри дата и есть секунды.

Отсюда набор делителей, который приходится помнить: 60 — минута, 3600 — час, 86400 — сутки. Последнее число узнаётся в 1С раньше любого другого.

Прибавление работает так же: Дата + 86400 — завтра, Дата - 3600 — час назад. Только с месяцами так нельзя: в месяце не постоянное число секунд, и для них есть ДобавитьМесяц.

Дней между датами — считать от начала дня

(Дата2 - Дата1) / 86400 даст дробь, если у дат есть время. «Позавчера в 18:00» и «сегодня в 9:00» — это 1,625 суток, а на вопрос «сколько дней прошло» ответ обычно нужен целый.

Поэтому обе даты сначала приводят к началу дня: (НачалоДня(Дата2) - НачалоДня(Дата1)) / 86400. Тот же приём, что с границами периода, и та же причина — время суток портит сравнение.

Упражнение 5. Сколько дней прошло

Напишите ДнейМежду(Начало, Окончание): целое число дней между датами, независимо от времени суток. Проверка передаёт 01.03.2026 и 10.03.2026 12:00 и ждёт 9.

Решается в тренажёре: код запускается и проверяется сразу.

Сравнение дат и попадание в период

Даты сравниваются обычными знаками: <, >, =. Раньше — значит меньше.

А вот «попадает ли дата в период» — то место, где ошибка из начала главы возвращается. Условие Дата <= КонецПериода отсекает всё, что случилось в последний день после полуночи, если КонецПериода — это дата без времени.

Правильно — сравнивать с КонецДня(КонецПериода). И то же самое в запросе: параметр &КонецПериода заполняют уже приведённым к концу дня, а не «как выбрали в форме».

Упражнение 6. Попадает ли в период

Напишите ВПериоде(Проверяемая, Начало, Конец): вернуть Истина, если дата попадает в период, включая оба крайних дня целиком. Проверка передаёт 31.03.2026 23:59:59 при периоде с 1 по 31 марта — и ждёт Истина.

Решается в тренажёре: код запускается и проверяется сразу.

Какая сейчас дата

Команд две, и разница между ними — частый вопрос на собеседовании.

ТекущаяДата() возвращает время того компьютера, где выполняется код. На сервере — серверное, на клиенте — клиентское. В управляемом приложении на клиенте её вызывать нельзя.

ТекущаяДатаСеанса() возвращает время сеанса — серверное, с учётом часового пояса сеанса. Это правильный способ, и в типовых пишут именно так.

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

Часовые пояса

Дата в базе хранится без часового пояса — просто число секунд. Пояс появляется в двух местах: у сеанса пользователя и у информационной базы.

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

В нашем тренажёре этой команды нет — браузерный движок сеансов не изображает. В рабочем коде пишут её.

Даты в запросе

В тексте запроса даты не пишут. Их передают параметром: Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(Начало)).

Причина не в красоте: дата, вписанная в текст запроса, зависит от формата и региональных настроек, и однажды разбирается неправильно. А ещё запрос с вписанной датой каждый раз новый — и база не может использовать готовый план его выполнения.

У языка запросов есть свои функции для дат, и они повторяют знакомые: НАЧАЛОПЕРИОДА(Дата, ДЕНЬ), КОНЕЦПЕРИОДА(Дата, МЕСЯЦ), ДОБАВИТЬКДАТЕ(Дата, ДЕНЬ, 7), РАЗНОСТЬДАТ(Дата1, Дата2, ДЕНЬ). Пригодятся, когда группировать надо по месяцам, а дата в данных — с временем.

Момент времени: две секунды одного дня

Два документа могут иметь одинаковую дату с точностью до секунды. Тогда «который раньше» по дате не определить, а для остатков это важно: приход должен лечь раньше расхода.

Для этого есть МоментВремени() — дата плюс ссылка на документ, дающие строгий порядок. Его и передают в виртуальные таблицы остатков: ОстаткиИОбороты(Граница, …), где граница построена из момента времени.

Это и есть правильный ответ на вопрос «почему при проведении берут остатки на момент времени, а не на дату документа».

Рабочие дни считает не язык

«Через пять рабочих дней» средствами языка не вычисляется: язык не знает ни выходных, ни праздников, ни переносов.

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

Знать это полезно до того, как начнёте писать свой список праздников массивом.

Шпаргалка по датам

КомандаЧто делает
Дата(Г, М, Д[, ч, м, с])собрать дату; без времени — полночь
ТекущаяДата()сейчас (на клиенте — время компьютера пользователя)
Год / Месяц / Деньчасти даты числом
Час / Минута / Секундачасти времени числом
ДеньНедели(Д)понедельник = 1, воскресенье = 7
НачалоДня / КонецДняграницы суток
НачалоМесяца / КонецМесяцаграницы месяца, с учётом високосного года
НачалоКвартала / КонецКварталаграницы квартала
НачалоГода / КонецГодаграницы года
ДобавитьМесяц(Д, N)сдвиг на месяцы с прижатием к концу
Д + 86400завтра; арифметика идёт в секундах
(Б - А) / 86400сколько дней между датами
ЗначениеЗаполнено(Д)Ложь для пустой даты 01.01.0001
Формат(Д, "ДФ=dd.MM.yyyy")текст для показа человеку

Упражнение 5. Просрочка

Напишите функцию ДнейПросрочки(Срок, Сегодня): сколько дней прошло после срока. Если срок ещё не наступил — 0, а не отрицательное число. Если срок не заполнен — тоже 0. Проверка передаёт незаполненный срок Дата(1,1,1) и ждёт 0.

Решается в тренажёре: код запускается и проверяется сразу.

Что дальше

Три базовых типа разобраны: число, строка, дата. Дальше — условия и циклы: как программа выбирает путь и повторяет действия. После них код перестаёт быть прямой линией.

Как закрепить

Читать про код и писать код — разные умения. В тренажёре к этой главе идут упражнения: вы пишете решение, оно запускается на настоящем интерпретаторе 1С и проверяется сразу, без установки платформы.

Открыть эту главу в тренажёре

Строки: склеить, разрезать, найти, проверить
Условия: Булево и выбор пути