Условие
Витрина режимов работы сотрудников (work_mode):
tab_num, start_date, finish_date,
wday_type01..wday_type05, -- режим для пн..пт
wplace_type, -- категория рабочего места
end_da -- альтернативная дата окончания
Преобразовать витрину к формату (tab_num, ymd_date, to_be_at_office) за период 01.09.2020 – 31.12.2020, по всем датам диапазона на каждого сотрудника.
Правила:
- Если
finish_date = '31.12.9999'иend_daне пуст — использоватьend_da. to_be_at_office ∈ {0, 1, NULL}:0— должен быть дома;1— должен быть в офисе;NULL— режима нет на эту дату ИЛИ это сб/вс.
- Для
wplace_type ∈ {3, 4}:3— «неделя через неделю»: 1-я неделя режима — полностью дистанционная, 2-я — в офисе, и т.д.4— «две недели через две»: первые 2 недели — дист., следующие 2 — офис, и т.д.
Решение
Подход
- Календарная таблица дат периода + сотрудники → cross join.
- Для каждой даты найти активный режим (start_date ≤ d ≤ effective_finish).
- Применить правила: будний день → берём флаг из
wday_typeNN, выходной → NULL. - Для wplace_type 3/4 — посчитать номер «недели режима» и выбрать дист./офис.
Реализация (PostgreSQL)
WITH calendar AS (
SELECT generate_series('2020-09-01'::date, '2020-12-31'::date, INTERVAL '1 day')::date AS ymd_date
),
mode_norm AS (
SELECT
tab_num,
start_date,
-- эффективная дата окончания
CASE WHEN finish_date = DATE '9999-12-31' AND end_da IS NOT NULL THEN end_da
ELSE finish_date END AS finish_eff,
wday_type01, wday_type02, wday_type03, wday_type04, wday_type05,
wplace_type
FROM work_mode
),
joined AS (
SELECT
e.tab_num,
cal.ymd_date,
m.start_date, m.finish_eff,
m.wday_type01, m.wday_type02, m.wday_type03, m.wday_type04, m.wday_type05,
m.wplace_type,
EXTRACT(ISODOW FROM cal.ymd_date)::int AS dow -- 1..7, пн=1
FROM (SELECT DISTINCT tab_num FROM mode_norm) e
CROSS JOIN calendar cal
LEFT JOIN mode_norm m
ON m.tab_num = e.tab_num
AND cal.ymd_date BETWEEN m.start_date AND m.finish_eff
)
SELECT
tab_num,
ymd_date,
CASE
WHEN dow IN (6, 7) THEN NULL -- сб/вс
WHEN start_date IS NULL THEN NULL -- режим не покрывает дату
-- режимы 3 и 4: недельная/двухнедельная ротация
WHEN wplace_type = 3 THEN
CASE
WHEN ((CAST(ymd_date - start_date AS int) / 7) % 2) = 0 THEN 0 -- 1-я неделя - дома
ELSE 1
END
WHEN wplace_type = 4 THEN
CASE
WHEN ((CAST(ymd_date - start_date AS int) / 14) % 2) = 0 THEN 0
ELSE 1
END
ELSE
-- для wplace_type 0/1/2: смотрим конкретный день недели
CASE dow
WHEN 1 THEN CASE WHEN wday_type01 = 0 THEN 1 WHEN wday_type01 IN (1,2,3) THEN 0 END
WHEN 2 THEN CASE WHEN wday_type02 = 0 THEN 1 WHEN wday_type02 IN (1,2,3) THEN 0 END
WHEN 3 THEN CASE WHEN wday_type03 = 0 THEN 1 WHEN wday_type03 IN (1,2,3) THEN 0 END
WHEN 4 THEN CASE WHEN wday_type04 = 0 THEN 1 WHEN wday_type04 IN (1,2,3) THEN 0 END
WHEN 5 THEN CASE WHEN wday_type05 = 0 THEN 1 WHEN wday_type05 IN (1,2,3) THEN 0 END
END
END AS to_be_at_office
FROM joined
ORDER BY tab_num, ymd_date;Логика «1-я неделя режима»
Дни от start_date целочисленно делим на 7 (для wplace_type=3) или 14 (для wplace_type=4). Чётный частный = «первый интервал режима» (дистант), нечётный = «второй интервал» (офис).
Уточнение из задачи: для wplace_type=3 «1-я (даже неполная) неделя — полностью дистанционная». То есть отсчёт ведётся от start_date, а не от понедельника. Формула с (d - start_date) / 7 это и обеспечивает.
Pandas-альтернатива
import pandas as pd
calendar = pd.date_range('2020-09-01', '2020-12-31', freq='D')
emp = work_mode['tab_num'].unique()
grid = pd.MultiIndex.from_product([emp, calendar], names=['tab_num', 'ymd_date']).to_frame(index=False)
m = work_mode.copy()
m['finish_eff'] = m['finish_date'].mask(m['finish_date'] == pd.Timestamp('9999-12-31'),
m['end_da'])
# merge_asof / cross-join + filterПодводные камни
finish_date = 31.12.9999при сравнении дат может не совпасть с'9999-12-31'из-за TZ/типов. Сравнивайте какDATE.- Несколько режимов перекрываются. В исходных данных у
17345две записи:28.09–25.10и26.10–31.12. Если перекрытие — нужна логика приоритета (последний поstart_date?). generate_seriesв SQLite не работает; нужен рекурсивный CTE для календаря.- wday_type значения 1,2,3. Все они означают «дистант», но 2 и 3 — для wplace_type 3/4 соответственно. В формуле выше упрощено до
IN (1,2,3) → 0. - Праздники. В задании их игнорируют, но в реальности нужна таблица
holidays. - Производительность. Cross join сотрудников × дни → миллиарды строк. На больших данных — фильтровать заранее, считать только активные периоды.
Эталонный ответ
generate_series + cross join сотрудников × календарь, LEFT JOIN с mode_norm (с эффективной finish_date), CASE по дню недели и wplace_type. Для типов 3/4 — формула с (d - start_date) / 7 или /14 и % 2 для «дистант / офис».