Skip to main content

jiff_core/civil/
datetime.rs

1use crate::{
2    bounds::RangeError,
3    civil::{Date, Time},
4    macros::{ctry, unwrapr},
5    tz::Offset,
6    Timestamp,
7};
8
9/// A civil time of a day on a particular Gregorian date.
10#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
11#[cfg_attr(feature = "defmt", derive(defmt::Format))]
12pub struct DateTime {
13    date: Date,
14    time: Time,
15}
16
17impl DateTime {
18    /// The minimum allowed Gregorian date and clock time.
19    pub const MIN: DateTime = DateTime { date: Date::MIN, time: Time::MIN };
20
21    /// The maximum allowed Gregorian date and clock time.
22    pub const MAX: DateTime = DateTime { date: Date::MAX, time: Time::MAX };
23
24    /// Creates a new civil datetime from its constituent components.
25    ///
26    /// If any of the values are out of their supported ranges, then an
27    /// error is returned. Additionally, if `year`, `month` and `day` do not
28    /// correspond to a valid Gregorian date, then an error is returned.
29    #[inline]
30    pub const fn new(
31        year: i16,
32        month: i8,
33        day: i8,
34        hour: i8,
35        minute: i8,
36        second: i8,
37        subsec_nanosecond: i32,
38    ) -> Result<DateTime, RangeError> {
39        let date = ctry!(Date::new(year, month, day));
40        let time = ctry!(Time::new(hour, minute, second, subsec_nanosecond));
41        Ok(DateTime::from_parts(date, time))
42    }
43
44    /// Creates a new `DateTime` from its [`Date`] and [`Time`] components.
45    #[inline]
46    pub const fn from_parts(date: Date, time: Time) -> DateTime {
47        DateTime { date, time }
48    }
49
50    /// Returns the Gregorian date component of this datetime.
51    #[inline]
52    pub const fn date(&self) -> Date {
53        self.date
54    }
55
56    /// Returns the civil time component of this datetime.
57    #[inline]
58    pub const fn time(&self) -> Time {
59        self.time
60    }
61
62    /// Adds the given number of seconds to this civil datetime.
63    ///
64    /// This returns an error when the resulting datetime would exceed either
65    /// [`DateTime::MIN`] or [`DateTime::MAX`].
66    #[inline]
67    pub const fn checked_add_seconds(
68        &self,
69        seconds: i32,
70    ) -> Result<DateTime, RangeError> {
71        let (second, added_days) =
72            ctry!(self.time().to_second().overflowing_add(seconds));
73        let date = ctry!(self.date().checked_add(added_days));
74        let time = unwrapr!(
75            second
76                .to_time()
77                .with_subsec_nanosecond(self.time().subsec_nanosecond()),
78            "subsec we started from hasn't change and must be valid",
79        );
80        Ok(DateTime::from_parts(date, time))
81    }
82
83    /// Like `DateTime::checked_add_seconds`, but arithmetic saturates to
84    /// either [`DateTime::MIN`] (when `seconds < 0`) or [`DateTime::MAX`]
85    /// (when `seconds > 0`).
86    #[inline]
87    pub const fn saturating_add_seconds(&self, seconds: i32) -> DateTime {
88        match self.checked_add_seconds(seconds) {
89            Ok(dt) => dt,
90            Err(_) => {
91                if seconds < 0 {
92                    DateTime::MIN
93                } else {
94                    DateTime::MAX
95                }
96            }
97        }
98    }
99
100    /// Converts this datetime, along with its offset from UTC, to a
101    /// corresponding Unix timestamp.
102    ///
103    /// Note that unlike the reverse operation, [`Timestamp::to_datetime`],
104    /// this is fallible. This is by design. Namely, by making this routine
105    /// fallible at the boundaries, it permits the reverse operation to be
106    /// infallible. That is, all instants can be converted to a civil datetime
107    /// (appropriate for formatting), but not all civil datetimes combined with
108    /// all UTC offsets can be converted to an instant in time.
109    ///
110    /// This errors when the timestamp returned would be outside the range
111    /// given by [`Timestamp::MIN`] and [`Timestamp::MAX`].
112    #[inline]
113    pub const fn to_timestamp(
114        &self,
115        offset: Offset,
116    ) -> Result<Timestamp, RangeError> {
117        offset.to_timestamp(*self)
118    }
119}
120
121impl core::fmt::Debug for DateTime {
122    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
123        write!(f, "{:?}T{:?}", self.date(), self.time())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn datetime(
132        year: i16,
133        month: i8,
134        day: i8,
135        hour: i8,
136        minute: i8,
137        second: i8,
138        subsec_nanosecond: i32,
139    ) -> DateTime {
140        DateTime::new(
141            year,
142            month,
143            day,
144            hour,
145            minute,
146            second,
147            subsec_nanosecond,
148        )
149        .unwrap()
150    }
151
152    fn stamp(second: i64, subsec: i32) -> Timestamp {
153        Timestamp::new(second, subsec).unwrap()
154    }
155
156    fn offset(second: i32) -> Offset {
157        Offset::from_seconds(second).unwrap()
158    }
159
160    #[test]
161    fn checked_add_seconds() {
162        let dt = datetime(2026, 2, 25, 0, 0, 0, 0);
163        assert_eq!(
164            dt.checked_add_seconds(1),
165            Ok(datetime(2026, 2, 25, 0, 0, 1, 0))
166        );
167        assert_eq!(
168            dt.checked_add_seconds(86_399),
169            Ok(datetime(2026, 2, 25, 23, 59, 59, 0))
170        );
171        assert_eq!(
172            dt.checked_add_seconds(86_400),
173            Ok(datetime(2026, 2, 26, 0, 0, 0, 0))
174        );
175        assert_eq!(
176            dt.checked_add_seconds(-1),
177            Ok(datetime(2026, 2, 24, 23, 59, 59, 0))
178        );
179        assert_eq!(
180            dt.checked_add_seconds(-86_399),
181            Ok(datetime(2026, 2, 24, 0, 0, 1, 0))
182        );
183        assert_eq!(
184            dt.checked_add_seconds(-86_400),
185            Ok(datetime(2026, 2, 24, 0, 0, 0, 0))
186        );
187
188        let dt = datetime(2026, 2, 25, 0, 0, 0, 1);
189        assert_eq!(
190            dt.checked_add_seconds(1),
191            Ok(datetime(2026, 2, 25, 0, 0, 1, 1))
192        );
193        assert_eq!(
194            dt.checked_add_seconds(86_399),
195            Ok(datetime(2026, 2, 25, 23, 59, 59, 1))
196        );
197        assert_eq!(
198            dt.checked_add_seconds(86_400),
199            Ok(datetime(2026, 2, 26, 0, 0, 0, 1))
200        );
201        assert_eq!(
202            dt.checked_add_seconds(-1),
203            Ok(datetime(2026, 2, 24, 23, 59, 59, 1))
204        );
205        assert_eq!(
206            dt.checked_add_seconds(-86_399),
207            Ok(datetime(2026, 2, 24, 0, 0, 1, 1))
208        );
209        assert_eq!(
210            dt.checked_add_seconds(-86_400),
211            Ok(datetime(2026, 2, 24, 0, 0, 0, 1))
212        );
213    }
214
215    #[test]
216    fn to_timestamp_no_subsec() {
217        let dt = datetime(1970, 1, 1, 0, 0, 0, 0);
218        assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(0, 0)));
219        assert_eq!(dt.to_timestamp(offset(3600)), Ok(stamp(-3600, 0)));
220        assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(3600, 0)));
221
222        let dt = datetime(1969, 12, 31, 23, 30, 0, 0);
223        assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(-1800, 0)));
224        assert_eq!(dt.to_timestamp(offset(3600)), Ok(stamp(-5400, 0)));
225        assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(1800, 0)));
226
227        let dt = datetime(1970, 1, 1, 0, 30, 0, 0);
228        assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(1800, 0)));
229        assert_eq!(dt.to_timestamp(offset(3600)), Ok(stamp(-1800, 0)));
230        assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(5400, 0)));
231    }
232
233    #[test]
234    fn to_timestamp_with_subsec() {
235        let dt = datetime(1970, 1, 1, 0, 0, 0, 123);
236        assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(0, 123)));
237        assert_eq!(
238            dt.to_timestamp(offset(3600)),
239            Ok(stamp(-3599, -999_999_877))
240        );
241        assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(3600, 123)));
242
243        let dt = datetime(1969, 12, 31, 23, 30, 0, 123);
244        assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(-1799, -999_999_877)));
245        assert_eq!(
246            dt.to_timestamp(offset(3600)),
247            Ok(stamp(-5399, -999_999_877))
248        );
249        assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(1800, 123)));
250
251        let dt = datetime(1970, 1, 1, 0, 30, 0, 123);
252        assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(1800, 123)));
253        assert_eq!(
254            dt.to_timestamp(offset(3600)),
255            Ok(stamp(-1799, -999_999_877))
256        );
257        assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(5400, 123)));
258    }
259
260    #[test]
261    fn to_timestamp_err() {
262        let dt = datetime(-9999, 1, 1, 0, 0, 0, 0);
263        assert_eq!(dt.to_timestamp(Offset::MIN), Ok(Timestamp::MIN));
264        assert!(dt.to_timestamp(offset(Offset::MIN.seconds() + 1)).is_err());
265
266        let dt = datetime(9999, 12, 31, 23, 59, 59, 999_999_999);
267        assert_eq!(dt.to_timestamp(Offset::MAX), Ok(Timestamp::MAX));
268        assert!(dt.to_timestamp(offset(Offset::MAX.seconds() - 1)).is_err());
269
270        let dt = datetime(9999, 12, 31, 23, 59, 59, 0);
271        assert!(dt.to_timestamp(offset(Offset::MAX.seconds() - 1)).is_err());
272    }
273}