Moment

when_exactly.Moment dataclass

A Moment represents a specific point in time with year, month, day, hour, minute, and second.

The Moment is analogous to the builtin datetime.datetime class, but it is immutable and provides additional functionality for date arithmetic.

Attributes:

Name Type Description
year int

The year (e.g., 2025)

month int

The month (1-12)

day int

The day of the month (1-31)

hour int

The hour (0-23)

minute int

The minute (0-59)

second int

The second (0-59)

Raises:

Type Description
when_exactly.core.errors.InvalidMomentError

If the provided values don't represent a valid date/time.

Example
>>> import when_exactly as wnx
>>> moment = wnx.Moment(2025, 1, 30, 15, 25, 30)
>>> moment
Moment(year=2025, month=1, day=30, hour=15, minute=25, second=30)
>>> moment.year
2025
>>> moment + wnx.Delta(days=1)
Moment(year=2025, month=1, day=31, hour=15, minute=25, second=30)

Source code in src/when_exactly/core/moment.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
@dataclasses.dataclass(frozen=True)
class Moment:
    """A Moment represents a specific point in time with year, month, day, hour, minute, and second.

    The `Moment` is analogous to the builtin [`datetime.datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) class,
    but it is immutable and provides additional functionality for date arithmetic.

    Attributes:
        year: The year (e.g., 2025)
        month: The month (1-12)
        day: The day of the month (1-31)
        hour: The hour (0-23)
        minute: The minute (0-59)
        second: The second (0-59)

    Raises:
        InvalidMomentError: If the provided values don't represent a valid date/time.

    Example:
        ```python
        >>> import when_exactly as wnx
        >>> moment = wnx.Moment(2025, 1, 30, 15, 25, 30)
        >>> moment
        Moment(year=2025, month=1, day=30, hour=15, minute=25, second=30)
        >>> moment.year
        2025
        >>> moment + wnx.Delta(days=1)
        Moment(year=2025, month=1, day=31, hour=15, minute=25, second=30)

        ```

    """

    year: int
    month: int
    day: int
    hour: int
    minute: int
    second: int

    def to_datetime(self) -> datetime.datetime:
        """Convert this Moment to a Python datetime.datetime object.

        Returns:
            A datetime.datetime object representing the same point in time.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> moment = wnx.Moment(2025, 1, 30, 15, 25, 30)
            >>> moment.to_datetime()
            datetime.datetime(2025, 1, 30, 15, 25, 30)

            ```
        """
        return datetime.datetime(
            self.year, self.month, self.day, self.hour, self.minute, self.second
        )

    @classmethod
    def from_datetime(cls, dt: datetime.datetime) -> Moment:
        """Create a Moment from a Python datetime.datetime object.

        Args:
            dt: The datetime object to convert.

        Returns:
            A new Moment representing the same point in time.

        Example:
            ```python
            >>> import datetime
            >>> import when_exactly as wnx
            >>> dt = datetime.datetime(2025, 1, 30, 15, 25, 30)
            >>> wnx.Moment.from_datetime(dt)
            Moment(year=2025, month=1, day=30, hour=15, minute=25, second=30)

            ```
        """
        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)

    def __post_init__(self) -> None:
        try:
            self.to_datetime()
        except ValueError as e:
            raise InvalidMomentError(str(e)) from e

    def __lt__(self, other: Moment) -> bool:
        return self.to_datetime() < other.to_datetime()

    def __le__(self, other: Moment) -> bool:
        return self.to_datetime() <= other.to_datetime()

    def __add__(self, delta: Delta) -> Moment:
        """Add a Delta to this Moment to get a new Moment.

        Args:
            delta: The time delta to add.

        Returns:
            A new Moment representing the point in time after adding the delta.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> moment = wnx.Moment(2025, 1, 31, 12, 30, 30)
            >>> moment + wnx.Delta(days=2)
            Moment(year=2025, month=2, day=2, hour=12, minute=30, second=30)
            >>> moment + wnx.Delta(months=1)
            Moment(year=2025, month=2, day=28, hour=12, minute=30, second=30)

            ```
        """
        new_moment_kwargs = {
            "year": self.year,
            "month": self.month,
            "day": self.day,
            "hour": self.hour,
            "minute": self.minute,
            "second": self.second,
        }
        if delta.years != 0:
            new_moment_kwargs["year"] += delta.years

        if delta.months != 0:
            new_moment_kwargs["month"] += delta.months
            while new_moment_kwargs["month"] > 12:
                new_moment_kwargs["month"] -= 12
                new_moment_kwargs["year"] += 1

            while new_moment_kwargs["month"] < 1:
                new_moment_kwargs["month"] += 12
                new_moment_kwargs["year"] -= 1

        while (
            new_moment_kwargs["day"] > 28
        ):  # if the day is too large for the month, wnx need to decrement it until it is valid
            try:
                Moment(**new_moment_kwargs)  # type: ignore
                break
            except InvalidMomentError:
                new_moment_kwargs["day"] -= 1

        dt = Moment(**new_moment_kwargs).to_datetime()  # type: ignore

        dt += datetime.timedelta(weeks=delta.weeks)
        dt += datetime.timedelta(days=delta.days)
        dt += datetime.timedelta(hours=delta.hours)
        dt += datetime.timedelta(minutes=delta.minutes)
        dt += datetime.timedelta(seconds=delta.seconds)

        return Moment.from_datetime(dt)

    def __sub__(self, delta: Delta) -> Moment:
        """Subtract a Delta from this Moment to get a new Moment.

        Args:
            delta: The time delta to subtract.

        Returns:
            A new Moment representing the point in time after subtracting the delta.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> moment = wnx.Moment(2025, 2, 5, 12, 30, 30)
            >>> moment - wnx.Delta(days=5)
            Moment(year=2025, month=1, day=31, hour=12, minute=30, second=30)

            ```
        """
        return self + Delta(
            years=-delta.years,
            months=-delta.months,
            weeks=-delta.weeks,
            days=-delta.days,
            hours=-delta.hours,
            minutes=-delta.minutes,
            seconds=-delta.seconds,
        )

    def __str__(self) -> str:
        return self.to_datetime().isoformat()

    @property
    def week_year(self) -> int:
        """The ISO week-numbering year of this moment.

        Returns:
            The ISO year, which may differ from the calendar year.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> # December 31, 2019 is in ISO week 2020-W01
            >>> moment = wnx.Moment(2019, 12, 31, 0, 0, 0)
            >>> moment.week_year
            2020

            ```
        """
        return self.to_datetime().isocalendar()[0]

    @property
    def week(self) -> int:
        """The ISO week number of this moment (1-53).

        Returns:
            The ISO week number.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> moment = wnx.Moment(2020, 1, 1, 0, 0, 0)
            >>> moment.week
            1

            ```
        """
        return self.to_datetime().isocalendar()[1]

    @property
    def week_day(self) -> int:
        """The ISO weekday of this moment (1=Monday, 7=Sunday).

        Returns:
            The ISO weekday number.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> # January 1, 2020 was a Wednesday
            >>> moment = wnx.Moment(2020, 1, 1, 0, 0, 0)
            >>> moment.week_day
            3

            ```
        """
        return self.to_datetime().isocalendar()[2]

    @property
    def ordinal_day(self) -> int:
        """The day of the year (1-366).

        Returns:
            The ordinal day within the year.

        Example:
            ```python
            >>> import when_exactly as wnx
            >>> moment = wnx.Moment(2020, 1, 1, 0, 0, 0)
            >>> moment.ordinal_day
            1
            >>> moment = wnx.Moment(2020, 12, 31, 0, 0, 0)
            >>> moment.ordinal_day
            366

            ```
        """
        return (
            self.to_datetime().toordinal()
            - datetime.date(self.year, 1, 1).toordinal()
            + 1
        )

ordinal_day property

The day of the year (1-366).

Returns:

Type Description
int

The ordinal day within the year.

Example
>>> import when_exactly as wnx
>>> moment = wnx.Moment(2020, 1, 1, 0, 0, 0)
>>> moment.ordinal_day
1
>>> moment = wnx.Moment(2020, 12, 31, 0, 0, 0)
>>> moment.ordinal_day
366

week property

The ISO week number of this moment (1-53).

Returns:

Type Description
int

The ISO week number.

Example
>>> import when_exactly as wnx
>>> moment = wnx.Moment(2020, 1, 1, 0, 0, 0)
>>> moment.week
1

week_day property

The ISO weekday of this moment (1=Monday, 7=Sunday).

Returns:

Type Description
int

The ISO weekday number.

Example
>>> import when_exactly as wnx
>>> # January 1, 2020 was a Wednesday
>>> moment = wnx.Moment(2020, 1, 1, 0, 0, 0)
>>> moment.week_day
3

week_year property

The ISO week-numbering year of this moment.

Returns:

Type Description
int

The ISO year, which may differ from the calendar year.

Example
>>> import when_exactly as wnx
>>> # December 31, 2019 is in ISO week 2020-W01
>>> moment = wnx.Moment(2019, 12, 31, 0, 0, 0)
>>> moment.week_year
2020

__add__(delta)

Add a Delta to this Moment to get a new Moment.

Parameters:

Name Type Description Default
delta when_exactly.core.delta.Delta

The time delta to add.

required

Returns:

Type Description
when_exactly.core.moment.Moment

A new Moment representing the point in time after adding the delta.

Example
>>> import when_exactly as wnx
>>> moment = wnx.Moment(2025, 1, 31, 12, 30, 30)
>>> moment + wnx.Delta(days=2)
Moment(year=2025, month=2, day=2, hour=12, minute=30, second=30)
>>> moment + wnx.Delta(months=1)
Moment(year=2025, month=2, day=28, hour=12, minute=30, second=30)

Source code in src/when_exactly/core/moment.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def __add__(self, delta: Delta) -> Moment:
    """Add a Delta to this Moment to get a new Moment.

    Args:
        delta: The time delta to add.

    Returns:
        A new Moment representing the point in time after adding the delta.

    Example:
        ```python
        >>> import when_exactly as wnx
        >>> moment = wnx.Moment(2025, 1, 31, 12, 30, 30)
        >>> moment + wnx.Delta(days=2)
        Moment(year=2025, month=2, day=2, hour=12, minute=30, second=30)
        >>> moment + wnx.Delta(months=1)
        Moment(year=2025, month=2, day=28, hour=12, minute=30, second=30)

        ```
    """
    new_moment_kwargs = {
        "year": self.year,
        "month": self.month,
        "day": self.day,
        "hour": self.hour,
        "minute": self.minute,
        "second": self.second,
    }
    if delta.years != 0:
        new_moment_kwargs["year"] += delta.years

    if delta.months != 0:
        new_moment_kwargs["month"] += delta.months
        while new_moment_kwargs["month"] > 12:
            new_moment_kwargs["month"] -= 12
            new_moment_kwargs["year"] += 1

        while new_moment_kwargs["month"] < 1:
            new_moment_kwargs["month"] += 12
            new_moment_kwargs["year"] -= 1

    while (
        new_moment_kwargs["day"] > 28
    ):  # if the day is too large for the month, wnx need to decrement it until it is valid
        try:
            Moment(**new_moment_kwargs)  # type: ignore
            break
        except InvalidMomentError:
            new_moment_kwargs["day"] -= 1

    dt = Moment(**new_moment_kwargs).to_datetime()  # type: ignore

    dt += datetime.timedelta(weeks=delta.weeks)
    dt += datetime.timedelta(days=delta.days)
    dt += datetime.timedelta(hours=delta.hours)
    dt += datetime.timedelta(minutes=delta.minutes)
    dt += datetime.timedelta(seconds=delta.seconds)

    return Moment.from_datetime(dt)

__sub__(delta)

Subtract a Delta from this Moment to get a new Moment.

Parameters:

Name Type Description Default
delta when_exactly.core.delta.Delta

The time delta to subtract.

required

Returns:

Type Description
when_exactly.core.moment.Moment

A new Moment representing the point in time after subtracting the delta.

Example
>>> import when_exactly as wnx
>>> moment = wnx.Moment(2025, 2, 5, 12, 30, 30)
>>> moment - wnx.Delta(days=5)
Moment(year=2025, month=1, day=31, hour=12, minute=30, second=30)

Source code in src/when_exactly/core/moment.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def __sub__(self, delta: Delta) -> Moment:
    """Subtract a Delta from this Moment to get a new Moment.

    Args:
        delta: The time delta to subtract.

    Returns:
        A new Moment representing the point in time after subtracting the delta.

    Example:
        ```python
        >>> import when_exactly as wnx
        >>> moment = wnx.Moment(2025, 2, 5, 12, 30, 30)
        >>> moment - wnx.Delta(days=5)
        Moment(year=2025, month=1, day=31, hour=12, minute=30, second=30)

        ```
    """
    return self + Delta(
        years=-delta.years,
        months=-delta.months,
        weeks=-delta.weeks,
        days=-delta.days,
        hours=-delta.hours,
        minutes=-delta.minutes,
        seconds=-delta.seconds,
    )

from_datetime(dt) classmethod

Create a Moment from a Python datetime.datetime object.

Parameters:

Name Type Description Default
dt datetime.datetime

The datetime object to convert.

required

Returns:

Type Description
when_exactly.core.moment.Moment

A new Moment representing the same point in time.

Example
>>> import datetime
>>> import when_exactly as wnx
>>> dt = datetime.datetime(2025, 1, 30, 15, 25, 30)
>>> wnx.Moment.from_datetime(dt)
Moment(year=2025, month=1, day=30, hour=15, minute=25, second=30)

Source code in src/when_exactly/core/moment.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@classmethod
def from_datetime(cls, dt: datetime.datetime) -> Moment:
    """Create a Moment from a Python datetime.datetime object.

    Args:
        dt: The datetime object to convert.

    Returns:
        A new Moment representing the same point in time.

    Example:
        ```python
        >>> import datetime
        >>> import when_exactly as wnx
        >>> dt = datetime.datetime(2025, 1, 30, 15, 25, 30)
        >>> wnx.Moment.from_datetime(dt)
        Moment(year=2025, month=1, day=30, hour=15, minute=25, second=30)

        ```
    """
    return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)

to_datetime()

Convert this Moment to a Python datetime.datetime object.

Returns:

Type Description
datetime.datetime

A datetime.datetime object representing the same point in time.

Example
>>> import when_exactly as wnx
>>> moment = wnx.Moment(2025, 1, 30, 15, 25, 30)
>>> moment.to_datetime()
datetime.datetime(2025, 1, 30, 15, 25, 30)

Source code in src/when_exactly/core/moment.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def to_datetime(self) -> datetime.datetime:
    """Convert this Moment to a Python datetime.datetime object.

    Returns:
        A datetime.datetime object representing the same point in time.

    Example:
        ```python
        >>> import when_exactly as wnx
        >>> moment = wnx.Moment(2025, 1, 30, 15, 25, 30)
        >>> moment.to_datetime()
        datetime.datetime(2025, 1, 30, 15, 25, 30)

        ```
    """
    return datetime.datetime(
        self.year, self.month, self.day, self.hour, self.minute, self.second
    )