Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next small release #199

Merged
merged 3 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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