I have shift
which is a datetime interval (a pair of datetime
s). My weeks have a labeled partition (every week is the same: divided into parts, and each part has a label). I want to split up shift
into labeled parts (i.e. into several subintervals), according to the partition of the week.
Example. Suppose shift
is the interval 2019-10-21 18:30
- 2019-10-22 08:00
, and the partition of the week is as follows: Monday to Friday 07:00 - 19:00 has label A
, and the rest of the week has label B
.
In this case the splitting of shift
should be the following list of labeled subintervals:
2019-10-21 18:30
- 2019-10-21 19:00
with label A
,2019-10-21 19:00
- 2019-10-22 07:00
with label B
, and2019-10-22 07:00
- 2019-10-22 08:00
with label A
.How do I do this in general?
Input: a datetime
interval (pair), and a labeled partition of the week (how to best represent this?)
Output: a list of labeled datetime
intervals (pairs).
Note that shift
can start in one week and end in another week (e.g. Sunday evening to Monday morning); each week does have the same labeled partition.
Here's a way to obtain the desired intervals:
from collections import namedtuple
from datetime import datetime, timedelta
import itertools as it
# Built-in as `it.pairwise` in Python 3.10+
def pairwise(iterable):
it = iter(iterable)
a = next(it, None)
for b in it:
yield (a, b)
a = b
def beginning_of_week(d: datetime) -> datetime:
''' Returns the datetime object for the beginning of the week the provided day is in. '''
return (d - timedelta(days=d.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
Partition = namedtuple('Partition', ('start', 'stop', 'label')) # output format
def _partition_shift_within_week(start: int, stop: int, partitions):
''' Splits the shift (defined by `start` and `stop`) into partitions within one week. '''
# Get partitions as ranges of absolute offsets from the beginning of the week in seconds
labels = (x for _, x in partitions)
absolute_offsets = it.accumulate(int(x.total_seconds()) for x, _ in partitions)
ranges = [range(x, y) for x, y in pairwise((0, *absolute_offsets))]
first_part_idx = [start in x for x in ranges].index(True)
last_part_idx = [stop in x for x in ranges].index(True)
for r, label in zip((ranges[i] for i in range(first_part_idx, last_part_idx + 1)), labels):
yield Partition(
timedelta(seconds=max(r.start, start)), # start of subinterval
timedelta(seconds=min(r.stop, stop)), # end of the subinterval
label
)
def _partition_shift_unjoined(shift, partitions):
''' Partitions a shift across weeks with partitions unjoined at the week edges. '''
start_monday = beginning_of_week(shift[0])
stop_monday = beginning_of_week(shift[1])
seconds_offsets = (
int((shift[0] - start_monday).total_seconds()),
*[604800] * ((stop_monday - start_monday).days // 7),
int((shift[1] - stop_monday).total_seconds()),
)
for x, y in pairwise(seconds_offsets):
num_weeks, x = divmod(x, 604800)
for part in _partition_shift_within_week(x, y - (y == 604800), partitions):
weeks_offset = timedelta(weeks=num_weeks)
yield Partition(
start_monday + weeks_offset + part.start,
start_monday + weeks_offset + part.stop,
part.label
)
def partition_shift(shift, partitions):
''' Partitions a shift across weeks. '''
results = []
for part in _partition_shift_unjoined(shift, partitions):
if len(results) and results[-1].label == part.label:
results[-1] = Partition(results[-1].start, part.stop, part.label)
else:
results.append(part)
return results
Usage example:
shift = (datetime(2019, 10, 21, 18, 30), datetime(2019, 10, 22, 8, 0))
# Partitions are stored as successive offsets from the beginning of the week
partitions = (
(timedelta(hours=7), 'B'), # Monday morning (midnight to 07:00)
(timedelta(hours=12), 'A'),
(timedelta(hours=12), 'B'), # Monday night & Tuesday morning (til 07:00)
(timedelta(hours=12), 'A'),
(timedelta(hours=12), 'B'), # Tuesday night & Wednesday morning (til 07:00)
(timedelta(hours=12), 'A'),
(timedelta(hours=12), 'B'), # Wednesday night & Thursday morning (til 07:00)
(timedelta(hours=12), 'A'),
(timedelta(hours=12), 'B'), # Thursday night & Friday morning (til 07:00)
(timedelta(hours=12), 'A'),
(timedelta(hours=53), 'B'), # Friday night & the weekend
)
for start, end, label in partition_shift(shift, partitions):
print(f"'{start}' - '{end}', label: {label}")
Output:
'2019-10-21 18:30:00' - '2019-10-21 19:00:00', label: A
'2019-10-21 19:00:00' - '2019-10-22 07:00:00', label: B
'2019-10-22 07:00:00' - '2019-10-22 08:00:00', label: A
This approach assumes that the partitions are input as successive offsets from the beginning of that week. The question did not specify how the partitions would be provided, so I choose to use this format. It's nice because it guarantees they do not overlap, and uses time deltas instead of being fixed to some particular date.
Converting other ways of specifying partitions into this one, or adapting this answer to work with other ways of specifying partitions has been left as an exercise to the reader.
Here's another usage example, using the same partitions
as before, but a shift
that starts in the previous week, thereby demonstrating that this approach works even when the shift spans multiple weeks.
shift = (datetime(2019, 10, 19, 18, 30), datetime(2019, 10, 22, 8, 0))
for start, end, label in partition_shift(shift, partitions):
print(f"'{start}' - '{end}', label: {label}")
Output:
'2019-10-19 18:30:00' - '2019-10-21 07:00:00', label: B
'2019-10-21 07:00:00' - '2019-10-21 19:00:00', label: A
'2019-10-21 19:00:00' - '2019-10-22 07:00:00', label: B
'2019-10-22 07:00:00' - '2019-10-22 08:00:00', label: A
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With