event listeners

This commit is contained in:
Klaas van Schelven
2023-12-19 18:40:55 +01:00
parent 63c02c9ff5
commit ea6378c2d4
2 changed files with 127 additions and 15 deletions

View File

@@ -9,12 +9,22 @@ MAX_HOURS = 24
MAX_DAYS = 30
MAX_MONTHS = 12
MAX_YEARS = 5
MAX_TOTALS = 1
FOO_MIN = 1000, 1, 1, 0, 0
FOO_MAX = 3000, 12, "?", 23, 59
# TL for "tuple length", the length of the tuples for a given time-period
TL_TOTAL = 0
TL_YEAR = 1
TL_MONTH = 2
TL_DAY = 3
TL_HOUR = 4
TL_MINUTE = 5
def apply_n(f, n, v):
for i in range(n):
v = f(v)
@@ -43,35 +53,78 @@ def _prev_tup(tup):
def _inc(d, tup, n, max_age):
new_period = False
if tup not in d:
# evict
min_tup = apply_n(_prev_tup, max_age, tup)
d = {k: v for k, v in d.items() if d >= min_tup}
if len(d) > 0:
new_period = True
min_tup = apply_n(_prev_tup, max_age - 1, tup)
for k, v in list(d.items()):
if k < min_tup:
del d[k]
# default
d[tup] = 0
# inc
d[tup] += n
return new_period
class PeriodCounter(object):
def __init__(self):
self.total = 0
self.years = {}
self.months = {}
self.days = {}
self.hours = {}
self.minutes = {}
self.counts = {tuple_length: {} for tuple_length in range(TL_MINUTE + 1)}
self.event_listeners = {tuple_length: {} for tuple_length in range(TL_MINUTE + 1)}
def inc(self, datetime_utc, n=1):
tup = datetime_utc.timetuple()
self.total += n # self.forevers, ()
for tl, mx in enumerate([MAX_TOTALS, MAX_YEARS, MAX_MONTHS, MAX_DAYS, MAX_HOURS, MAX_MINUTES]):
new_period = _inc(self.counts[tl], tup[:tl], n, mx)
_inc(self.years, tup[:1], n, MAX_YEARS)
_inc(self.months, tup[:2], n, MAX_MONTHS)
_inc(self.days, tup[:3], n, MAX_DAYS)
_inc(self.hours, tup[:4], n, MAX_HOURS)
_inc(self.minutes, tup[:5], n, MAX_MINUTES)
event_listeners_for_tl = self.event_listeners[tl]
for ((how_many_periods, gte_threshold), (wbt, wbf, is_true)) in list(event_listeners_for_tl.items()):
if is_true:
if not new_period:
continue # no new period means: never becomes false, because no old period becomes irrelevant
if not self._get_event_state(tup[:tl], tl, how_many_periods, gte_threshold):
event_listeners_for_tl[(how_many_periods, gte_threshold)] = (wbt, wbf, False)
wbf()
else:
if self._get_event_state(tup[:tl], tl, how_many_periods, gte_threshold):
event_listeners_for_tl[(how_many_periods, gte_threshold)] = (wbt, wbf, True)
wbt()
def add_event_listener(self, period_name, how_many_periods, gte_threshold, when_becomes_true, when_becomes_false,
event_state=None, tup=None):
if len([arg for arg in [event_state, tup] if arg is None]) != 1:
# either be explicit, or let us deduce
raise ValueError("Provide exactly one of (event_state, tup)")
tl = self._tl_for_period(period_name)
if event_state is None:
event_state = self._get_event_state(tup, tl, how_many_periods, gte_threshold)
self.event_listeners[tl][(how_many_periods, gte_threshold)] = \
(when_becomes_true, when_becomes_false, event_state)
def _tl_for_period(self, period_name):
return {
"total": 0,
"year": 1,
"month": 2,
"day": 3,
"hour": 4,
"minute": 5,
}[period_name]
def _get_event_state(self, tup, tl, how_many_periods, gte_threshold):
min_tup = apply_n(_prev_tup, how_many_periods - 1, tup)
d = self.counts[tl]
total = sum([v for k, v in d.items() if k >= min_tup])
return total >= gte_threshold

View File

@@ -11,6 +11,14 @@ def apply_n(f, n, v):
return v
class callback(object):
def __init__(self):
self.calls = 0
def __call__(self):
self.calls += 1
class PeriodCounterTestCase(TestCase):
def test_prev_tup(self):
@@ -42,3 +50,54 @@ class PeriodCounterTestCase(TestCase):
datetime_utc = datetime.now(timezone.utc) # basically I just want to write this down somewhere
pc = PeriodCounter()
pc.inc(datetime_utc)
def test_event_listeners_for_total(self):
timepoint = datetime(2020, 1, 1, 10, 15, tzinfo=timezone.utc)
pc = PeriodCounter()
wbt = callback()
wbf = callback()
pc.add_event_listener("total", 1, 2, wbt, wbf, event_state=False)
# first inc: should not yet trigger
pc.inc(timepoint)
self.assertEquals(0, wbt.calls)
# second inc: should trigger (threshold of 2)
pc.inc(timepoint)
self.assertEquals(1, wbt.calls)
# third inc: should not trigger again
pc.inc(timepoint)
self.assertEquals(1, wbt.calls)
def test_event_listeners_for_year(self):
tp_2020 = datetime(2020, 1, 1, 10, 15, tzinfo=timezone.utc)
tp_2021 = datetime(2021, 1, 1, 10, 15, tzinfo=timezone.utc)
tp_2022 = datetime(2022, 1, 1, 10, 15, tzinfo=timezone.utc)
pc = PeriodCounter()
wbt = callback()
wbf = callback()
pc.add_event_listener("year", 2, 3, wbt, wbf, event_state=False)
pc.inc(tp_2020)
self.assertEquals(0, wbt.calls)
pc.inc(tp_2020)
self.assertEquals(0, wbt.calls)
# 3rd in total: become True
pc.inc(tp_2021)
self.assertEquals(1, wbt.calls)
# into a new year, total == 2: become false
self.assertEquals(0, wbf.calls)
pc.inc(tp_2022)
self.assertEquals(1, wbf.calls)
self.assertEquals(1, wbt.calls) # unchanged
# 3rd in (new) total: become True again
pc.inc(tp_2022)
self.assertEquals(2, wbt.calls)
self.assertEquals(1, wbf.calls) # unchanged