Собесов

Сбербанк (DE) — преобразовать витрину режимов работы сотрудников в дневной формат

SQLPivot и календарьСложнаяMiddle

Условие

Витрина режимов работы сотрудников (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 — офис, и т.д.

Решение

Подход

  1. Календарная таблица дат периода + сотрудники → cross join.
  2. Для каждой даты найти активный режим (start_date ≤ d ≤ effective_finish).
  3. Применить правила: будний день → берём флаг из wday_typeNN, выходной → NULL.
  4. Для 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

Подводные камни

  1. finish_date = 31.12.9999 при сравнении дат может не совпасть с '9999-12-31' из-за TZ/типов. Сравнивайте как DATE.
  2. Несколько режимов перекрываются. В исходных данных у 17345 две записи: 28.09–25.10 и 26.10–31.12. Если перекрытие — нужна логика приоритета (последний по start_date?).
  3. generate_series в SQLite не работает; нужен рекурсивный CTE для календаря.
  4. wday_type значения 1,2,3. Все они означают «дистант», но 2 и 3 — для wplace_type 3/4 соответственно. В формуле выше упрощено до IN (1,2,3) → 0.
  5. Праздники. В задании их игнорируют, но в реальности нужна таблица holidays.
  6. Производительность. Cross join сотрудников × дни → миллиарды строк. На больших данных — фильтровать заранее, считать только активные периоды.

Эталонный ответ

generate_series + cross join сотрудников × календарь, LEFT JOIN с mode_norm (с эффективной finish_date), CASE по дню недели и wplace_type. Для типов 3/4 — формула с (d - start_date) / 7 или /14 и % 2 для «дистант / офис».

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти