I'd approach this by building the generic data structure, and then mapping week-minutes on top of it.
The generic structure looks like this:
class OrderedRangeMap:
""" ranges must be contiguous ; 0..limit """
def __init__(self, limit, default_value=""):
self.ranges = [(0,default_value),(limit,None)]
def find(self, key):
# could do bsearch
# what if value < self.ranges[0]?
kv = self.ranges[0]
if key < kv[0]:
return None,0,False
# what if value = self.ranges[0]?
# what if value == vl[0]?
for i,kv in enumerate(self.ranges):
k = kv[0]
if key < k:
return kvp,i-1,False
if key == k:
return kv,i,True
kvp = kv
# off the end
return None, len(self.ranges)-1, False
def add(self, skey, ekey, value):
newblock = (skey,value)
oldblock,si,sx = self.find(skey)
endblock,ei,ex = self.find(ekey)
if sx: #if start match, replace the oldblock
self.ranges[si] = newblock
else: #else insert after the oldblock
# bump
si += 1
ei += 1
self.ranges.insert(si,newblock)
if si == ei:
# insert the split block after that
self.ranges.insert(si+1,(ekey,oldblock[1]))
else:
# different blocks
# end block starts at new end point
self.ranges[ei] = (ekey,endblock[1])
# delete any in between
del self.ranges[si+1:ei]
# is that it?
def __getitem__(self, key):
block,index,match = self.find(key)
if index >= len(self.ranges) - 1:
return block[0], block[0], block[1]
return block[0], self.ranges[index+1][0], block[1]
def test_orm():
orm = OrderedRangeMap(100, "B")
assert orm.ranges == [(0,"B"),(100,None)]
# s/e in same block
orm.add(10,20, "A")
assert orm.ranges == [(0,"B"),(10,"A"),(20,"B"),(100,None)]
# s/e in same blocks, matches
orm.add(10,13, "a")
assert orm.ranges == [(0,"B"),(10,"a"),(13, "A"),(20,"B"),(100,None)]
# more blocks
orm.add(30,50, "c")
assert orm.ranges == [(0,"B"),(10,"a"),(13, "A"),(20,"B"),(30,"c"),(50,"B"),(100,None)]
# s/e in different blocks, no matches
orm.add(15,33, "d")
assert orm.ranges == [(0,"B"),(10,"a"),(13, "A"),(15,"d"),(33,"c"),(50,"B"),(100,None)]
# s/e in different blocks, s matches
orm.add(15,44, "e")
assert orm.ranges == [(0,"B"),(10,"a"),(13, "A"),(15,"e"),(44,"c"),(50,"B"),(100,None)]
# s/e in different blocks, s & e matches
orm.add(13,50, "f")
assert orm.ranges == [(0,"B"),(10,"a"),(13, "f"),(50,"B"),(100,None)]
# NOT tested: add outside of original range
test_orm()
(Edited to add:)
The upper layer converts from datetime to week minutes
import datetime
class WeekShiftLabels:
# this is assuming Monday=0
week_minutes = 7*24*60
def __init__(self, default_label="?"):
self.orm = OrderedRangeMap(self.week_minutes, default_label)
def add(self, dow, starttime, endtime, label):
dm = dow * 24*60
st = dm + t2m(starttime)
et = dm + t2m(endtime)
self.orm.add(st, et, label)
def __getitem__(self, dt):
wm = dt2wm(dt)
block,index,match = self.find(wm)
if index >= len(self.ranges) - 1:
return None
return block[1]
class WSLI:
# This doesn't handle modulo week_minutes
def __init__(self, wsl, sdt, edt):
self.wsl = wsl
self.base = sdt - datetime.timedelta(days=sdt.weekday())
t = sdt.time()
self.base -= datetime.timedelta(hours=t.hour,minutes=t.minute)
self.i = dt2wm(sdt)
self.em = dt2wm(edt)
def __next__(self):
if self.i < 0:
raise StopIteration
block,index,match = self.wsl.orm.find(self.i)
if not block:
raise StopIteration # or something else
start = wm2dt(self.base, self.i)
end = self.wsl.orm.ranges[index+1][0]
if end >= self.em:
end = self.em
self.i = -1
else:
self.i = end
end = wm2dt(self.base, end)
return start, end, block[1]
def __iter__(self):
return self
def __call__(self, sdt, edt):
return self.WSLI(self, sdt, edt)
def dt2wm(dt):
t = dt.time()
return dt.weekday() * 24*60 + t.hour*60 + t.minute
def wm2dt(base,wm):
return base + datetime.timedelta(minutes=wm)
def t2m(t):
return t.hour*60 + t.minute
def test_wsl():
wsl = WeekShiftLabels("B")
st = datetime.time(hour=7)
et = datetime.time(hour=19)
for dow in range(0,6):
wsl.add(dow, st, et, "A")
r = list(wsl(datetime.datetime(2019, 10, 21, 18, 30), datetime.datetime(2019, 10, 22, 8, 0)))
assert len(r) == 3
assert r[0]==(datetime.datetime(2019, 10, 21, 18, 30), datetime.datetime(2019, 10, 21, 19, 0), 'A')
assert r[1]==(datetime.datetime(2019, 10, 21, 19, 0), datetime.datetime(2019, 10, 22, 7, 0), 'B')
assert r[2]==(datetime.datetime(2019, 10, 22, 7, 0), datetime.datetime(2019, 10, 22, 8, 0), 'A')
test_wsl()