Skip to content

Commit

Permalink
Next small release (#199)
Browse files Browse the repository at this point in the history
Add `day_length()` and `start_of_day()` helpers
  • Loading branch information
ariebovenberg authored Jan 30, 2025
1 parent 9aeaf23 commit 8ffefb0
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 142 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
🚀 Changelog
============

0.6.17 (2025-01-30)
-------------------

- Added ``day_length()`` and ``start_of_day()`` methods to ``ZonedDateTime``
to make it easier to work with edge cases around DST transitions,
and prepare for implementing rounding methods in the future.
- Fix cases in type stubs where positional-only arguments weren't marked as such

0.6.16 (2024-12-22)
-------------------

Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ There’s no way to be sure...
✨ Until now! ✨

*Whenever* helps you write **correct** and **type checked** datetime code,
using **well-established concepts** from modern libraries in other languages.
using **well-established concepts** from [modern libraries](#acknowledgements) in other languages.
It's also **way faster** than other third-party libraries—and usually the standard library as well.
If performance isn't your top priority, a **pure Python** version is available as well.

Expand Down Expand Up @@ -214,10 +214,10 @@ For more details, see the licenses included in the distribution.

## Acknowledgements

This project is inspired by—and borrows concepts from—the following projects. Check them out!
This project is inspired by—and borrows most concepts from—the following projects. Check them out!

- [Noda Time](https://nodatime.org/) and [Joda Time](https://www.joda.org/joda-time/)
- [Temporal](https://tc39.es/proposal-temporal/docs/)
- [Noda Time](https://nodatime.org/) and [Joda Time](https://www.joda.org/joda-time/)

The benchmark comparison graph is based on the one from the [Ruff](https://github.com/astral-sh/ruff) project.
For timezone data, **Whenever** uses Python's own `zoneinfo` module.
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Concrete classes
:members:
tz,
is_ambiguous,
start_of_day,
day_length,
:member-order: bysource
:show-inheritance:

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ maintainers = [
{name = "Arie Bovenberg", email = "[email protected]"},
]
readme = "README.md"
version = "0.6.16"
version = "0.6.17"
description = "Modern datetime library for Python"
requires-python = ">=3.9"
classifiers = [
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
38 changes: 35 additions & 3 deletions pysrc/whenever/_pywhenever.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
# - It saves some overhead
from __future__ import annotations

__version__ = "0.6.16"
__version__ = "0.6.17"

import enum
import re
Expand Down 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 Expand Up @@ -4310,6 +4310,38 @@ def is_ambiguous(self) -> bool:
# ambiguous datetimes are never equal across timezones
return self._py_dt.astimezone(_UTC) != self._py_dt

def day_length(self) -> TimeDelta:
"""The duration between the start of the current day and the next.
This is usually 24 hours, but may be different due to timezone transitions.
Example
-------
>>> ZonedDateTime(2020, 8, 15, tz="Europe/London").day_length()
TimeDelta(24:00:00)
>>> ZonedDateTime(2023, 10, 29, tz="Europe/Amsterdam").day_length()
TimeDelta(25:00:00)
"""
midnight = _datetime.combine(
self._py_dt.date(), _time(), self._py_dt.tzinfo
)
next_midnight = midnight + _timedelta(days=1)
return TimeDelta.from_py_timedelta(
next_midnight.astimezone(_UTC) - midnight.astimezone(_UTC)
)

def start_of_day(self) -> ZonedDateTime:
"""The start of the current calendar day.
This is almost always at midnight the same day, but may be different
for timezones which transition at—and thus skip over—midnight.
"""
midnight = _datetime.combine(
self._py_dt.date(), _time(), self._py_dt.tzinfo
)
return ZonedDateTime._from_py_unchecked(
midnight.astimezone(_UTC).astimezone(self._py_dt.tzinfo), 0
)

def __repr__(self) -> str:
return f"ZonedDateTime({str(self).replace('T', ' ', 1)})"

Expand Down
60 changes: 31 additions & 29 deletions src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,7 @@ impl Date {
// Since the data already fits within an i32
// we don't need to do any extra hashing. It may be counterintuitive,
// but this is also what `int` does: `hash(6) == 6`.
mem::transmute::<_, i32>(self)
}

pub(crate) const fn increment(mut self) -> Self {
if self.day < days_in_month(self.year, self.month) {
self.day += 1
} else if self.month < 12 {
self.day = 1;
self.month += 1;
} else {
self.year += 1;
self.month = 1;
self.day = 1;
}
self
}

pub(crate) const fn decrement(mut self) -> Self {
if self.day > 1 {
self.day -= 1;
} else if self.month > 1 {
self.month -= 1;
self.day = days_in_month(self.year, self.month);
} else {
self.day = 31;
self.month = 12;
self.year -= 1;
}
self
mem::transmute(self)
}

pub(crate) const fn ord(self) -> u32 {
Expand Down Expand Up @@ -199,6 +171,36 @@ impl Date {
*s = &s[10..];
result
}

// Faster methods for small adjustments.
// OPTIMIZE: actually determine if these are worth it
pub(crate) const fn increment(mut self) -> Self {
if self.day < days_in_month(self.year, self.month) {
self.day += 1
} else if self.month < 12 {
self.day = 1;
self.month += 1;
} else {
self.year += 1;
self.month = 1;
self.day = 1;
}
self
}

pub(crate) const fn decrement(mut self) -> Self {
if self.day > 1 {
self.day -= 1;
} else if self.month > 1 {
self.month -= 1;
self.day = days_in_month(self.year, self.month);
} else {
self.day = 31;
self.month = 12;
self.year -= 1;
}
self
}
}

impl PyWrapped for Date {}
Expand Down
27 changes: 25 additions & 2 deletions src/docstrings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1603,8 +1603,8 @@ Inverse of :meth:`format_common_iso`
Example
-------
>>> YearMonth.parse_common_iso(\"2021-01-02\")
YearMonth(2021-01-02)
>>> YearMonth.parse_common_iso(\"2021-01\")
YearMonth(2021-01)
";
pub(crate) const YEARMONTH_REPLACE: &CStr = c"\
replace($self, /, *, year=None, month=None)
Expand Down Expand Up @@ -1634,6 +1634,20 @@ specify how to handle such a situation using the ``disambiguate`` argument.
See `the documentation <https://whenever.rtfd.io/en/latest/overview.html#arithmetic>`_
for more information.
";
pub(crate) const ZONEDDATETIME_DAY_LENGTH: &CStr = c"\
day_length($self)
--
The duration between the start of the current day and the next.
This is usually 24 hours, but may be different due to timezone transitions.
Example
-------
>>> ZonedDateTime(2020, 8, 15, tz=\"Europe/London\").day_length()
TimeDelta(24:00:00)
>>> ZonedDateTime(2023, 10, 29, tz=\"Europe/Amsterdam\").day_length()
TimeDelta(25:00:00)
";
pub(crate) const ZONEDDATETIME_FORMAT_COMMON_ISO: &CStr = c"\
format_common_iso($self)
--
Expand Down Expand Up @@ -1761,6 +1775,15 @@ Construct a new instance with the time replaced.
See the ``replace()`` method for more information.
";
pub(crate) const ZONEDDATETIME_START_OF_DAY: &CStr = c"\
start_of_day($self)
--
The start of the current calendar day.
This is almost always at midnight the same day, but may be different
for timezones which transition at—and thus skip over—midnight.
";
pub(crate) const ZONEDDATETIME_SUBTRACT: &CStr = c"\
subtract($self, delta=None, /, *, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, disambiguate=None)
--
Expand Down
17 changes: 8 additions & 9 deletions src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,15 @@ impl Display for Time {
}
}

pub(crate) const MIDNIGHT: Time = Time {
hour: 0,
minute: 0,
second: 0,
nanos: 0,
};

pub(crate) const SINGLETONS: &[(&CStr, Time); 3] = &[
(
c"MIDNIGHT",
Time {
hour: 0,
minute: 0,
second: 0,
nanos: 0,
},
),
(c"MIDNIGHT", MIDNIGHT),
(
c"NOON",
Time {
Expand Down
Loading

0 comments on commit 8ffefb0

Please sign in to comment.