So, I finally circled back to this yesterday, and I think I have a workable solution for my situation. It’s a little simpler than I thought it would be, actually. My needs were strictly week based, but I don’t think it would be too hard to increase the flexibility of the code to handle other units of time.
First, I made a table to store info about the schedule that included the schedule name, the origin date, and the length of the cycle in weeks.
Next, a table to store the actual shift data. The time ranges are the actual shift time ranges for the week of the origin date and the x weeks of the initial cycle. The id column ties it to the named schedule. The sample data I have in the table has just three days, Monday of the first week, Tuesday of the second week, and Wednesday of the third week. Makes it easier to see patterns for now
To use the system, first we need to determine the offset in weeks from the origin date to the start of the current cycle. For my system, the origin date of a schedule, and the first date of the current week will always be Mondays (the beginning of our weekly pay cycle). Postgresql can give us this info via query. Substitute the first date with the current week’s Monday, and the second date with the origin date of the schedule you are working with. The final /3
is the cycle length in weeks of the schedule. These substitutions could be by subquery, or variables provided by scripting.
select ((extract(epoch from '2021-4-26'::date) - extract(epoch from '2021-3-1'::date))/604800)::integer/3
result = 2
So, two complete cycles have occurred since the origin date. We can have postgres add an offset to the original shift patterns to project the shifts to the current date. This query will generate the current shift pattern (also queries the original data so I can see it to compare). The offset applied is the number of cycles X the length of the cycle. 2 cycles X 3 weeks = 6 weeks.
SELECT *, tstzrange(lower(scheduledshift) + '6 weeks'::interval, upper(scheduledshift)+'6 weeks'::interval) as shift
FROM employee_schedule_data
WHERE id = 1
ORDER BY lower(scheduledshift)
Result:
We can also use postgres to narrow the result to the week we are actually working in, instead of the complete 3 week pattern.
SELECT *, tstzrange(lower(scheduledshift) + '6 weeks'::interval, upper(scheduledshift)+'6 weeks'::interval) as shift
FROM employee_schedule_data
WHERE id = 1 and scheduledshift && tstzrange('2021-4-26'::date - '6 weeks'::interval, '2021-5-3'::date - '6 weeks'::interval)
ORDER BY lower(scheduledshift)
Result:
Going forward, I’m planning on wrapping this into a CTE and comparing the standard shift to actual time worked.
Edit to add another code snippet, this one to generate the repeating schedule across multiple cycles. Useful for displaying on a calendar, for example. The only variables to provide are the three items for the generate_series function. The first two are the min and max (cycle*weeks) to generate, and the third is the number of weeks in a cycle.
WITH offsets as (
select generate_series(3,9,3) as weekoffset
)
SELECT *,
tstzrange(lower(scheduledshift)+ (weekoffset* '1 week'::interval),upper(scheduledshift)+ (weekoffset* '1 week'::interval)) FROM employee_schedule_data
CROSS JOIN offsets
WHERE id = 1
Result:
Hope this solution is useful to someone in the future!