Как стать автором
Обновить

Производственный календарь своими руками в Postgresql

Время на прочтение5 мин
Количество просмотров14K
image

Здравствуйте, меня зовут Виктор и я разработчик в компании Gems Development. Я хочу рассказать, как мы реализовывали создание и заполнение производственного календаря в Postgresql.

Рабочий процесс зависит от официальных государственных и международных праздников. В корпоративных приложениях часто возникает задача расчета даты с учетом рабочих дней и выходных. Например, такие услуги, как «Выдача разрешения на строительство», «Выдача разрешения на ввод в эксплуатацию» должны быть оказаны в определенные сроки.

После проведения анализа задачи мы пришли к выводу, что в календаре достаточно хранить выходные и праздничные дни, т.к именно они представляют сложность для расчетов и могут меняться в соответствии с производственным календарем в каждом году.

Правила работы с календарем:

  • Определение рабочего дня зависит от того, находится ли дата в списке выходных и праздников или нет.
  • Если рабочий день в связи с переносом становится нерабочим, то он тоже записывается в таблицу.
  • Если выходной день является рабочим, то, соответственно, запись из этой таблицы удаляется.

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

create schema calendar;
set search_path=calendar;

Для поддержки подобных вычислений в базе данных была создана таблица:

CREATE TABLE d_holidays
(
  holiday_date date,
  comment character varying(1000),
  CONSTRAINT d_holidays_pk PRIMARY KEY (holiday_date)
);

А также индекс, позволяющий ускорить поиск нужной даты в таблице:
CREATE UNIQUE INDEX d_holidays_pk_idx
  ON d_holidays
  USING btree
  (holiday_date);

Для создания данных используем стандартную функцию из Postgresql, возвращающую множество. Проверим работу индекса, предварительно заполнив таблицу достаточно большим набором сгенерированных данных: все дни с 01.01.2017 по 31.12.2025.

insert into d_holidays(holiday_date)
select generate_series(to_date('01.01.2017','dd.mm.yyyy'),
            to_date('31.12.2025','dd.mm.yyyy'),
            '1 day');

У нас получилось 3287 записей, что соответствует количеству дней с указанных дат. Важно обратить внимание на то, чтобы в конфигурации Postgresql был включен параметр “autovacuum = on”.

Посмотрим план запроса:

explain(analyze)
select comment from d_holidays where holiday_date='09.09.2020';

Мы убедились, что индекс используется:

Index Scan using d_holidays_pk on d_holidays  (cost=0.28..8.30 rows=1 width=516) (actual time=0.017..0.018 rows=1 loops=1)
  Index Cond: (holiday_date = '2020-09-09'::date)
Planning time: 0.083 ms
Execution time: 0.044 ms

Расчет выходных дней будем проводить в рамках года. Проверив все даты с 1 января указанного года по 31 декабря выберем выходные дни и запишем их в таблицу. Для реализации алгоритма используем процедурный язык plpgsql и конструкцию анонимного блока.

Приготовим конструкцию, которая формирует дату на 1 января и на 31 декабря.

do
$$
declare
    calc_year integer:=2020; --Указанный год
    begin_date date; --переменная для начальной даты года
    end_date date; --Переменная для конечной даты годы
    dow integer; --номер дня недели. Пн-1,...,Сб-6,Вс-0
begin
    --Проверка года на корректность
    if calc_year between 1988 and 2099 then
        begin_date :=to_date('01.01.'||calc_year, 'dd.mm.yyyy');
        end_date :=to_date('31.12.'||calc_year, 'dd.mm.yyyy');
    end if;
end$$;

Для вычисления дня недели воспользуемся функцией extract, которая определит номер дня недели (с учетом того, что воскресенье — 0). Для определения наименования дня недели используем простую конструкцию case.

Получаем следующий скрипт:

do
$$
declare
    calc_year integer:=2020; --Указанный год
    begin_date date; --переменная для начальной даты года
    end_date date; --Переменная для конечной даты годы
    dow_value integer; --номер дня недели. Пн-1,...,Сб-6,Вс-0
begin
    --Проверка года на коорректность
    if calc_year between 1988 and 2099 then
        begin_date :=to_date('01.01.'||calc_year, 'dd.mm.yyyy');
        end_date :=to_date('31.12.'||calc_year, 'dd.mm.yyyy');
        
        while begin_date<=end_date
        loop
            raise info '%',to_char(begin_date,'dd.mm.yyyy');
            begin_date:=begin_date+interval '1 day';
            dow_value:=extract(dow from begin_date);
            if dow_value in(0,6) then
                insert into d_holidays(holiday_date,comment)
                        values(begin_date,
                            case when dow_value=6 then 'Суббота' else 'Воскресенье' end);
            end if;
        end loop;
    end if;
end$$;

Разберем алгоритм добавления праздничных дней:

insert into d_holidays(holiday_date,comment) 
    values(to_date('01.01.2020','dd.mm.yyyy'),'Новогодние каникулы') 
        on conflict(holiday_date) do update set comment=EXCLUDED.comment;
…        
insert into d_holidays(holiday_date,comment) 
    values(to_date('04.05.2020','dd.mm.yyyy'),'Перенос с 04.01.2020') 
        on conflict(holiday_date) do update set comment=EXCLUDED.comment;

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

delete from d_holidays where holiday_date=to_date('29.12.2018','dd.mm.yyyy'); 
delete from d_holidays where holiday_date=to_date('09.06.2018','dd.mm.yyyy'); 

Обратим внимание на конструкцию:

on conflict(holiday_date) do update set comment=EXCLUDED.comment;

Данное нововведение появилось в версии Postgresql 9.5 и позволяет эффективно обрабатывать конфликты при вставке записей. В нашем примере, если праздник уже является выходным, то чтобы избежать конфликта по первичному ключу при добавлении записи с уже существующей датой мы обновляем описание даты, не создавая дубликат. Подробнее об этом синтаксисе можно прочитать в документации:

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

do
$$
declare
    calc_year integer:=2020; --Указанный год
    begin_date date; --переменная для начальной даты года
    end_date date; --Переменная для конечной даты годы
    dow_value integer; --номер дня недели. Пн-1,...,Сб-6,Вс-0
begin
    --Проверка года на коорректность
    if calc_year between 1988 and 2099 then
        begin_date :=to_date('01.01.'||calc_year, 'dd.mm.yyyy');
        end_date :=to_date('31.12.'||calc_year, 'dd.mm.yyyy');
        
        while begin_date<=end_date
        loop
            raise info '%',to_char(begin_date,'dd.mm.yyyy');
            begin_date:=begin_date+interval '1 day';
            dow_value:=extract(dow from begin_date);
            if dow_value in(0,6) then
                insert into d_holidays(holiday_date,comment)
                        values(begin_date,
                            case when dow_value=6 then 'Суббота' else 'Воскресенье' end);
            end if;
        end loop;
        /*Блок праздников и переносов*/
        insert into d_holidays(holiday_date,comment) 
            values(to_date('01.01.2020','dd.mm.yyyy'),'Новогодние каникулы') 
            on conflict(holiday_date) do update set comment=EXCLUDED.comment;
            
        /*Опустим часть Insert для сокращения объема*/
            
        insert into d_holidays(holiday_date,comment) 
            values(to_date('05.11.2020','dd.mm.yyyy'),'День народного единства') 
            on conflict(holiday_date) do update set comment=EXCLUDED.comment;
            
        insert into d_holidays(holiday_date,comment) 
            values(to_date('04.05.2020','dd.mm.yyyy'),'Перенос с 04.01.2020') 
            on conflict(holiday_date) do update set comment=EXCLUDED.comment;
       /*Блок праздников и переносов*/
    end if;
end$$;

В настоящее время мы также реализовали логику заполнения и ведения производственного календаря в приложении через интерфейс и перенесли реализацию на C#. Также у нас есть возможность формировать отчеты календаря в Excel и LibreOffice.

image

Графический интерфейс календаря и сформированный отчет в Excel:

image

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

А как вы ведете производственный календарь?
Теги:
Хабы:
+5
Комментарии12

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн