ako/
moment.rs

1use core::fmt::{self, Debug, Display, Formatter};
2use core::ops::{Add, Sub};
3use core::str::FromStr;
4use core::time::Duration;
5
6use crate::calendar::Iso;
7use crate::{AsMoment, AsTime, Calendar, Date, DateTime, PlainTime, TimeInterval, TimeZone};
8
9const SECONDS_IN_DAY: i64 = 86_400;
10
11/// A precise **moment** in time.
12///
13/// A [`Moment`] has no concept of a particular time zone or calendar. It represents a single
14/// point in time that can be globally agreed upon.
15///
16/// Internally, moments are represented in the [Coordinated Universal Time] (UTC) time scale
17/// as the signed number of seconds and sub-second nanoseconds since the [Unix epoch] at January 1, 1970.
18/// A positive number of seconds indicates a moment after the Unix epoch where a negative number of
19/// seconds indicates a moment before the Unix epoch.
20///
21/// A [`Moment`] can be created from and displayed as a [RFC 3339] string like `2018-02-15T03:41:23Z` with
22/// the [`FromStr`][core::str::FromStr] and [`Display`][core::fmt::Display] traits.
23///
24/// [Unix epoch]: https://en.wikipedia.org/wiki/Unix_time
25/// [Unix time]: https://en.wikipedia.org/wiki/Unix_time
26/// [Julian Day]: https://en.wikipedia.org/wiki/Julian_day
27/// [Coordinated Universal Time]: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
28/// [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339
29///
30/// # Interoperability
31///
32/// The [`Moment`] type can be created from and converted to many representations of exact time
33/// and other time scales, such as [Unix time], a [Julian day], or [RFC 3339].<sup>†</sup>
34///
35/// <sup>†</sup><sub>Please feel free to [open an issue](https://github.com/transact-rs/ako/issues/new) requesting additional
36/// representations for conversion to and from.</sub>
37///
38/// ## [Unix Time]
39///
40/// [Unix time] is widely used as a storage and transmission format for date and time. It
41/// is the count of non-leap seconds from January 1, 1970, at 00:00:00 UTC.
42///
43/// #### Create a [`Moment`] from a Unix time, in milliseconds
44///
45/// ```rust
46/// use ako::Moment;
47///
48/// let moment = Moment::from_unix_milliseconds(1727597174768);
49///
50/// assert_eq!(moment.to_string(), "2024-09-29T08:06:14.768000000Z");
51/// ```
52///
53/// #### Get the current time as a Unix timestamp
54///
55/// ```rust
56/// use ako::Moment;
57///
58/// // secs is the number of seconds since the Unix epoch
59/// // nsec is the sub-second nanoseconds
60/// let (secs, nsec) = Moment::now().to_unix();
61/// ```
62///
63/// ## [`SystemTime`][std::time::SystemTime] ([`std`])
64///
65/// [`Moment`] and [`std::time::SystemTime`][std::time::SystemTime] can be freely converted between each other.
66///
67/// #### Get a value of [`SystemTime`][std::time::SystemTime] from RFC 3339
68///
69/// ```rust
70/// use std::time::SystemTime;
71/// use ako::Moment;
72///
73/// # fn main() -> ako::Result<()> {
74/// let moment: Moment = "2018-02-15T03:41:23Z".parse()?;
75/// let system = SystemTime::from(moment);
76/// #
77/// # assert_eq!(Moment::from(system), moment);
78/// # Ok(()) }
79/// ```
80///
81/// #### Create a [`Moment`] from the current system time
82///
83/// ```rust
84/// use std::time::SystemTime;
85/// use ako::Moment;
86///
87/// let now: Moment = SystemTime::now().into();
88/// ```
89///
90/// This is what [`Moment::now()`] does internally.
91///
92#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93pub struct Moment {
94    pub(crate) secs: i64,
95    pub(crate) nsec: u32,
96}
97
98// Constants
99impl Moment {
100    /// The maximum supported [`Moment`], `+1587287-03-17T15:30:07.999999999Z`.
101    ///
102    /// # Examples
103    ///
104    /// ```rust
105    /// use ako::Moment;
106    ///
107    /// assert_eq!(Moment::MAX.to_unix_seconds(), i64::MAX);
108    /// # assert_eq!(Moment::MAX.to_string(), "+1587287-03-17T15:30:07.999999999Z");
109    /// ```
110    ///
111    pub const MAX: Self = Self {
112        secs: i64::MAX,
113        nsec: 999_999_999,
114    };
115
116    /// The minimum (the largest negative) supported [`Moment`], `-1583348-10-16T08:29:52Z`.
117    ///
118    /// # Examples
119    ///
120    /// ```rust
121    /// use ako::Moment;
122    ///
123    /// assert_eq!(Moment::MIN.to_unix_seconds(), i64::MIN);
124    /// # assert_eq!(Moment::MIN.to_string(), "-1583348-10-16T08:29:52Z");
125    /// ```
126    ///
127    pub const MIN: Self = Self {
128        secs: i64::MIN,
129        nsec: 0,
130    };
131}
132
133// Construction
134impl Moment {
135    /// Returns the moment corresponding to “now.”
136    ///
137    /// # Examples
138    ///
139    /// ```rust
140    /// use ako::Moment;
141    ///
142    /// // Current moment as reported by the system
143    /// let now = Moment::now();
144    /// # assert!(now.to_unix_seconds() >= 1727634133);
145    /// ```
146    ///
147    #[cfg(feature = "std")]
148    #[must_use]
149    pub fn now() -> Self {
150        std::time::SystemTime::now().into()
151    }
152}
153
154// Conversion From
155impl Moment {
156    // This exists because From::from is not `const`
157    /// Obtains the moment corresponding to the given [`Date`].
158    #[must_use]
159    pub(crate) const fn from_date<C: Calendar>(date: Date<C>) -> Self {
160        Self {
161            secs: (date.as_days_since_unix_epoch() as i64) * SECONDS_IN_DAY,
162            nsec: 0,
163        }
164    }
165
166    /// Creates a [`Moment`] from the given number of `seconds` and `nanoseconds`
167    /// since the Unix epoch of January 1, 1970, at 00:00:00 UTC.
168    ///
169    /// Allows for an out-of-range `nanoseconds` value to be passed in. The
170    /// `nanoseconds` will overflow into `seconds` to ensure that the
171    /// stored sub-second nanoseconds are in the range 0 to 999,999,999.
172    ///
173    #[must_use]
174    pub const fn from_unix(seconds: i64, nanoseconds: u32) -> Self {
175        let mut nanoseconds = nanoseconds as i128;
176        nanoseconds += seconds as i128 * 1_000_000_000;
177
178        Self::from_unix_nanoseconds(nanoseconds)
179    }
180
181    /// Creates a [`Moment`] from the given number of `seconds`
182    /// since the Unix epoch of January 1, 1970, at 00:00:00 UTC.
183    ///
184    /// # Examples
185    ///
186    /// ```rust
187    /// use ako::Moment;
188    ///
189    /// // first appearance of the gregorian date represented by the
190    /// // Unix timestamp to be in the timestamp
191    /// let date_in_timestamp = Moment::from_unix_seconds(1_1973_10_17);
192    /// assert_eq!(date_in_timestamp.to_string(), "1973-10-17T18:36:57Z");
193    /// ```
194    ///
195    #[must_use]
196    pub const fn from_unix_seconds(seconds: i64) -> Self {
197        Self {
198            secs: seconds,
199            nsec: 0,
200        }
201    }
202
203    /// Creates a [`Moment`] from the given number of `milliseconds`
204    /// since the Unix epoch of January 1, 1970, at 00:00:00 UTC.
205    #[must_use]
206    pub const fn from_unix_milliseconds(milliseconds: i64) -> Self {
207        Self::from_unix_nanoseconds(milliseconds as i128 * 1_000_000)
208    }
209
210    /// Creates a [`Moment`] from the given number of `microseconds`
211    /// since the Unix epoch of January 1, 1970, at 00:00:00 UTC.
212    #[must_use]
213    pub const fn from_unix_microseconds(microseconds: i128) -> Self {
214        Self::from_unix_nanoseconds(microseconds * 1_000)
215    }
216
217    /// Creates a [`Moment`] from the given number of `nanoseconds`
218    /// since the Unix epoch of January 1, 1970, at 00:00:00 UTC.
219    #[must_use]
220    pub const fn from_unix_nanoseconds(nanoseconds: i128) -> Self {
221        let secs = nanoseconds.div_euclid(1_000_000_000) as i64;
222        let nsec = nanoseconds.rem_euclid(1_000_000_000) as u32;
223
224        Self { secs, nsec }
225    }
226
227    /// Creates a [`Moment`] corresponding to the given [Julian day] (JD).
228    ///
229    /// The Julian day is the continuous count of days and fractions thereof
230    /// from the beginning of the Julian period, where day 0 is Monday, January 1, 4713 BCE of the
231    /// Julian calendar.
232    ///
233    /// [Julian day]: https://en.wikipedia.org/wiki/Julian_day
234    ///
235    #[cfg(feature = "astronomy")]
236    pub fn from_julian_day(jd: f64) -> crate::Result<Self> {
237        crate::astronomy::from_julian_day(jd)
238    }
239
240    /// Creates a [`Moment`] corresponding to the given [Julian day] (JD) in
241    /// the Dynamical timescale (also known as [Ephemeris time]).
242    ///
243    /// [Julian day]: https://en.wikipedia.org/wiki/Julian_day
244    /// [Ephemeris time]: https://en.wikipedia.org/wiki/Ephemeris_time
245    ///
246    #[cfg(feature = "astronomy")]
247    pub fn from_julian_ephemeris_day(jde: f64) -> crate::Result<Self> {
248        let mut moment = Self::from_julian_day(jde)?;
249
250        // the difference (ΔT) between dynamic time (TD) and universal time (UT), in seconds
251        let dt = crate::astronomy::delta_t(moment.to_julian_year());
252        moment.secs -= dt as i64;
253
254        Ok(moment)
255    }
256}
257
258// Composition
259impl Moment {
260    /// Converts this moment to a [`DateTime`] projected to the ISO calendar, at the
261    /// given time zone.
262    ///
263    #[must_use]
264    pub(crate) fn at(self, tz: &TimeZone) -> DateTime<Iso> {
265        DateTime::from_moment(Iso, self).at(tz)
266    }
267
268    /// Converts this moment to a [`DateTime`] projected on the given `calendar`.
269    ///
270    #[must_use]
271    pub(crate) const fn on<C: Calendar>(self, calendar: C) -> DateTime<C> {
272        DateTime::from_moment(calendar, self)
273    }
274
275    /// Gets this moment as a date projected to the given calendar.
276    ///
277    #[must_use]
278    pub(crate) const fn as_date<C: Calendar>(self, calendar: C) -> Date<C> {
279        Date::from_unix_timestamp(calendar, self.secs)
280    }
281}
282
283// Conversion To
284impl Moment {
285    /// Gets the number of seconds and nanoseconds since the Unix epoch.
286    /// Negative seconds represent moments before the Unix epoch.
287    #[must_use]
288    pub const fn to_unix(self) -> (i64, u32) {
289        (self.secs, self.nsec)
290    }
291
292    /// Gets the number of seconds since the Unix epoch.
293    /// Negative seconds represent moments before the Unix epoch.
294    #[must_use]
295    pub const fn to_unix_seconds(self) -> i64 {
296        self.secs
297    }
298
299    /// Gets the number of milliseconds since the Unix epoch.
300    #[must_use]
301    pub const fn to_unix_milliseconds(self) -> i64 {
302        self.to_unix_nanoseconds().div_euclid(1_000_000) as i64
303    }
304
305    /// Gets the number of microseconds since the Unix epoch.
306    #[must_use]
307    pub const fn to_unix_microseconds(self) -> i64 {
308        self.to_unix_nanoseconds().div_euclid(1_000) as i64
309    }
310
311    /// Gets the number of nanoseconds since the Unix epoch.
312    #[must_use]
313    pub const fn to_unix_nanoseconds(self) -> i128 {
314        ((self.secs as i128) * 1_000_000_000) + (self.nsec as i128)
315    }
316
317    /// Gets the [Julian day] (JD) for this moment.
318    ///
319    /// [Julian day]: https://en.wikipedia.org/wiki/Julian_day
320    ///
321    #[cfg(feature = "astronomy")]
322    #[must_use]
323    pub fn to_julian_day(self) -> f64 {
324        crate::astronomy::to_julian_day(self)
325    }
326
327    /// Gets the [Julian year] (a) for this moment.
328    ///
329    /// In astronomy, a [Julian year] (a) is a unit of measurement of time defined
330    /// as exactly 365.25 days of 86,400 seconds each. The [Julian year] does not
331    /// correspond to years in any calendar.
332    ///
333    /// [Julian year]: https://en.wikipedia.org/wiki/Julian_year_(astronomy)
334    ///
335    #[cfg(feature = "astronomy")]
336    #[must_use]
337    pub fn to_julian_year(self) -> f64 {
338        crate::astronomy::to_julian_year(self)
339    }
340}
341
342impl Add<TimeInterval> for Moment {
343    type Output = Self;
344
345    fn add(self, rhs: TimeInterval) -> Self::Output {
346        let nanoseconds = self
347            .to_unix_nanoseconds()
348            .checked_add(rhs.as_total_nanoseconds())
349            .expect("overflow adding `TimeInterval` to `Moment`");
350
351        Self::from_unix_nanoseconds(nanoseconds)
352    }
353}
354
355impl Add<Moment> for TimeInterval {
356    type Output = Moment;
357
358    #[inline]
359    fn add(self, rhs: Moment) -> Self::Output {
360        rhs + self
361    }
362}
363
364impl Sub<TimeInterval> for Moment {
365    type Output = Self;
366
367    fn sub(self, rhs: TimeInterval) -> Self::Output {
368        let nanoseconds = self
369            .to_unix_nanoseconds()
370            .checked_sub(rhs.as_total_nanoseconds())
371            .expect("overflow subtracting `TimeInterval` from `Moment`");
372
373        Self::from_unix_nanoseconds(nanoseconds)
374    }
375}
376
377impl Add<Duration> for Moment {
378    type Output = Self;
379
380    fn add(self, rhs: Duration) -> Self::Output {
381        let nanoseconds = i128::try_from(rhs.as_nanos())
382            .ok()
383            .and_then(|rhs| self.to_unix_nanoseconds().checked_add(rhs))
384            .expect("overflow adding `Duration` to `Moment`");
385
386        Self::from_unix_nanoseconds(nanoseconds)
387    }
388}
389
390impl Add<Moment> for Duration {
391    type Output = Moment;
392
393    #[inline]
394    fn add(self, rhs: Moment) -> Self::Output {
395        rhs + self
396    }
397}
398
399impl Sub<Duration> for Moment {
400    type Output = Self;
401
402    fn sub(self, rhs: Duration) -> Self::Output {
403        let nanoseconds = i128::try_from(rhs.as_nanos())
404            .ok()
405            .and_then(|rhs| self.to_unix_nanoseconds().checked_sub(rhs))
406            .expect("overflow subtracting `Duration` from `Moment`");
407
408        Self::from_unix_nanoseconds(nanoseconds)
409    }
410}
411
412#[cfg(feature = "std")]
413impl From<std::time::SystemTime> for Moment {
414    fn from(time: std::time::SystemTime) -> Self {
415        use std::time::SystemTime;
416
417        let (secs, nsec) = time.duration_since(SystemTime::UNIX_EPOCH).map_or_else(
418            |_| {
419                let ts = SystemTime::UNIX_EPOCH
420                    .duration_since(time)
421                    .unwrap_or_default();
422
423                (-(ts.as_secs() as i64), ts.subsec_nanos())
424            },
425            |ts| (ts.as_secs() as i64, ts.subsec_nanos()),
426        );
427
428        Self { secs, nsec }
429    }
430}
431
432#[cfg(feature = "std")]
433impl From<Moment> for std::time::SystemTime {
434    fn from(moment: Moment) -> Self {
435        let (secs, nsec) = moment.to_unix();
436        let duration = Duration::new(secs.unsigned_abs(), nsec);
437
438        if secs.is_negative() {
439            Self::UNIX_EPOCH - duration
440        } else {
441            Self::UNIX_EPOCH + duration
442        }
443    }
444}
445
446impl<C: Calendar> From<Date<C>> for Moment {
447    fn from(date: Date<C>) -> Self {
448        Self::from_date(date)
449    }
450}
451
452impl Display for Moment {
453    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
454        f.pad(&self.format_rfc3339())
455    }
456}
457
458impl Debug for Moment {
459    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
460        <Self as Display>::fmt(self, f)
461    }
462}
463
464impl FromStr for Moment {
465    type Err = crate::Error;
466
467    fn from_str(s: &str) -> Result<Self, Self::Err> {
468        Self::parse_rfc3339(s)
469    }
470}
471
472impl AsMoment for Moment {
473    fn as_moment(&self) -> Self {
474        *self
475    }
476}
477
478impl AsTime for Moment {
479    fn as_time(&self) -> PlainTime {
480        let (secs, nsec) = self.as_timestamp();
481        PlainTime::from_unix_timestamp(secs, nsec)
482    }
483
484    fn as_timestamp(&self) -> (i64, u32) {
485        self.to_unix()
486    }
487}
488
489impl Sub<Self> for Moment {
490    type Output = TimeInterval;
491
492    fn sub(self, rhs: Self) -> Self::Output {
493        // between flips the order, think of it like you write "end - start" but
494        // you would say "between start and end"
495        TimeInterval::between(rhs, self)
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use core::time::Duration;
502
503    use test_case::test_case;
504
505    use crate::{AsMoment, DateTime, Moment};
506
507    #[cfg(feature = "astronomy")]
508    // From Astronomical Algorithms 2nd Edition, Chapter 7, Page 62
509    #[test_case(2443259.9, "1977-04-26T09:35:59.999991953Z")]
510    #[test_case(2451545.0, "2000-01-01T12:00:00Z")]
511    #[test_case(2415020.5, "1900-01-01T00:00:00Z")]
512    #[test_case(2299161.5, "1582-10-16T00:00:00Z")]
513    #[test_case(2299160.5, "1582-10-15T00:00:00Z")]
514    #[test_case(2299159.5, "1582-10-14T00:00:00Z")]
515    #[test_case(2026871.8, "0837-04-14T07:12:00.000004023Z")]
516    #[test_case(1355671.4, "-1001-08-07T21:35:59.999991953Z")]
517    #[test_case(0.0, "-4713-11-24T12:00:00Z")]
518    fn moment_from_julian_day(jd: f64, expected: &str) -> crate::Result<()> {
519        let moment = Moment::from_julian_day(jd)?;
520
521        assert_eq!(moment.format_rfc3339(), expected);
522        assert_eq!(moment.to_julian_day(), jd);
523
524        let dt = DateTime::parse_rfc3339(expected)?;
525
526        assert_eq!(dt.as_moment(), moment);
527
528        Ok(())
529    }
530
531    #[cfg(feature = "std")]
532    #[test_case(-1727313985 ; "negative 1727313985")]
533    #[test_case(0)]
534    #[test_case(1727313985)]
535    fn moment_from_system_time(timestamp: i64) {
536        use std::time::SystemTime;
537
538        let moment = Moment::from_unix_seconds(timestamp);
539        let st = SystemTime::from(moment);
540        let moment_2 = Moment::from(st);
541
542        assert_eq!(moment_2, moment);
543    }
544
545    #[test_case(1727313985, 86_400, "2024-09-27T01:26:25Z")]
546    #[test_case(1727313985, 18_000, "2024-09-26T06:26:25Z")]
547    #[test_case(1727313985, 600, "2024-09-26T01:36:25Z")]
548    fn moment_add_duration(timestamp: i64, seconds: u64, expected: &str) {
549        let mut moment = Moment::from_unix_seconds(timestamp);
550        moment = moment + Duration::from_secs(seconds);
551
552        assert_eq!(moment.format_rfc3339(), expected);
553    }
554
555    #[test_case(1727313985, 86_400, "2024-09-25T01:26:25Z")]
556    #[test_case(1727313985, 18_000, "2024-09-25T20:26:25Z")]
557    #[test_case(1727313985, 600, "2024-09-26T01:16:25Z")]
558    fn moment_sub_duration(timestamp: i64, seconds: u64, expected: &str) {
559        let mut moment = Moment::from_unix_seconds(timestamp);
560        moment = moment - Duration::from_secs(seconds);
561
562        assert_eq!(moment.format_rfc3339(), expected);
563    }
564
565    #[test_case(1727313985, 1727480105, (1, 22, 8, 40))]
566    fn moment_sub_moment(timestamp_start: i64, timestamp_end: i64, expected: (i64, i8, i8, i8)) {
567        let moment_start = Moment::from_unix_seconds(timestamp_start);
568        let moment_end = Moment::from_unix_seconds(timestamp_end);
569        let ti = moment_end - moment_start;
570
571        assert_eq!(ti.days(), expected.0);
572        assert_eq!(ti.hours(), expected.1);
573        assert_eq!(ti.minutes(), expected.2);
574        assert_eq!(ti.seconds(), expected.3);
575    }
576}