Skip to content

Commit

Permalink
add hours_in_day and start_of_day helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
ariebovenberg committed Jan 14, 2025
1 parent 5c1dc7b commit 503cf78
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 93 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
🚀 Changelog
============

0.6.17 (2024-12-??)
0.6.17 (2025-01-15)
-------------------

- Added ``hours_in_day()`` and ``start_of_day()`` methods to ``ZonedDateTime``
to make it easier to work with edge cases around DST transitions.
- Fix cases in type stubs where positional-only arguments weren't marked as such

0.6.16 (2024-12-22)
-------------------
Expand Down
32 changes: 17 additions & 15 deletions pysrc/whenever/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class Time:
def second(self) -> int: ...
@property
def nanosecond(self) -> int: ...
def on(self, d: Date) -> LocalDateTime: ...
def on(self, d: Date, /) -> LocalDateTime: ...
def py_time(self) -> _time: ...
@classmethod
def from_py_time(cls, t: _time, /) -> Time: ...
Expand Down Expand Up @@ -659,6 +659,8 @@ class ZonedDateTime(_KnowsInstantAndLocal):
*,
disambiguate: Literal["compatible", "raise", "earlier", "later"] = ...,
) -> ZonedDateTime: ...
def hours_in_day(self) -> float: ...
def start_of_day(self) -> ZonedDateTime: ...
# FUTURE: disable date components in strict stubs version
def __add__(self, delta: Delta) -> ZonedDateTime: ...
@overload
Expand Down Expand Up @@ -828,12 +830,12 @@ class LocalDateTime(_KnowsLocal):
tz: str,
/,
*,
disambiguate: Literal["compatible", "raise", "earlier", "later"],
disambiguate: Literal["compatible", "raise", "earlier", "later"] = ...,
) -> ZonedDateTime: ...
def assume_system_tz(
self,
*,
disambiguate: Literal["compatible", "raise", "earlier", "later"],
disambiguate: Literal["compatible", "raise", "earlier", "later"] = ...,
) -> SystemDateTime: ...
@classmethod
def from_py_datetime(cls, d: _datetime, /) -> LocalDateTime: ...
Expand All @@ -854,8 +856,8 @@ class LocalDateTime(_KnowsLocal):
second: int = ...,
nanosecond: int = ...,
) -> LocalDateTime: ...
def replace_date(self, d: Date) -> LocalDateTime: ...
def replace_time(self, t: Time) -> LocalDateTime: ...
def replace_date(self, d: Date, /) -> LocalDateTime: ...
def replace_time(self, t: Time, /) -> LocalDateTime: ...
@overload
def add(
self,
Expand Down Expand Up @@ -947,16 +949,16 @@ FRIDAY = Weekday.FRIDAY
SATURDAY = Weekday.SATURDAY
SUNDAY = Weekday.SUNDAY

def years(i: int) -> DateDelta: ...
def months(i: int) -> DateDelta: ...
def weeks(i: int) -> DateDelta: ...
def days(i: int) -> DateDelta: ...
def hours(i: float) -> TimeDelta: ...
def minutes(i: float) -> TimeDelta: ...
def seconds(i: float) -> TimeDelta: ...
def milliseconds(i: float) -> TimeDelta: ...
def microseconds(i: float) -> TimeDelta: ...
def nanoseconds(i: int) -> TimeDelta: ...
def years(i: int, /) -> DateDelta: ...
def months(i: int, /) -> DateDelta: ...
def weeks(i: int, /) -> DateDelta: ...
def days(i: int, /) -> DateDelta: ...
def hours(i: float, /) -> TimeDelta: ...
def minutes(i: float, /) -> TimeDelta: ...
def seconds(i: float, /) -> TimeDelta: ...
def milliseconds(i: float, /) -> TimeDelta: ...
def microseconds(i: float, /) -> TimeDelta: ...
def nanoseconds(i: int, /) -> TimeDelta: ...

class _TimePatch:
def shift(self, *args: Any, **kwargs: Any) -> None: ...
Expand Down
4 changes: 2 additions & 2 deletions pysrc/whenever/_pywhenever.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,8 +612,8 @@ def parse_common_iso(cls, s: str, /) -> YearMonth:
Example
-------
>>> YearMonth.parse_common_iso("2021-01-02")
YearMonth(2021-01-02)
>>> YearMonth.parse_common_iso("2021-01")
YearMonth(2021-01)
"""
if not _match_yearmonth(s):
raise ValueError(f"Invalid format: {s!r}")
Expand Down
6 changes: 4 additions & 2 deletions tests/test_instant.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,8 +826,10 @@ def test_to_system_tz():
assert d.to_system_tz().exact_eq(
SystemDateTime(2022, 11, 6, 1, disambiguate="earlier")
)
assert Instant.from_utc(2022, 11, 6, 6).to_system_tz() == SystemDateTime(
2022, 11, 6, 1, disambiguate="later"
assert (
Instant.from_utc(2022, 11, 6, 6)
.to_system_tz()
.exact_eq(SystemDateTime(2022, 11, 6, 1, disambiguate="later"))
)

with pytest.raises((ValueError, OverflowError)):
Expand Down
79 changes: 53 additions & 26 deletions tests/test_local_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ def test_assume_fixed_offset():
class TestAssumeTz:
def test_typical(self):
d = LocalDateTime(2020, 8, 15, 23)
assert d.assume_tz(
"Asia/Tokyo", disambiguate="raise"
) == ZonedDateTime(2020, 8, 15, 23, tz="Asia/Tokyo")
assert d.assume_tz("Asia/Tokyo", disambiguate="raise").exact_eq(
ZonedDateTime(2020, 8, 15, 23, tz="Asia/Tokyo")
)
assert d.assume_tz("Asia/Tokyo").exact_eq(
ZonedDateTime(2020, 8, 15, 23, tz="Asia/Tokyo")
)

def test_ambiguous(self):
d = LocalDateTime(2023, 10, 29, 2, 15)
Expand All @@ -94,13 +97,27 @@ def test_ambiguous(self):

assert d.assume_tz(
"Europe/Amsterdam", disambiguate="earlier"
) == ZonedDateTime(
2023, 10, 29, 2, 15, tz="Europe/Amsterdam", disambiguate="earlier"
).exact_eq(
ZonedDateTime(
2023,
10,
29,
2,
15,
tz="Europe/Amsterdam",
disambiguate="earlier",
)
)
assert d.assume_tz(
"Europe/Amsterdam", disambiguate="later"
) == ZonedDateTime(
2023, 10, 29, 2, 15, tz="Europe/Amsterdam", disambiguate="later"
assert d.assume_tz("Europe/Amsterdam", disambiguate="later").exact_eq(
ZonedDateTime(
2023,
10,
29,
2,
15,
tz="Europe/Amsterdam",
disambiguate="later",
)
)

def test_nonexistent(self):
Expand All @@ -111,17 +128,27 @@ def test_nonexistent(self):

assert d.assume_tz(
"Europe/Amsterdam", disambiguate="earlier"
) == ZonedDateTime(
2023, 3, 26, 2, 15, tz="Europe/Amsterdam", disambiguate="earlier"
).exact_eq(
ZonedDateTime(
2023,
3,
26,
2,
15,
tz="Europe/Amsterdam",
disambiguate="earlier",
)
)


class TestAssumeSystemTz:
@system_tz_ams()
def test_typical(self):
assert LocalDateTime(2020, 8, 15, 23).assume_system_tz(
disambiguate="raise"
) == SystemDateTime(2020, 8, 15, 23)
assert (
LocalDateTime(2020, 8, 15, 23)
.assume_system_tz(disambiguate="raise")
.exact_eq(SystemDateTime(2020, 8, 15, 23))
)

@system_tz_ams()
def test_ambiguous(self):
Expand All @@ -130,14 +157,14 @@ def test_ambiguous(self):
with pytest.raises(RepeatedTime, match="02:15.*system"):
d.assume_system_tz(disambiguate="raise")

assert d.assume_system_tz(disambiguate="earlier") == SystemDateTime(
2023, 10, 29, 2, 15, disambiguate="earlier"
assert d.assume_system_tz(disambiguate="earlier").exact_eq(
SystemDateTime(2023, 10, 29, 2, 15, disambiguate="earlier")
)
assert d.assume_system_tz(disambiguate="compatible") == SystemDateTime(
2023, 10, 29, 2, 15, disambiguate="earlier"
assert d.assume_system_tz(disambiguate="compatible").exact_eq(
SystemDateTime(2023, 10, 29, 2, 15, disambiguate="earlier")
)
assert d.assume_system_tz(disambiguate="later") == SystemDateTime(
2023, 10, 29, 2, 15, disambiguate="later"
assert d.assume_system_tz(disambiguate="later").exact_eq(
SystemDateTime(2023, 10, 29, 2, 15, disambiguate="later")
)

@system_tz_ams()
Expand All @@ -147,14 +174,14 @@ def test_nonexistent(self):
with pytest.raises(SkippedTime, match="02:15.*system"):
d.assume_system_tz(disambiguate="raise")

assert d.assume_system_tz(disambiguate="earlier") == SystemDateTime(
2023, 3, 26, 2, 15, disambiguate="earlier"
assert d.assume_system_tz(disambiguate="earlier").exact_eq(
SystemDateTime(2023, 3, 26, 2, 15, disambiguate="earlier")
)
assert d.assume_system_tz(disambiguate="later") == SystemDateTime(
2023, 3, 26, 2, 15, disambiguate="later"
assert d.assume_system_tz(disambiguate="later").exact_eq(
SystemDateTime(2023, 3, 26, 2, 15, disambiguate="later")
)
assert d.assume_system_tz(disambiguate="compatible") == SystemDateTime(
2023, 3, 26, 2, 15, disambiguate="compatible"
assert d.assume_system_tz(disambiguate="compatible").exact_eq(
SystemDateTime(2023, 3, 26, 2, 15, disambiguate="compatible")
)


Expand Down
30 changes: 15 additions & 15 deletions tests/test_system_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,8 @@ def test_basic(self):
assert d.offset == hours(2)

def test_optionality(self):
assert (
SystemDateTime(2020, 8, 15, 12)
== SystemDateTime(2020, 8, 15, 12, 0)
== SystemDateTime(2020, 8, 15, 12, 0, 0)
== SystemDateTime(2020, 8, 15, 12, 0, 0, nanosecond=0)
== SystemDateTime(
assert SystemDateTime(2020, 8, 15, 12).exact_eq(
SystemDateTime(
2020, 8, 15, 12, 0, 0, nanosecond=0, disambiguate="raise"
)
)
Expand Down Expand Up @@ -136,14 +132,16 @@ class TestInstant:
@system_tz_ams()
def test_common_time(self):
d = SystemDateTime(2020, 8, 15, 11)
assert d.instant() == Instant.from_utc(2020, 8, 15, 9)
assert d.instant().exact_eq(Instant.from_utc(2020, 8, 15, 9))

@system_tz_ams()
def test_amibiguous_time(self):
d = SystemDateTime(2023, 10, 29, 2, 15, disambiguate="earlier")
assert d.instant() == Instant.from_utc(2023, 10, 29, 0, 15)
assert d.replace(disambiguate="later").instant() == Instant.from_utc(
2023, 10, 29, 1, 15
assert d.instant().exact_eq(Instant.from_utc(2023, 10, 29, 0, 15))
assert (
d.replace(disambiguate="later")
.instant()
.exact_eq(Instant.from_utc(2023, 10, 29, 1, 15))
)


Expand All @@ -167,12 +165,14 @@ def test_to_tz():
.to_tz("America/New_York")
.exact_eq(nyc.replace(hour=21, disambiguate="raise"))
)
assert nyc.to_system_tz() == ams
assert nyc.replace(
hour=21, disambiguate="raise"
).to_system_tz() == ams.replace(disambiguate="later")
assert nyc.to_system_tz().exact_eq(ams)
assert (
nyc.replace(hour=21, disambiguate="raise")
.to_system_tz()
.exact_eq(ams.replace(disambiguate="later"))
)
# disambiguation doesn't affect NYC time because there's no ambiguity
assert nyc.replace(disambiguate="later").to_system_tz() == ams
assert nyc.replace(disambiguate="later").to_system_tz().exact_eq(ams)

try:
d_min = Instant.MIN.to_system_tz()
Expand Down
Loading

0 comments on commit 503cf78

Please sign in to comment.