Даты

Материал из Eludia
Перейти к: навигация, поиск

Содержание

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

Общие проблемы

Дата на первый взгляд кажется всего лишь числовым значением: количеством принятых атомарных единиц времени (суток, секунд, миллисекунд) от "начала эпохи" (Р. Х., 1 января 1970 г... не важно). Однако проблема заключается в том, что такие значения годятся лишь для внутреннего представления, но не для интерфейса пользователя. Юлианский день в быту абсолютно неприменим.

Високосы

Нормальные люди считают на годы / месяцы / дни — и вот тут начинается сущий ад для программиста, поскольку год состоит из иррационального числа дней. Физически. А потому любой календарь, опирающийся одновременно на годы и на дни как на единицы измерения времени, обязан предусматривать високосы.

Отсюда следует, что "месяц" в принципе не может быть единицей измерения времени: для каждого конкретного месяца длительность в сутках — это функция, вообще говоря, от номера года и номера месяца (в принятом календаре). В результате скалярное число тиков (point in time), пригодное для простых арифметических действий, заменяется в приложении на объект со сложным набором специфических методов.

Как считают кадровики

Несмотря на то, что было сказано в предыдущем разделе, есть люди, которые считают "месяц" единицей измерения времени, такой же, как "сутки": это сотрудники отделов кадров. В отечественном кадровом делопроизводстве принят учёт стажа сотрудников в виде триплетов "годов / месяцев / дней", которые получаются вычитанием дат (скажем, увольнения и приёма на работу).

Например, сотрудник проработавший с 01.02.2009 по 28.02.2009 (уволенный с 01.03.2009), получит "0 лет 1 месяц 0 дней" стажа, как и тот, кто числился на должности с 01.08.2009 по 31.08.2009 (уволившись 01.09.2009). При последующем сложении каждый такой "месяц" принимается равным 30 дням.

Почему-то предполагается, что погрешности в целом должны компенсироваться. Но на практике попытка рассчитать стаж астрономически приводит к тому, что вычисленная величина расходится с содержимым документов, на которых уже стоят подписи и печати. А это посерьёзнее движения планет. Это документально подтверждённые права на отпуска и прочие социальные блага.

Таким образом, конкретно для кадрового учёта арифметику дат необходимо реализовывать по месту. Вероятно, это не единственный подобный пример.

"Рабочие дни"

Наталкиваясь в ТЗ на формулировку вида "приказ должен утверждаться не более, чем за 10 дней", надо непременно уточнять, идёт ли речь об астрономических или о рабочих днях. В первом случае всё более-менее стандартно, во втором — у вас есть хороший повод потребовать много дополнительного времени и денег. Как нетрудно сообразить, вследствие официально поддерживаемой практики "переносов рабочих дней", прямое вычисление признака "рабочий / выходной" невозможно в принципе. К тому же список государственных праздников меняется каждый год. И к ним вполне могут прибавляться отраслевые / корпоративные.

Поэтому первое же упоминание "рабочих дней" как единиц измерения интервалов времени должно автоматически приводить к доработке ТЗ разделом "Календарь праздников и переносов" и упоминанием его содержимого во всех подходящих местах.

Циклические события

Упомянув "Календарь праздников и переносов", стоит сказать пару слов о календарях с повторяющимися событиями. Соответствующая схема данных обычно подразумевает минимум 2 таблицы:

  • события с конкретными датами (скажем, праздники);
  • генераторы повторяющихся событий (в нашем случае это месяцы и дни ежегодных праздников).

Как правило, среди событий могут быть как связанные с циклическими генераторами (Новый год), так и одноразовые, вводимые напрямую (юбилей чего-то, вдруг признанного важным). В рассматриваемом случае праздников можно обойтись относительно простой структурой данных (день / месяц), но в пределе вам может понадобиться полный ICal.

При использовании календаря с повторениями необходимо реализовать процедуру актуализации на заданный промежуток времени. Скажем, для расчёта значения "ДД.ММ.ГГГГ + 10 рабочих дней" убедиться, что для каждого периодического праздника сгенерировано событие на год ГГГГ. Если вычисленная дата выйдет за пределы ГГГГ, необходимо актуализировать ГГГГ + 1 и повторить расчёт. И так далее, пока, наконец, результирующая дата не уложится в полностью обсчитанный промежуток. Всегда стоит предусматривать явный ограничитель (более 10 лет — ошибка): ведь никто не мешает объявить каждый день ежегодным праздником. В этом случае вы получите вечный цикл со 100% загрузкой CPU.

Самое сладкое в таких ситуациях — пересчёт событий при изменении генератора. Скажем, вычёркивание определённого дня из списка "красных дат календаря" должно отменять все связанные с ним наперёд рассчитанные события (по крайней мере в будущем относительно момента операции удаления), что может привести к каскадному пересчёту всех дат (те же deadline'ы на визирование), которые были получены в контексте этих событий. Или не привести (потому что подписи на бумаге уже стоят): в каждом случае необходимо уточнять отдельно.

Работа с датами в Eludia.pm

Формат ISO

Во всех современных Eludia-приложениях при передаче данных между perl-процедурами и сервером БД используется формат 'YYYY-MM-DD hh:mm:ss', который в MySQL называется 'ISO' (на самом деле, ISO 8601 как таковой заметно сложнее). Этот формат обладает следующими ценными свойствами:

  • его воспринимает любя реализация SQL;
  • он обеспечивает соответствие алфавитного порядка и хронологического.

Последнее обстоятельство периодически используется в процедурах обработки данных, когда требуется сортировка и фильтрация списков записей, уже извлечённых из БД, на основании значений полей-дат.

Итак, даты в процедурах представляются в формате ISO. Однако в интерфейсе пользователя они должны фигурировать как строки ДД.ММ.ГГГГ. Преобразования между форматами необходимо осуществлять явно, в приложении.

Выборка данных

Функция sql предусматривает несколько специальных механизмов, удобных для решения многих типовых задач, связанных с типом "дата/время", такие как:

Ввод данных

Неотъемлемая часть грамотного ввода — проверка данных. Специально для этой стадии предусмотрена функция vld_date. Строка

vld_date ('dt_start', 1);

не только гарантирует непустоту $_REQUEST {_dt_start} и соответствие входного значения формату ДД.ММ.ГГГГ, но и "разворачивает" его в ГГГГ-ММ-ДД. Кроме того, vld_date поддерживает ввод неполных дат. Например, строка "1 3" превращается в 1 марта текущего года.

Вывод данных

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

__d ($i, 'dt', 'dt_created');

Арифметика дат

Самая распространённая операция в арифметике дат — сдвиг на заданный интервал. Для неё в API Eludia.pm предусмотрена функция dt_add.

Для прочих вычислений, связанных с датами, в Eludia-приложениях обычно напрямую используется модуль Date::Calc. Его функции требуют оперируют с датами, представленными в виде массивов ($y, $m, $d). При проверке данных %_REQUEST такой массив можно получить на выходе у вышеупомянутой vld_date, а для произвольного строкового значения — dt_y_m_d.

Сериализовать же массив в форматированную дату удобно процедурами dt_iso и dt_dmy.

Полезные запросы

Рассмотрим таблицу schedule с полями:

id_user
ссылка на сотрудника (пользователя системы);
dt_start
начало периода (непустая дата);
dt_finish
окончание периода (NULL соответствует потенциально бесконечному значению "по нынешнее время").

Поиск коллизии

Пусть $_REQUEST {_dt_start} и $_REQUEST {_dt_finish} — даты начала и окончания периода, который планируется записать в таблицу. На этапе проверки данных требуется найти любую запись для сотрудника $_REQUEST {_id_user}, изображающую отрезок времени, пересекающийся с заданным.

Все интервалы, в том числе и проверяемый, могут быть как ограниченными, так и открытыми (с пустым dt_finish).

my $conflict = sql (schedule => [

 ['id <> '                    => $_REQUEST {id}],       # чтобы исключить конфликт записи с самой собой

 [id_user                     => $_REQUEST {_id_user}], # может быть векторным
			
 ['dt_start .. dt_finish... ' => [$_REQUEST {_dt_start}, $_REQUEST {_dt_finish} || '9999-12-31']],

 [LIMIT                       => 1],
				
]);

Расчистка заданного интервала

В двух нижеследующих подразделах показано, как освободить отрезок расписания для сотрудника $_REQUEST {_id_user} с $_REQUEST {_dt_start} по $_REQUEST {_dt_finish}.

Удаление внутренних интервалов

Требуется удалить все записи, полностью (от начала до конца) укладывающиеся в интервал от $_REQUEST {_dt_start} до $_REQUEST {_dt_finish}.

sql (schedule => [
	
 DELETE, # если опустить DELETE, то записи будут не удалены, а возвращены как результат
		
 [ id_user        => $_REQUEST {_id_user}   ],
 ['dt_start  >= ' => $_REQUEST {_dt_start}  ],
 ['dt_finish <= ' => $_REQUEST {_dt_finish} ],
	
]);

Коррекция пограничных интервалов

Пусть требуется "подрезать справа" интервалы, содержащие $_REQUEST {_dt_start}, таким образом, чтобы они оканчивались строго до этой даты. Обозначим дату, предшествующую $_REQUEST {_dt_start} как $pre_start. Её можно вычислить следующим образом:

my $pre_start = dt_add ($_REQUEST {start_dt}, -1);

Коррекция правых концов отрезков осуществляется следующим запросом:

sql (schedule => [
	
 [UPDATE => [[dt_finish => $pre_start]]],
		
 [ id_user        => $_REQUEST {_id_user}  ],
 ['dt_start  <  ' => $_REQUEST {_dt_start} ],
 ['dt_finish >= ' => $_REQUEST {_dt_start} ],
	
]);

Аналогично для левых концов отрезков, относительно $_REQUEST {_dt_finish}:

sql (schedule => [
	
 [UPDATE => [[dt_start => $post_finish]]],
		
 [ id_user        => $_REQUEST {_id_user}  ],
 ['dt_start  <= ' => $_REQUEST {_dt_finish} ],
 ['dt_finish >  ' => $_REQUEST {_dt_finish} ],
	
]);

Карточка сотрудника с актуальной должностью

Рассмотрим таблицу posts (историю смены должностей) со следующими полями:

id_user
ссылка на справочник сотрудников users;
id_voc_post
ссылка на справочник должностей posts;
dt_start
первый день в должности;
dt_finish
последний день в должности (возможно, NULL).

Предположим, в каждый день для 1 сотрудника должность единственна (то есть коллизии предотвращаются на этапе ввода данных).

Требуется извлечь карточку сотрудника $id_user вместе с должностью, актуальной на дату $dt:

my $user = sql (users => $id_user, # здесь скаляр $id_user работает как набор фильтров [[id => $id_user]]
 
 [posts => [                       # это JOIN с локальными фильтрами
  [fake => 0],
  ['dt_start .. dt_finish...' => $dt],
 ]],

 'voc_posts'

);

После этого наименование должности доступно как $user -> {voc_post} -> {label}.

Более сложный запрос, связанный с кадровой историей и требующий специфической индексации, рассмотрен в разделе "Срез штатного расписания".

Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
Разработчику
Администратору
Инструменты