diff --git a/rocketry/core/time/anchor.py b/rocketry/core/time/anchor.py index cda10dd8..10897d57 100644 --- a/rocketry/core/time/anchor.py +++ b/rocketry/core/time/anchor.py @@ -54,7 +54,7 @@ class AnchoredInterval(TimeInterval): _unit_names: ClassVar[List] = None _unit_mapping: ClassVar[Dict[str, int]] = {} - def __init__(self, start=None, end=None, time_point=None, right_closed=False): + def __init__(self, start=None, end=None, time_point=None, starting=None, right_closed=False): if start is None and end is None: if time_point: @@ -63,7 +63,7 @@ def __init__(self, start=None, end=None, time_point=None, right_closed=False): object.__setattr__(self, "_end", self._scope_max) else: self.set_start(start) - self.set_end(end, time_point=time_point, right_closed=right_closed) + self.set_end(end, time_point=time_point, right_closed=right_closed, starting=starting) def anchor(self, value, **kwargs): "Turn value to nanoseconds relative to scope of the class" @@ -81,7 +81,7 @@ def anchor(self, value, **kwargs): def anchor_int(self, i, side=None, time_point=None, **kwargs): if side == "end": - return (i + 1) * self._unit_resolution - 1 + return (i + 1) * self._unit_resolution return i * self._unit_resolution def anchor_dict(self, d, **kwargs): @@ -120,14 +120,16 @@ def set_start(self, val): ms = 0 else: ms = self.anchor(val, side="start") - + self._validate(ms, orig=val) object.__setattr__(self, "_start", ms) object.__setattr__(self, "_start_orig", val) - def set_end(self, val, right_closed=False, time_point=False): + def set_end(self, val, right_closed=False, time_point=False, starting=False): if time_point and val is None: # Interval is "at" type, ie. on monday, at 10:00 (10:00 - 10:59:59) ms = self.to_timepoint(self._start) + elif starting and val is None: + ms = self._start elif val is None: ms = self._scope_max else: @@ -138,7 +140,7 @@ def set_end(self, val, right_closed=False, time_point=False): # given the end argument, ie. if "09:00 to 10:00" excludes 10:00 # we can include it by adding one nanosecond to 10:00 ms += 1 - + self._validate(ms, orig=val) object.__setattr__(self, "_end", ms) object.__setattr__(self, "_end_orig", val) @@ -149,6 +151,10 @@ def to_timepoint(self, ms:int): # but can be overridden for non linear such as year return ms + self._unit_resolution + def _validate(self, n:int, orig): + if n < 0 or n > self._scope_max: + raise ValueError(f"Out of bound: {repr(orig)}") + @property def start(self): delta = to_timedelta(self._start, unit="microsecond") @@ -417,4 +423,4 @@ def at(cls, value): @classmethod def starting(cls, value): - return cls(value, value) \ No newline at end of file + return cls(value, starting=True) \ No newline at end of file diff --git a/rocketry/test/time/interval/test_construct.py b/rocketry/test/time/interval/test_construct.py index f7580fd4..43f491aa 100644 --- a/rocketry/test/time/interval/test_construct.py +++ b/rocketry/test/time/interval/test_construct.py @@ -1,6 +1,7 @@ import pytest from rocketry.time.interval import ( + TimeOfMinute, TimeOfDay, TimeOfHour, TimeOfMonth, @@ -8,6 +9,7 @@ TimeOfYear ) +MS_IN_MILLISECOND = 1000 MS_IN_SECOND = int(1e+6) MS_IN_MINUTE = int(1e+6 * 60) MS_IN_HOUR = int(1e+6 * 60 * 60) @@ -29,6 +31,8 @@ def pytest_generate_tests(metafunc): params = cls.scen_open_right elif method_name == "test_time_point": params = cls.scen_time_point + elif method_name == "test_starting": + params = cls.scen_starting elif method_name == "test_value_error": params = cls.scen_value_error else: @@ -40,7 +44,7 @@ def pytest_generate_tests(metafunc): argvalues = [] argvalues = [] for scen in params: - idlist.append(scen.pop("id", f'{scen.get("start")}, {scen.get("end")}')) + idlist.append(scen.pop("id", f'{repr(scen.get("start"))}, {repr(scen.get("end"))}')) argvalues.append(tuple(scen[name] for name in argnames)) metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class") @@ -81,10 +85,98 @@ def test_time_point(self, start, expected_start, expected_end, **kwargs): assert expected_start == time._start assert expected_end == time._end + def test_starting(self, start, expected_start, **kwargs): + time = self.cls(start, starting=True) + assert time.is_full() + assert expected_start == time._start + assert expected_start == time._end + + time = self.cls.starting(start) + assert time.is_full() + assert expected_start == time._start + assert expected_start == time._end + def test_value_error(self, start, end): with pytest.raises(ValueError): time = self.cls(start, end) +class TestTimeOfMinute(ConstructTester): + + cls = TimeOfMinute + + max_ms = MS_IN_MINUTE + + scen_closed = [ + { + "start": "15", + "end": "45", + "expected_start": 15 * MS_IN_SECOND, + "expected_end": 45 * MS_IN_SECOND, + }, + { + "start": "15.000", + "end": "45.000", + "expected_start": 15 * MS_IN_SECOND, + "expected_end": 45 * MS_IN_SECOND, + }, + { + "start": "15.5", + "end": "45.5", + "expected_start": 15 * MS_IN_SECOND + MS_IN_MILLISECOND * 500, + "expected_end": 45 * MS_IN_SECOND + MS_IN_MILLISECOND * 500, + }, + { + "start": "15.005", + "end": "45.005", + "expected_start": 15 * MS_IN_SECOND + MS_IN_MILLISECOND * 5, + "expected_end": 45 * MS_IN_SECOND + MS_IN_MILLISECOND * 5, + }, + { + "start": "15.000005", + "end": "45.000005", + "expected_start": 15 * MS_IN_SECOND + 5, + "expected_end": 45 * MS_IN_SECOND + 5, + }, + { + "start": 15, + "end": 45, + "expected_start": 15 * MS_IN_SECOND, + "expected_end": 46 * MS_IN_SECOND, + }, + ] + + scen_open_left = [ + { + "end": "45.00", + "expected_end": 45 * MS_IN_SECOND + } + ] + scen_open_right = [ + { + "start": "45", + "expected_start": 45 * MS_IN_SECOND + } + ] + scen_time_point = [ + { + "start": "12:00", + "expected_start": 12 * MS_IN_SECOND, + "expected_end": 13 * MS_IN_SECOND, + } + ] + scen_starting = [ + { + "start": "12:00", + "expected_start": 12 * MS_IN_SECOND, + } + ] + scen_value_error = [ + { + "start": 60, + "end": None + } + ] + class TestTimeOfHour(ConstructTester): cls = TimeOfHour @@ -102,7 +194,7 @@ class TestTimeOfHour(ConstructTester): "start": 15, "end": 45, "expected_start": 15 * MS_IN_MINUTE, - "expected_end": 46 * MS_IN_MINUTE - 1, + "expected_end": 46 * MS_IN_MINUTE, }, ] @@ -125,12 +217,22 @@ class TestTimeOfHour(ConstructTester): "expected_end": 13 * MS_IN_MINUTE, } ] + scen_starting = [ + { + "start": "12:00", + "expected_start": 12 * MS_IN_MINUTE, + } + ] scen_value_error = [ { "start": 60, "end": None - } + }, + { + "start": None, + "end": "60:01" + }, ] class TestTimeOfDay(ConstructTester): @@ -150,7 +252,7 @@ class TestTimeOfDay(ConstructTester): "start": 10, "end": 12, "expected_start": 10 * MS_IN_HOUR, - "expected_end": 13 * MS_IN_HOUR - 1, + "expected_end": 13 * MS_IN_HOUR, }, ] @@ -171,13 +273,36 @@ class TestTimeOfDay(ConstructTester): "start": "12:00", "expected_start": 12 * MS_IN_HOUR, "expected_end": 13 * MS_IN_HOUR, + }, + { + "start": "23:00", + "expected_start": 23 * MS_IN_HOUR, + "expected_end": 24 * MS_IN_HOUR, + }, + ] + scen_starting = [ + { + "start": "12:00", + "expected_start": 12 * MS_IN_HOUR, } ] scen_value_error = [ + { + "start": -1, + "end": None, + }, + { + "start": "asd", + "end": None, + }, { "start": 24, "end": None, - } + }, + { + "start": None, + "end": "24:01", + }, ] @@ -193,28 +318,28 @@ class TestTimeOfWeek(ConstructTester): "start": "Tue", "end": "Wed", "expected_start": 1 * MS_IN_DAY, - "expected_end": 3 * MS_IN_DAY - 1, + "expected_end": 3 * MS_IN_DAY, }, { # Spans from Tue 00:00:00 to Wed 23:59:59 999 "start": "Tuesday", "end": "Wednesday", "expected_start": 1 * MS_IN_DAY, - "expected_end": 3 * MS_IN_DAY - 1, + "expected_end": 3 * MS_IN_DAY, }, { # Spans from Tue 00:00:00 to Wed 23:59:59 999 "start": 2, "end": 3, "expected_start": 1 * MS_IN_DAY, - "expected_end": 3 * MS_IN_DAY - 1, + "expected_end": 3 * MS_IN_DAY, }, ] scen_open_left = [ { "end": "Tue", - "expected_end": 2 * MS_IN_DAY - 1 # Tuesday 23:59:59 ... + "expected_end": 2 * MS_IN_DAY # Tuesday 23:59:59 ... } ] scen_open_right = [ @@ -230,11 +355,21 @@ class TestTimeOfWeek(ConstructTester): "expected_end": 2 * MS_IN_DAY, } ] + scen_starting = [ + { + "start": "Tue", + "expected_start": 1 * MS_IN_DAY, + } + ] scen_value_error = [ { "start": 0, "end": None - } + }, + { + "start": "Asd", + "end": None + }, ] @@ -249,26 +384,26 @@ class TestTimeOfMonth(ConstructTester): "start": "2.", "end": "3.", "expected_start": 1 * MS_IN_DAY, - "expected_end": 3 * MS_IN_DAY - 1, + "expected_end": 3 * MS_IN_DAY, }, { "start": "2nd", "end": "4th", "expected_start": 1 * MS_IN_DAY, - "expected_end": 4 * MS_IN_DAY - 1, + "expected_end": 4 * MS_IN_DAY, }, { "start": 2, "end": 4, "expected_start": 1 * MS_IN_DAY, - "expected_end": 4 * MS_IN_DAY - 1, + "expected_end": 4 * MS_IN_DAY, }, ] scen_open_left = [ { "end": "3.", - "expected_end": 3 * MS_IN_DAY - 1 + "expected_end": 3 * MS_IN_DAY } ] scen_open_right = [ @@ -284,6 +419,12 @@ class TestTimeOfMonth(ConstructTester): "expected_end": 2 * MS_IN_DAY, } ] + scen_starting = [ + { + "start": "2.", + "expected_start": 1 * MS_IN_DAY, + } + ] scen_value_error = [ { "start": 0, @@ -292,7 +433,11 @@ class TestTimeOfMonth(ConstructTester): { "start": None, "end": 32, - } + }, + { + "start": "33.", + "end": None + }, ] class TestTimeOfYear(ConstructTester): @@ -359,6 +504,12 @@ class TestTimeOfYear(ConstructTester): "expected_end": 366 * MS_IN_DAY - 1, }, ] + scen_starting = [ + { + "start": "Feb", + "expected_start": 31 * MS_IN_DAY, + } + ] scen_value_error = [ { "start": 0, diff --git a/rocketry/test/time/interval/test_contains.py b/rocketry/test/time/interval/test_contains.py new file mode 100644 index 00000000..b0a2f385 --- /dev/null +++ b/rocketry/test/time/interval/test_contains.py @@ -0,0 +1,366 @@ + +import pytest +from datetime import datetime + +from rocketry.time.interval import ( + TimeOfMinute, + TimeOfHour, + TimeOfDay, + TimeOfWeek, + TimeOfMonth, +) + +# TimeOfMinute +# ------------ + +@pytest.mark.parametrize( + "dt,time", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 11, 0, 15), + TimeOfMinute("15.00", "45.00"), + id="Left of interval"), + pytest.param( + datetime(2020, 1, 1, 11, 0, 44, 999_999), + TimeOfMinute("15.00", "45.00"), + id="Right of interval"), + ], +) +def test_in_time_of_minute(time, dt): + assert dt in time + + +@pytest.mark.parametrize( + "dt,time", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 11, 0, 14, 999_999), + TimeOfMinute("15.00", "45.00"), + id="Left of interval"), + pytest.param( + datetime(2020, 1, 1, 11, 00, 45, 0), + TimeOfMinute("15.00", "45.00"), + id="Right of interval"), + ], +) +def test_not_in_time_of_minute(time, dt): + assert dt not in time + +# TimeOfHour +# ---------- + +@pytest.mark.parametrize( + "dt,time", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 11, 15), + TimeOfHour("15:00", "45:00"), + id="Left of interval"), + pytest.param( + datetime(2020, 1, 1, 11, 44, 59, 999_999), + TimeOfHour("15:00", "45:00"), + id="Right of interval"), + ], +) +def test_in_time_of_hour(time, dt): + assert dt in time + +@pytest.mark.parametrize( + "dt,time", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 11, 14, 59, 999_999), + TimeOfHour("15:00", "45:00"), + id="Left of interval"), + pytest.param( + datetime(2020, 1, 1, 11, 45), + TimeOfHour("15:00", "45:00"), + id="Right of interval"), + ], +) +def test_not_in_time_of_hour(time, dt): + assert dt not in time + +# TimeOfDay +# --------- + +@pytest.mark.parametrize( + "dt,start,end", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 11, 00), + "10:00", "12:00", + id="Middle of interval"), + + # Left is closed + pytest.param( + datetime(2020, 1, 1, 10, 00), + "10:00", "12:00", + id="Left of interval"), + pytest.param( + datetime(2020, 1, 1, 22, 00), + "22:00", "02:00", + id="Left of overnight interval"), + + # Overnight + pytest.param( + datetime(2020, 1, 1, 23, 59, 59, 999999), + "22:00", "02:00", + id="Middle left of overnight interval"), + pytest.param( + datetime(2020, 1, 1, 00, 00), + "22:00", "02:00", + id="Middle right of overnight interval"), + + # Full Cycle + pytest.param( + datetime(2020, 1, 1, 10, 00), + None, None, + id="Full interval"), + pytest.param( + datetime(2020, 1, 1, 10, 00), + "10:00", "10:00", + id="Joint of full interval"), + pytest.param( + datetime(2020, 1, 1, 12, 00), + "10:00", "10:00", + id="Right of full interval"), + pytest.param( + datetime(2020, 1, 1, 8, 00), + "10:00", "10:00", + id="Left of full interval"), + ], +) +def test_in_time_of_day(start, end, dt): + time = TimeOfDay(start, end) + assert dt in time + + +@pytest.mark.parametrize( + "dt,start,end", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 9, 59, 59, 999999), + "10:00", "12:00", + id="Left from interval"), + pytest.param( + datetime(2020, 1, 1, 12, 00, 00, 1), + "10:00", "12:00", + id="Right from interval"), + + # Right is opened + pytest.param( + datetime(2020, 1, 1, 12, 00), + "10:00", "12:00", + id="Right of interval"), + pytest.param( + datetime(2020, 1, 1, 2, 00), + "22:00", "02:00", + id="Right of overnight interval"), + + # Overnight + pytest.param( + datetime(2020, 1, 1, 21, 59, 59, 999999), + "22:00", "02:00", + id="Left from overnight interval"), + pytest.param( + datetime(2020, 1, 1, 2, 00, 00, 1), + "22:00", "02:00", + id="Right from overnight interval"), + ], +) +def test_not_in_time_of_day(start, end, dt): + time = TimeOfDay(start, end) + assert dt not in time + + +# TimeOfWeek +# ---------- + +# TimeOfWeek +# Year 2024 was chosen as it starts on monday +@pytest.mark.parametrize( + "dt,start,end", + [ + # Regular + pytest.param( + datetime(2024, 1, 2, 10, 00), + "Tue 10:00", "Sat 12:00", + id="Left of interval (with time)"), + pytest.param( + datetime(2024, 1, 4, 11, 00), + "Tue 10:00", "Sat 12:00", + id="Middle of interval (with time)"), + pytest.param( + datetime(2024, 1, 6, 11, 59, 59, 999999), + "Tue 10:00", "Sat 12:00", + id="Right of interval (with time)"), + pytest.param( + datetime(2024, 1, 6, 23, 59, 59, 999999), + "Tue", "Sat", + id="Right of interval"), + + # Over weekend + pytest.param( + datetime(2024, 1, 6, 10, 00), + "Sat 10:00", "Tue 12:00", + id="Left of over weekend interval (with time)"), + pytest.param( + datetime(2024, 1, 7, 23, 59, 59, 999999), + "Sat 10:00", "Tue 12:00", + id="Middle left of over weekend interval (with time)"), + pytest.param( + datetime(2024, 1, 8, 00, 00), + "Sat 10:00", "Tue 12:00", + id="Middle right of over weekend interval (with time)"), + + # Full Cycle + pytest.param( + datetime(2024, 1, 1, 00, 00), + None, None, + id="Full interval"), + pytest.param( + datetime(2024, 1, 2, 00, 00), + "Tue", "Tue", + id="Joint of full interval"), + pytest.param( + datetime(2024, 1, 1, 00, 00), + "Tue 00:00", "Tue 00:00", + id="Right of full interval"), + pytest.param( + datetime(2024, 1, 6, 00, 00), + "Tue 00:00", "Tue 00:00", + id="Left of full interval"), + ], +) +def test_in_time_of_week(start, end, dt): + time = TimeOfWeek(start, end) + assert dt in time + + +@pytest.mark.parametrize( + "dt,start,end", + [ + # Regular + pytest.param( + datetime(2024, 1, 1, 0, 0), + "Tue 10:00", "Sat 12:00", + id="Left from interval"), + pytest.param( + datetime(2024, 1, 7, 14, 00, 00, 1), + "Tue 10:00", "Sat 12:00", + id="Right from interval"), + + # Right is opened + pytest.param( + datetime(2024, 1, 6, 12, 00), + "Tue 10:00", "Sat 12:00", + id="Right of interval"), + pytest.param( + datetime(2024, 1, 9, 12, 00), + "Sat 10:00", "Tue 12:00", + id="Right of over weekend interval"), + + # Over weekend + pytest.param( + datetime(2024, 1, 6, 9, 59, 59, 999999), + "Sat 10:00", "Tue 12:00", + id="Left from over weekend interval"), + pytest.param( + datetime(2024, 1, 9, 12, 1, 00), + "Sat 10:00", "Tue 12:00", + id="Right from over weekend interval"), + ], +) +def test_not_in_time_of_week(start, end, dt): + time = TimeOfWeek(start, end) + assert dt not in time + + +# TimeOfMonth +# --------- + +@pytest.mark.parametrize( + "dt,time", + [ + # Regular + pytest.param( + datetime(2020, 1, 2, 11, 00), + TimeOfMonth("2.", "3."), + id="Middle of interval"), + + # Left is closed + pytest.param( + datetime(2020, 1, 2, 0, 00), + TimeOfMonth("2.", "3."), + id="Left of interval"), + pytest.param( + datetime(2020, 1, 3, 23, 59, 59, 999999), + TimeOfMonth("2.", "3."), + id="Left of overnight interval"), + + # Over month + pytest.param( + datetime(2020, 1, 23), + TimeOfMonth("22.", "2."), + id="Middle left of over month"), + pytest.param( + datetime(2020, 1, 1, 00, 00), + TimeOfMonth("22.", "2."), + id="Middle right of over month"), + + # Full Cycle + pytest.param( + datetime(2020, 1, 31), + TimeOfMonth(None, None), + id="Full interval"), + pytest.param( + datetime(2020, 1, 5), + TimeOfMonth.starting("5."), + id="Joint of full interval"), + pytest.param( + datetime(2020, 1, 1), + TimeOfMonth.starting("5."), + id="Right of full interval"), + pytest.param( + datetime(2020, 1, 6), + TimeOfMonth.starting("5."), + id="Left of full interval"), + ], +) +def test_in_time_of_month(time, dt): + assert dt in time + + +@pytest.mark.parametrize( + "dt,time", + [ + # Regular + pytest.param( + datetime(2020, 1, 1, 23, 59, 59, 999999), + TimeOfMonth("2.", "3."), + id="Left from interval"), + pytest.param( + datetime(2020, 1, 4), + TimeOfMonth("2.", "3."), + id="Right from interval"), + + # Right is opened + pytest.param( + datetime(2020, 1, 21), + TimeOfMonth("22.", "2."), + id="Right of over month"), + pytest.param( + datetime(2020, 1, 3), + TimeOfMonth("22.", "2."), + id="Left of over month"), + ], +) +def test_not_in_time_of_month(time, dt): + assert dt not in time \ No newline at end of file diff --git a/rocketry/test/time/interval/timeofday/test_roll.py b/rocketry/test/time/interval/test_roll.py similarity index 96% rename from rocketry/test/time/interval/timeofday/test_roll.py rename to rocketry/test/time/interval/test_roll.py index 1c0ec08a..bf26048d 100644 --- a/rocketry/test/time/interval/timeofday/test_roll.py +++ b/rocketry/test/time/interval/test_roll.py @@ -8,6 +8,9 @@ from_iso = datetime.fromisoformat +# TimeOfDay +# --------- + @pytest.mark.parametrize( "dt,start,end,roll_start,roll_end", [ @@ -68,7 +71,7 @@ id="Left of full interval"), ], ) -def test_rollforward(start, end, dt, roll_start, roll_end): +def test_rollforward_time_of_day(start, end, dt, roll_start, roll_end): time = TimeOfDay(start, end) interval = time.rollforward(dt) @@ -136,7 +139,7 @@ def test_rollforward(start, end, dt, roll_start, roll_end): id="Left of full interval"), ], ) -def test_rollback(start, end, dt, roll_start, roll_end): +def test_rollback_time_of_day(start, end, dt, roll_start, roll_end): time = TimeOfDay(start, end) interval = time.rollback(dt) diff --git a/rocketry/test/time/interval/timeofday/__init__.py b/rocketry/test/time/interval/timeofday/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/rocketry/test/time/interval/timeofday/test_contains.py b/rocketry/test/time/interval/timeofday/test_contains.py deleted file mode 100644 index cf2c47ef..00000000 --- a/rocketry/test/time/interval/timeofday/test_contains.py +++ /dev/null @@ -1,97 +0,0 @@ - -from datetime import datetime -import pytest -from rocketry.time.interval import ( - TimeOfDay -) - -@pytest.mark.parametrize( - "dt,start,end", - [ - # Regular - pytest.param( - datetime(2020, 1, 1, 11, 00), - "10:00", "12:00", - id="Middle of interval"), - - # Left is closed - pytest.param( - datetime(2020, 1, 1, 10, 00), - "10:00", "12:00", - id="Left of interval"), - pytest.param( - datetime(2020, 1, 1, 22, 00), - "22:00", "02:00", - id="Left of overnight interval"), - - # Overnight - pytest.param( - datetime(2020, 1, 1, 23, 59, 59, 999999), - "22:00", "02:00", - id="Middle left of overnight interval"), - pytest.param( - datetime(2020, 1, 1, 00, 00), - "22:00", "02:00", - id="Middle right of overnight interval"), - - # Full Cycle - pytest.param( - datetime(2020, 1, 1, 10, 00), - None, None, - id="Full interval"), - pytest.param( - datetime(2020, 1, 1, 10, 00), - "10:00", "10:00", - id="Joint of full interval"), - pytest.param( - datetime(2020, 1, 1, 12, 00), - "10:00", "10:00", - id="Right of full interval"), - pytest.param( - datetime(2020, 1, 1, 8, 00), - "10:00", "10:00", - id="Left of full interval"), - ], -) -def test_in(start, end, dt): - time = TimeOfDay(start, end) - assert dt in time - - -@pytest.mark.parametrize( - "dt,start,end", - [ - # Regular - pytest.param( - datetime(2020, 1, 1, 9, 59, 59, 999999), - "10:00", "12:00", - id="Left from interval"), - pytest.param( - datetime(2020, 1, 1, 12, 00, 00, 1), - "10:00", "12:00", - id="Right from interval"), - - # Right is opened - pytest.param( - datetime(2020, 1, 1, 12, 00), - "10:00", "12:00", - id="Right of interval"), - pytest.param( - datetime(2020, 1, 1, 2, 00), - "22:00", "02:00", - id="Right of overnight interval"), - - # Overnight - pytest.param( - datetime(2020, 1, 1, 21, 59, 59, 999999), - "22:00", "02:00", - id="Left from overnight interval"), - pytest.param( - datetime(2020, 1, 1, 2, 00, 00, 1), - "22:00", "02:00", - id="Right from overnight interval"), - ], -) -def test_not_in(start, end, dt): - time = TimeOfDay(start, end) - assert dt not in time diff --git a/rocketry/test/time/interval/timeofweek/test_contains.py b/rocketry/test/time/interval/timeofweek/test_contains.py deleted file mode 100644 index 31ea6c87..00000000 --- a/rocketry/test/time/interval/timeofweek/test_contains.py +++ /dev/null @@ -1,98 +0,0 @@ - -import pytest -from datetime import datetime - -from rocketry.time.interval import ( - TimeOfWeek -) - -# TimeOfWeek -# Year 2024 was chosen as it starts on monday -@pytest.mark.parametrize( - "dt,start,end", - [ - # Regular - pytest.param( - datetime(2024, 1, 2, 10, 00), - "Tue 10:00", "Sat 12:00", - id="Left of interval"), - pytest.param( - datetime(2024, 1, 4, 11, 00), - "Tue 10:00", "Sat 12:00", - id="Middle of interval"), - - # Over weekend - pytest.param( - datetime(2024, 1, 6, 10, 00), - "Sat 10:00", "Tue 12:00", - id="Left of over weekend interval"), - pytest.param( - datetime(2024, 1, 7, 23, 59, 59, 999999), - "Sat 10:00", "Tue 12:00", - id="Middle left of over weekend interval"), - pytest.param( - datetime(2024, 1, 8, 00, 00), - "Sat 10:00", "Tue 12:00", - id="Middle right of over weekend interval"), - - # Full Cycle - pytest.param( - datetime(2024, 1, 1, 00, 00), - None, None, - id="Full interval"), - pytest.param( - datetime(2024, 1, 2, 00, 00), - "Tue", "Tue", - id="Joint of full interval"), - pytest.param( - datetime(2024, 1, 1, 00, 00), - "Tue 00:00", "Tue 00:00", - id="Right of full interval"), - pytest.param( - datetime(2024, 1, 6, 00, 00), - "Tue 00:00", "Tue 00:00", - id="Left of full interval"), - ], -) -def test_in(start, end, dt): - time = TimeOfWeek(start, end) - assert dt in time - - -@pytest.mark.parametrize( - "dt,start,end", - [ - # Regular - pytest.param( - datetime(2024, 1, 1, 0, 0), - "Tue 10:00", "Sat 12:00", - id="Left from interval"), - pytest.param( - datetime(2024, 1, 7, 14, 00, 00, 1), - "Tue 10:00", "Sat 12:00", - id="Right from interval"), - - # Right is opened - pytest.param( - datetime(2024, 1, 6, 12, 00), - "Tue 10:00", "Sat 12:00", - id="Right of interval"), - pytest.param( - datetime(2024, 1, 9, 12, 00), - "Sat 10:00", "Tue 12:00", - id="Right of over weekend interval"), - - # Over weekend - pytest.param( - datetime(2024, 1, 6, 9, 59, 59, 999999), - "Sat 10:00", "Tue 12:00", - id="Left from over weekend interval"), - pytest.param( - datetime(2024, 1, 9, 12, 1, 00), - "Sat 10:00", "Tue 12:00", - id="Right from over weekend interval"), - ], -) -def test_not_in(start, end, dt): - time = TimeOfWeek(start, end) - assert dt not in time \ No newline at end of file diff --git a/rocketry/test/time/interval/timeofweek/test_core.py b/rocketry/test/time/interval/timeofweek/test_core.py index 4cff3661..96787e98 100644 --- a/rocketry/test/time/interval/timeofweek/test_core.py +++ b/rocketry/test/time/interval/timeofweek/test_core.py @@ -48,7 +48,7 @@ def test_anchor_equal(dt, string, ms): # From Tue 00:00 to Wed 23:59:59.000 () "Tue", "Wed", # - MS_IN_DAY, MS_IN_DAY * 3 - 1, + MS_IN_DAY, MS_IN_DAY * 3, id="Strings: minimal"), ], ) diff --git a/rocketry/time/interval.py b/rocketry/time/interval.py index cee6fdc8..a788620d 100644 --- a/rocketry/time/interval.py +++ b/rocketry/time/interval.py @@ -18,10 +18,6 @@ class TimeOfMinute(AnchoredInterval): min: 0 seconds, 0 microsecond max: 59 seconds, 999999 microsecond - - Example: - # From 5th second of a minute to 30th second of a minute - TimeOfHour("5:00", "30:00") """ _scope: ClassVar[str] = "minute" @@ -30,19 +26,19 @@ class TimeOfMinute(AnchoredInterval): _unit_resolution: ClassVar[int] = to_microseconds(second=1) _unit_names: ClassVar[List[str]] = [f"{i:02d}" for i in range(60)] # 00, 01 etc. till 59 + def anchor_int(self, i, **kwargs): + if not 0 <= i <= 59: + raise ValueError(f"Invalid value: {i}. Allowed: 0-59") + return super().anchor_int(i, **kwargs) + def anchor_str(self, s, **kwargs): # ie. 30.123 res = re.search(r"(?P[0-9][0-9])([.](?P[0-9]{0,6}))?", s, flags=re.IGNORECASE) if res: + res = res.groupdict() if res["microsecond"] is not None: res["microsecond"] = res["microsecond"].ljust(6, "0") - return to_microseconds(**{key: int(val) for key, val in res.groupdict().items() if val is not None}) - - res = re.search(r"(?P[1-4] ?(quarter|q))", s, flags=re.IGNORECASE) - if res: - # ie. "1 quarter" - n_quarters = res["n"] - return (self._scope_max + 1) / 4 * n_quarters - 1 + return to_microseconds(**{key: int(val) for key, val in res.items() if val is not None}) @dataclass(frozen=True, init=False) @@ -74,12 +70,12 @@ def anchor_str(self, s, **kwargs): res["microsecond"] = res["microsecond"].ljust(6, "0") return to_microseconds(**{key: int(val) for key, val in res.groupdict().items() if val is not None}) - res = re.search(r"(?P[1-4]) ?(quarter|q)", s, flags=re.IGNORECASE) + res = re.search(r"(?P[0-4]) ?(quarter|q)", s, flags=re.IGNORECASE) if res: # ie. "1 quarter" n_quarters = int(res["n"]) return (self._scope_max + 1) / 4 * n_quarters - 1 - + raise ValueError(f"Invalid value: {repr(s)}") @dataclass(frozen=True, init=False) class TimeOfDay(AnchoredInterval): @@ -164,11 +160,14 @@ def anchor_str(self, s, side=None, **kwargs): comps = res.groupdict() dayofweek = comps.pop("dayofweek") time = comps.pop("time") - nth_day = self._unit_mapping[dayofweek.lower()] + try: + nth_day = self._unit_mapping[dayofweek.lower()] + except KeyError: + raise ValueError(f"Invalid day of week: {dayofweek}") # TODO: TimeOfDay.anchor_str as function if not time: - microseconds = to_microseconds(day=1) - 1 if side == "end" else 0 + microseconds = to_microseconds(day=1) if side == "end" else 0 else: microseconds = TimeOfDay().anchor_str(time) @@ -234,7 +233,7 @@ def anchor_str(self, s, side=None, **kwargs): # If one says 'thing X was organized between # 15th and 17th of July', the sentence # includes 17th till midnight. - microseconds = to_microseconds(day=1) - 1 + microseconds = to_microseconds(day=1) elif time: microseconds = TimeOfDay().anchor_str(time) else: