Разность дат в 1С во встроенном языке — обычное вычитание: ДатаОкончания - ДатаНачала возвращает число секунд. Дни, полные месяцы, возраст и стаж, часы и минуты получают уже из этого числа или из частей даты, и у каждой задачи свои пограничные случаи: время суток, 31-е число, 29 февраля, пустая дата. Ниже — готовые функции для общего модуля и разбор таких случаев. Разность дат в запросе (функция РАЗНОСТЬДАТ) разобрана в статье о датах в запросе, здесь — только код на встроенном языке.
В коде результат вычитания дат — число, а не дата. Дата плюс число — снова дата, сдвинутая на столько секунд. Тип Дата не хранит часовой пояс, поэтому между началами соседних суток всегда 86 400 секунд и разность начал дней делится на 86 400 без остатка.
Начало = Дата(2025, 3, 10, 18, 0, 0); // 10.03.2025 18:00
Окончание = Дата(2025, 3, 12, 9, 30, 0); // 12.03.2025 09:30
Секунд = Окончание - Начало; // 142200 - число секунд, а не дата
Минут = Цел(Секунд / 60); // 2370
Часов = Секунд / 3600; // 39.5
ПолныхСуток = Цел(Секунд / 86400); // 1 - по 24 часа прошло один раз
КалендарныхДней = (НачалоДня(Окончание) - НачалоДня(Начало)) / 86400; // 2
Срок = Начало + 3 * 86400; // дата + число = дата: 13.03.2025 18:00
СрокНеНаступил = Окончание < Срок; // даты сравниваются напрямую: Истина
Полных суток и календарных дней здесь разное количество: с вечера 10-го до утра 12-го 24 часа прошли один раз, а дата сменилась дважды. В отчётах и сроках почти всегда нужны календарные дни. Месяцы и годы делением не получить: в месяце от 28 до 31 дня, в году 365 или 366.
Функции ниже рассчитаны на общий модуль ДатыКлиентСервер. К базе они не обращаются, поэтому вызываются и на клиенте, и на сервере без серверного вызова. Флажки такого модуля разобраны в статье об общих модулях.
// Общий модуль ДатыКлиентСервер: флажки «Клиент (управляемое приложение)», «Сервер»,
// «Внешнее соединение». К базе функции не обращаются - работают и на клиенте, и на сервере.
// Количество календарных дней между датами, время суток не учитывается.
// Включительно = Истина - считать оба крайних дня: с 01.03 по 31.03 - 31 день.
// Если ДатаОкончания раньше ДатаНачала, результат отрицательный.
Функция ДнейМеждуДатами(Знач ДатаНачала, Знач ДатаОкончания, Знач Включительно = Ложь) Экспорт
Если Не ЗначениеЗаполнено(ДатаНачала) Или Не ЗначениеЗаполнено(ДатаОкончания) Тогда
Возврат 0; // пустая дата - это 01.01.0001, разность с ней - сотни тысяч дней
КонецЕсли;
// Обе даты - на начало суток: между ними целое число раз по 86400 секунд
Дней = (НачалоДня(ДатаОкончания) - НачалоДня(ДатаНачала)) / 86400;
Если Включительно Тогда
Дней = Дней + ?(Дней < 0, -1, 1);
КонецЕсли;
Возврат Дней;
КонецФункции
НачалоДня() у обеих дат — главное. Без него 31.01 23:00 и 01.02 01:00 дают 0,08 дня, а Цел() превращает их в ноль, хотя дата сменилась.ЗначениеЗаполнено() — незаполненный реквизит хранит 01.01.0001, разность с ним — около 740 тысяч дней. Функция возвращает 0; если вызывающему коду важно отличать «не заполнено» от «ноль дней», проверяйте даты до вызова.Включительно — для периодов «с … по …»: в марте 31 день, а разность 31.03 и 01.03 — 30. Для сроков и просрочек флаг не нужен.Получить разность дат программно, например дни до конца срока, на сервере: ДатыКлиентСервер.ДнейМеждуДатами(ТекущаяДатаСеанса(), СрокДействия). Положительное число — дней осталось, ноль — срок сегодня, отрицательное — дни просрочки. Обратная задача, сдвиг даты на N дней или месяцев, — в статье о ДОБАВИТЬКДАТЕ.
Разность дат в месяцах считается по частям даты: сколько раз сменился месяц и дошло ли в последнем месяце число до нужного. Короткие месяцы — по правилу «если нужного числа нет, хватает последнего дня»: так же ДобавитьМесяц() и ДОБАВИТЬКДАТЕ переносят 31 января плюс месяц на 28 или 29 февраля.
// Количество полных месяцев между датами, время суток не учитывается.
// Месяц полный, когда в последнем месяце наступило то же число, что у ДатаНачала.
// Если такого числа в месяце нет, хватает последнего дня:
// от 31.01 до 28.02 (в високосный год до 29.02) - один полный месяц.
Функция ПолныхМесяцевМеждуДатами(Знач ДатаНачала, Знач ДатаОкончания) Экспорт
Если Не ЗначениеЗаполнено(ДатаНачала) Или Не ЗначениеЗаполнено(ДатаОкончания) Тогда
Возврат 0;
КонецЕсли;
Если ДатаОкончания < ДатаНачала Тогда
Возврат -ПолныхМесяцевМеждуДатами(ДатаОкончания, ДатаНачала);
КонецЕсли;
// Сколько раз сменился месяц
Месяцев = (Год(ДатаОкончания) - Год(ДатаНачала)) * 12 + Месяц(ДатаОкончания) - Месяц(ДатаНачала);
// Последний месяц засчитываем, только если дошли до нужного числа
ДеньСрока = Мин(День(ДатаНачала), День(КонецМесяца(ДатаОкончания)));
Если День(ДатаОкончания) < ДеньСрока Тогда
Месяцев = Месяцев - 1;
КонецЕсли;
Возврат Месяцев;
КонецФункции
// Полные годы между датами по тому же правилу: стаж, срок договора, выслуга.
Функция ПолныхЛетМеждуДатами(Знач ДатаНачала, Знач ДатаОкончания) Экспорт
// Цел отбрасывает дробную часть, в том числе у отрицательного числа
Возврат Цел(ПолныхМесяцевМеждуДатами(ДатаНачала, ДатаОкончания) / 12);
КонецФункции
// Возраст в полных годах на дату. Родившийся 29 февраля
// в невисокосный год становится на год старше 1 марта.
Функция ВозрастНаДату(Знач ДатаРождения, Знач ДатаНа) Экспорт
Если Не ЗначениеЗаполнено(ДатаРождения) Или НачалоДня(ДатаНа) < НачалоДня(ДатаРождения) Тогда
Возврат 0;
КонецЕсли;
Лет = Год(ДатаНа) - Год(ДатаРождения);
// Месяц и день как число ММДД: 15 марта - 315, 29 февраля - 229.
// День рождения в году ДатаНа ещё не наступил - последний год неполный.
Если Месяц(ДатаНа) * 100 + День(ДатаНа) < Месяц(ДатаРождения) * 100 + День(ДатаРождения) Тогда
Лет = Лет - 1;
КонецЕсли;
Возврат Лет;
КонецФункции
| Начало | Окончание | Функция | Результат |
|---|---|---|---|
| 15.01.2025 | 14.02.2025 | ПолныхМесяцевМеждуДатами | 0 — 15-е ещё не наступило |
| 31.01.2025 | 28.02.2025 | ПолныхМесяцевМеждуДатами | 1 — 31-го в феврале нет, хватает 28-го |
| 28.02.2025 | 28.03.2025 | ПолныхМесяцевМеждуДатами | 1 — важно число, а не «последний день» |
| 10.06.2000 | 09.06.2025 | ПолныхЛетМеждуДатами | 24 — до 25 лет не хватает дня |
| 29.02.2024 | 28.02.2025 | ПолныхЛетМеждуДатами | 1 |
| 29.02.2024 | 28.02.2025 | ВозрастНаДату | 0 — день рождения наступит 1 марта |
Месяцев = (Год(...) - Год(...)) * 12 + ... — число смен месяца, дальше одна проверка числа без цикла.ДеньСрока = Мин(...) — правило коротких месяцев: от 31 января полный месяц наступает 28 (в високосный год 29) февраля.ВозрастНаДату() сравнивает месяц и день числом ММДД, а не делит дни на 365 — високосные годы ей не мешают.ПолныхЛетМеждуДатами() засчитывает год 28 февраля, ВозрастНаДату() — 1 марта. Какой вариант нужен, решает постановка задачи; если для возраста нужно 28 февраля, считайте его через ПолныхЛетМеждуДатами().Для стажа и сроков договора нужен ответ вида «2 года 1 месяц 24 дня». Полные месяцы уже посчитаны, остаток в днях берётся от даты, до которой они дошли.
// Интервал «лет, месяцев, дней» для стажа, срока договора, возраста ребёнка.
// Включительно = Истина - последний день входит в период:
// с 01.03.2024 по 28.02.2025 включительно - ровно 1 год.
// Пустые даты или даты в обратном порядке - все части равны нулю.
Функция РазностьЛетМесяцевДней(Знач ДатаНачала, Знач ДатаОкончания, Знач Включительно = Ложь) Экспорт
Результат = Новый Структура("Лет, Месяцев, Дней", 0, 0, 0);
Если Не ЗначениеЗаполнено(ДатаНачала) Или Не ЗначениеЗаполнено(ДатаОкончания) Тогда
Возврат Результат;
КонецЕсли;
Начало = НачалоДня(ДатаНачала);
Окончание = НачалоДня(ДатаОкончания);
Если Включительно Тогда
Окончание = Окончание + 86400; // граница - начало следующего дня
КонецЕсли;
Если Окончание <= Начало Тогда
Возврат Результат;
КонецЕсли;
Месяцев = ПолныхМесяцевМеждуДатами(Начало, Окончание);
Результат.Лет = Цел(Месяцев / 12);
Результат.Месяцев = Месяцев % 12;
// Остаток в днях - от даты, до которой дошли полные месяцы
Результат.Дней = (Окончание - ДатаЧерезМесяцев(Начало, Месяцев)) / 86400;
Возврат Результат;
КонецФункции
// Дата через заданное число месяцев по правилу ПолныхМесяцевМеждуДатами:
// если нужного числа в месяце нет, берётся последний день. Время отбрасывается.
Функция ДатаЧерезМесяцев(Знач ИсходнаяДата, Знач Месяцев) Экспорт
НомерМесяца = Год(ИсходнаяДата) * 12 + Месяц(ИсходнаяДата) - 1 + Месяцев;
ПервоеЧисло = Дата(Цел(НомерМесяца / 12), НомерМесяца % 12 + 1, 1);
ЧислоМесяца = Мин(День(ИсходнаяДата), День(КонецМесяца(ПервоеЧисло)));
Возврат ПервоеЧисло + (ЧислоМесяца - 1) * 86400;
КонецФункции
Окончание + 86400 при Включительно — последний день периода отработан, поэтому граница — начало следующего дня. С 15.01.2023 по 10.03.2025 включительно — 2 года 1 месяц 24 дня, без флага — на день меньше.ДатаЧерезМесяцев() собирает дату из частей по тому же правилу, что и ПолныхМесяцевМеждуДатами(). ДобавитьМесяц() почти всегда даст то же, но её поведение на последних числах месяца в синтакс-помощнике не описано, а при расхождении правил остаток дней может уйти в минус.Модуль формы вызывает клиент-серверные функции прямо на клиенте. Склонение даёт платформенная СтрокаСЧислом() (с версии 8.3.10), подробнее о шаблонах — в статье о склонении числительных.
// Модуль формы документа со стажем сотрудника (имена реквизитов условные).
// Реквизит формы Стаж (Строка) выводится надписью и пересчитывается при изменении дат.
&НаКлиенте
Процедура ДатаНачалаПриИзменении(Элемент)
ОбновитьСтаж();
КонецПроцедуры
&НаКлиенте
Процедура ДатаОкончанияПриИзменении(Элемент)
ОбновитьСтаж();
КонецПроцедуры
&НаКлиенте
Процедура ОбновитьСтаж()
Если Не ЗначениеЗаполнено(Объект.ДатаНачала) Тогда
Стаж = "";
Возврат;
КонецЕсли;
ДатаОкончания = Объект.ДатаОкончания;
Если Не ЗначениеЗаполнено(ДатаОкончания) Тогда
// ТекущаяДатаСеанса() в тонком клиенте недоступна, в БСП есть замена
ДатаОкончания = ОбщегоНазначенияКлиент.ДатаСеанса();
КонецЕсли;
// Клиент-серверный модуль работает на клиенте без вызова сервера
Интервал = ДатыКлиентСервер.РазностьЛетМесяцевДней(Объект.ДатаНачала, ДатаОкончания, Истина);
Стаж = ПредставлениеИнтервала(Интервал);
КонецПроцедуры
// «2 года 1 месяц 24 дня», нулевые части пропускаются.
&НаКлиентеНаСервереБезКонтекста
Функция ПредставлениеИнтервала(Интервал)
Части = Новый Массив;
Если Интервал.Лет > 0 Тогда
Части.Добавить(СтрокаСЧислом(";%1 год;;%1 года;%1 лет;%1 года",
Интервал.Лет, ВидЧисловогоЗначения.Количественное));
КонецЕсли;
Если Интервал.Месяцев > 0 Тогда
Части.Добавить(СтрокаСЧислом(";%1 месяц;;%1 месяца;%1 месяцев;%1 месяца",
Интервал.Месяцев, ВидЧисловогоЗначения.Количественное));
КонецЕсли;
Если Интервал.Дней > 0 Или Части.Количество() = 0 Тогда
// ЧН=0: без него ноль подставится пустой строкой
Части.Добавить(СтрокаСЧислом(";%1 день;;%1 дня;%1 дней;%1 дня",
Интервал.Дней, ВидЧисловогоЗначения.Количественное, "ЧН=0"));
КонецЕсли;
Возврат СтрСоединить(Части, " ");
КонецФункции
ТекущаяДатаСеанса() в тонком клиенте недоступна, поэтому текущая дата берётся из БСП — ОбщегоНазначенияКлиент.ДатаСеанса(). В конфигурации без БСП получите дату серверным вызовом или считайте стаж на сервере.
Для часов и минут секунды нужно разложить на части. Удобный на вид трюк Формат(Дата(1, 1, 1) + Секунд, "ДФ=ЧЧ:мм") ломается дважды: после 24 часов счёт начинается заново, а ноль секунд — это пустая дата, и Формат() вернёт пустую строку.
// Секунды - в строку «Ч:ММ:СС». Часы не обрезаются на 24, знак сохраняется:
// 93784 -> «26:03:04», -90 -> «-0:01:30».
Функция ДлительностьСтрокой(Знач Секунд) Экспорт
Знак = "";
Если Секунд < 0 Тогда
Знак = "-";
Секунд = -Секунд;
КонецЕсли;
Секунд = Цел(Секунд); // доли секунды (после деления миллисекунд на 1000) отбрасываем
Часов = Цел(Секунд / 3600);
Минут = Цел((Секунд % 3600) / 60);
Секунд = Секунд % 60;
// ЧН - представление нуля: без него Формат(0) возвращает пустую строку
Возврат Знак + Формат(Часов, "ЧН=0; ЧГ=0")
+ ":" + Формат(Минут, "ЧЦ=2; ЧН=00; ЧВН=")
+ ":" + Формат(Секунд, "ЧЦ=2; ЧН=00; ЧВН=");
КонецФункции
// Продолжительность смены в секундах по времени начала и окончания.
// Реквизиты с составом даты «Время» хранят дату 01.01.0001, и смена 22:00-06:00
// при простом вычитании даёт минус 16 часов. Смена - не длиннее суток.
Функция ДлительностьСменыСекунд(Знач ВремяНачала, Знач ВремяОкончания) Экспорт
// Оставляем только время суток: функция работает и с полными датами
Начало = ВремяНачала - НачалоДня(ВремяНачала);
Окончание = ВремяОкончания - НачалоДня(ВремяОкончания);
Секунд = Окончание - Начало;
Если Секунд <= 0 Тогда
Секунд = Секунд + 86400; // переход через полночь; одинаковое время - ровно сутки
КонецЕсли;
Возврат Секунд;
КонецФункции
// Разность в часах с дробной частью для табеля и расчёта по ставке:
// 7 ч 45 мин -> 7.75.
Функция ЧасовМеждуДатами(Знач Начало, Знач Окончание, Знач Знаков = 2) Экспорт
Возврат Окр((Окончание - Начало) / 3600, Знаков);
КонецФункции
Секунд % 3600 — остаток от деления, секунды сверх целых часов. У % и / один приоритет, скобки — для читаемости.ЧЦ=2; ЧН=00; ЧВН= — две цифры с ведущим нулём, а ноль — «00». Для часов ЧГ=0 убирает разделитель групп, иначе 1234 часа выведутся как «1 234».ДлительностьСменыСекунд() — для реквизитов с составом даты «Время». Функция отбрасывает дату и прибавляет сутки, если окончание не позже начала: смена с 22:00 до 06:00 — 28 800 секунд, то есть «8:00:00».Вычитание дат меряет время с шагом в секунду. Для замера длительности операции берите ТекущаяУниверсальнаяДатаВМиллисекундах(): она возвращает число и доступна и на клиенте, и на сервере.
&НаКлиенте
Процедура ЗаполнитьТовары(Команда)
// Число миллисекунд UTC: разность точнее, чем у дат, где шаг - секунда
Старт = ТекущаяУниверсальнаяДатаВМиллисекундах();
ЗаполнитьТоварыНаСервере();
Прошло = ТекущаяУниверсальнаяДатаВМиллисекундах() - Старт;
ПоказатьОповещениеПользователя("Товары заполнены", ,
"Заняло " + Формат(Прошло / 1000, "ЧДЦ=1; ЧН=0") + " с");
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьТоварыНаСервере()
// запрос и заполнение табличной части
КонецПроцедуры
Для разности дат в БСП (УТ 11, КА 2, ERP 2) пригодятся две серверные функции:
КалендарныеГрафики.РазностьДатПоКалендарю(ГрафикРаботы, ДатаНачала, ДатаОкончания, ВызыватьИсключение) — сколько рабочих дней в периоде по производственному календарю (справочник «Производственные календари») или графику работы (справочник «Графики работы»);ОбщегоНазначения.ИнтервалВремениСтрокой(ВремяНачала, ВремяОкончания) — текст «3 дня», «более месяца», «более года» или «менее одного дня».// Общий модуль с флажком «Сервер», конфигурации на БСП 3.1 (УТ 11, КА 2, ERP 2).
// Просрочка от срока до даты контроля: календарные и рабочие дни плюс текст.
Функция ОписаниеПросрочки(Знач Срок, Знач ДатаКонтроля) Экспорт
Результат = Новый Структура("КалендарныхДней, РабочихДней, Представление", 0, 0, "");
Если Не ЗначениеЗаполнено(Срок) Или НачалоДня(ДатаКонтроля) <= НачалоДня(Срок) Тогда
Возврат Результат; // срока нет или он ещё не прошёл
КонецЕсли;
// Просрочка начинается со дня, следующего за сроком
ПервыйДень = НачалоДня(Срок) + 86400;
ПоследнийДень = НачалоДня(ДатаКонтроля);
Результат.КалендарныхДней = (ПоследнийДень - ПервыйДень) / 86400 + 1;
Календарь = КалендарныеГрафики.ОсновнойПроизводственныйКалендарь();
Если ЗначениеЗаполнено(Календарь) Тогда
// Рабочие и предпраздничные дни с первой даты по вторую включительно.
// Ложь - вместо исключения вернуть Неопределено, если календарь не заполнен.
Результат.РабочихДней = КалендарныеГрафики.РазностьДатПоКалендарю(Календарь,
ПервыйДень, ПоследнийДень, Ложь);
Иначе
Результат.РабочихДней = Неопределено; // календарь в базе не настроен
КонецЕсли;
// «3 дня», «более месяца», «более года»
Результат.Представление = ОбщегоНазначения.ИнтервалВремениСтрокой(НачалоДня(Срок), ПоследнийДень);
Возврат Результат;
КонецФункции
ВызыватьИсключение = Ложь возвращает Неопределено. В УТ 11.4 (БСП 3.1.5) такой проверки нет: незагруженный год молча даст ноль рабочих дней.ИнтервалВремениСтрокой() без второго параметра считает до ТекущаяДатаСеанса().(ДатаОкончания - ДатаНачала) / 86400 даёт 30,99999 вместо 31, а Цел() — 30. Используйте ДнейМеждуДатами(ДатаНачала, ДатаОкончания, Истина).Цел(Дней / 365) прибавляет год раньше срока: родившемуся 01.01.2000 на 31.12.2003 он даст 4 года из-за 29 февраля. Цел(Дней / 365.25) ошибается в обратную сторону: ровно через год после 01.03.2001 даст 0.ТекущаяДата() на клиенте вернёт часы компьютера пользователя, они могут отличаться от серверных.ТекущаяУниверсальнаяДата() или время из внешней системы с «Z» против даты документа — разность съедет на смещение пояса, для Москвы на 3 часа. На сервере приводите к местному времени: МестноеВремя(ДатаUTC, ЧасовойПоясСеанса()); без второго параметра берётся пояс компьютера.ЗначениеЗаполнено().РазностьДатПоКалендарю() считает дни периода включительно, и от понедельника до вторника получается 2. Чтобы сдвинуть дату на N рабочих дней, есть КалендарныеГрафики.ДатаПоКалендарю().ИнтервалВремениСтрокой() получила даты в обратном порядке, сравните их до вызова. «График работы "…" не заполнен на период …» — в производственном календаре нет данных на год периода: заполните его или передайте ВызыватьИсключение = Ложь и обработайте Неопределено.Приведите обе даты к началу дня и разделите на 86 400: (НачалоДня(Дата2) - НачалоДня(Дата1)) / 86400. Результат целый; если вторая дата раньше первой — отрицательный.
Полные месяцы — функцией ПолныхМесяцевМеждуДатами(). Если нужно, сколько раз сменился календарный месяц (так считает РАЗНОСТЬДАТ с типом МЕСЯЦ в запросе), хватит формулы (Год(Д2) - Год(Д1)) * 12 + Месяц(Д2) - Месяц(Д1): между 31.01 и 01.02 она даёт 1.
Есть две, обе серверные: КалендарныеГрафики.РазностьДатПоКалендарю() считает рабочие дни, ОбщегоНазначения.ИнтервалВремениСтрокой() возвращает текст «N дней», «более месяца», «более года». Полные месяцы, возраст и часы удобнее считать функциями из этой статьи.
Окр((Дата2 - Дата1) / 3600, 2) или ЧасовМеждуДатами(): 7 часов 45 минут — 7,75. Для табеля и расчёта по ставке это удобнее, чем «7:45»; для вывода пользователю — наоборот, тогда нужна ДлительностьСтрокой().
Если разность дат — часть большой задачи (отчёт о просрочках в рабочих днях, контроль сроков договоров, расчёт стажа по своей методике), её быстрее отдать программисту. Такие отчёты на СКД и обработки делаются в рамках услуги отчёты и обработки 1С. Опишите задачу на странице «Разместить задание» — оценка бесплатная, ответ в течение рабочего дня.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.