irox_time/
lib.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5//!
6//! Module Structure
7//! -----------------
8//!  * [`crate`] - Contains the base `Time` struct, describing a standard `Hours/minutes/seconds` framework.
9//!  * [`datetime`] - Contains `UTCDateTime` structs, describing a `Date` with a `Time`
10//!  * [`epoch`] - Contains `Epoch`, `UnixEpoch`, `GPSEpoch`, and others, providing the datum anchor for timestamps
11//!     `UnixTimestamp`, `GPSTimestamp`, etc.
12//!  * [`gregorian`] - Contains `Date` and `Month`, that describe a gregorian calendar date.
13//!  * [`julian`] - Contains `JulianDate` and it's associated epochs.
14//!  * [`crate::format`] - Contains `Format` and `FormatParser` to tranlate dates to and from strings.
15//!    * [`crate::format::iso8601`] - ISO8601 Implementations of `DateFormat` and `DateFormatParser`
16//!
17//! The top level module Contains the various representations of [`Time`]
18//!
19//! A [`Time`] is a specific time offset into a Day.  Intended for use where Hour:Minute:Seconds are
20//! needed.
21//!
22//! The following are variants of [`epoch::Timestamp`], with specific methods and sizes to
23//! to represent the Duration against an [`Epoch`].  These follow the same binary format as the NTP
24//! Timestamp format, if used with the `NTP Epoch`.
25//! * A [`Time32`] is a Q16.16 `Timestamp` where Seconds and Fractional Seconds are `u16`'s
26//! * A [`Time64`] is a Q32.32 `Timestamp` where Seconds and Fractional Seconds are `u32`'s
27//! * A [`Time128`] is a Q64.64 `Timestamp` where Seconds and Fractional Seconds are `u64`'s
28//!
29#![forbid(unsafe_code)]
30#![cfg_attr(not(feature = "std"), no_std)]
31
32extern crate alloc;
33use crate::datetime::UTCDateTime;
34use crate::epoch::{Epoch, Timestamp, UnixTimestamp, UNIX_EPOCH};
35use crate::format::iso8601::ISO8601_DATE_TIME;
36use crate::format::{Format, FormatError, FormatParser};
37use alloc::string::String;
38use core::cmp::Ordering;
39use core::fmt::{Display, Formatter};
40use core::hash::{Hash, Hasher};
41use irox_fixedmath::{FixedU128, FixedU32, FixedU64};
42pub use irox_units::bounds::{GreaterThanEqualToValueError, LessThanValue, Range};
43pub use irox_units::units::duration::{Duration, DurationUnit};
44use irox_units::units::duration::{NANOS_TO_SEC, SEC_TO_NANOS};
45
46pub mod datetime;
47pub mod epoch;
48pub mod format;
49pub mod gregorian;
50pub mod julian;
51
52///
53/// Represents a time of the day, an offset into the day from midnight.
54///
55/// Corresponds to a `UTC of day` in section 5.3.3 of ISO8601
56#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
57pub struct Time {
58    second_of_day: u32,
59    nanoseconds: u32,
60}
61
62impl Time {
63    ///
64    /// Creates a Time from the specified seconds and nanoseconds,
65    ///
66    /// The valid range of 'second_of_day' is `0..86400`,
67    /// The valid range of 'nanoseconds' is `0..1_000_000_000`
68    pub fn new(
69        second_of_day: u32,
70        nanoseconds: u32,
71    ) -> Result<Time, GreaterThanEqualToValueError<u32>> {
72        LessThanValue::new(86400).check_value_is_valid(&second_of_day)?;
73        LessThanValue::new(1_000_000_000).check_value_is_valid(&nanoseconds)?;
74        Ok(Time {
75            second_of_day,
76            nanoseconds,
77        })
78    }
79
80    ///
81    /// Creates a Time from the specified fractional seconds, valid range `0..86400`
82    pub fn from_seconds_f64(seconds: f64) -> Result<Time, GreaterThanEqualToValueError<f64>> {
83        LessThanValue::new(86400_f64).check_value_is_valid(&seconds)?;
84
85        let second_of_day = seconds as u32;
86        let frac_nanos = seconds - second_of_day as f64;
87        let nanoseconds = (frac_nanos * SEC_TO_NANOS) as u32;
88        Ok(Time {
89            second_of_day,
90            nanoseconds,
91        })
92    }
93
94    pub fn from_hms(
95        hours: u8,
96        minutes: u8,
97        seconds: u8,
98    ) -> Result<Time, GreaterThanEqualToValueError<u8>> {
99        LessThanValue::new(24u8).check_value_is_valid(&hours)?;
100        LessThanValue::new(60u8).check_value_is_valid(&minutes)?;
101        LessThanValue::new(60u8).check_value_is_valid(&seconds)?;
102
103        let second_of_day = hours as u32 * 3600 + minutes as u32 * 60 + seconds as u32;
104        Ok(Time {
105            second_of_day,
106            nanoseconds: 0,
107        })
108    }
109
110    pub fn from_hms_f64(
111        hours: u8,
112        minutes: u8,
113        seconds: f64,
114    ) -> Result<Time, GreaterThanEqualToValueError<f64>> {
115        LessThanValue::new(24u8).check_value_is_valid(&hours)?;
116        LessThanValue::new(60u8).check_value_is_valid(&minutes)?;
117        LessThanValue::new(60f64).check_value_is_valid(&seconds)?;
118        let nanoseconds = (irox_tools::f64::FloatExt::fract(seconds) * SEC_TO_NANOS) as u32;
119        let second_of_day = hours as u32 * 3600 + minutes as u32 * 60 + seconds as u32;
120        Ok(Time {
121            second_of_day,
122            nanoseconds,
123        })
124    }
125
126    pub fn from_hms_millis(
127        hours: u8,
128        minutes: u8,
129        seconds: u8,
130        millis: u32,
131    ) -> Result<Time, GreaterThanEqualToValueError<u32>> {
132        LessThanValue::new(1_000_000).check_value_is_valid(&millis)?;
133        let time = Self::from_hms(hours, minutes, seconds)?;
134        Ok(Time {
135            second_of_day: time.second_of_day,
136            nanoseconds: millis * 1000,
137        })
138    }
139
140    ///
141    /// Returns the number of seconds into the "current day"
142    #[must_use]
143    pub fn get_seconds(&self) -> u32 {
144        self.second_of_day
145    }
146
147    ///
148    /// Returns the number of fractional second nanoseconds
149    #[must_use]
150    pub fn get_nanoseconds(&self) -> u32 {
151        self.nanoseconds
152    }
153
154    ///
155    /// Converts this time into a duration
156    #[must_use]
157    pub fn as_duration(&self) -> Duration {
158        let time = self.second_of_day as f64 + self.nanoseconds as f64 * NANOS_TO_SEC;
159        Duration::new(time, DurationUnit::Second)
160    }
161
162    ///
163    /// Returns the number of hours represented by this time.
164    #[must_use]
165    pub fn as_hours(&self) -> u32 {
166        (self.second_of_day as f64 / SECONDS_IN_HOUR as f64) as u32
167    }
168
169    ///
170    /// Returns the minute offset into the day represented by this time.
171    #[must_use]
172    pub fn as_minutes(&self) -> u32 {
173        (self.second_of_day as f64 / SECONDS_IN_MINUTE as f64) as u32
174    }
175
176    ///
177    /// Returns a triplet, (hours, minutes, seconds) representing this time
178    #[must_use]
179    pub fn as_hms(&self) -> (u32, u32, u32) {
180        let hours = self.as_hours();
181        let minutes = self.as_minutes() - hours * MINUTES_IN_HOUR;
182        let seconds = self.get_seconds() - hours * SECONDS_IN_HOUR - minutes * SECONDS_IN_MINUTE;
183        (hours, minutes, seconds)
184    }
185
186    ///
187    /// Returns a triplet, (hours, minutes, seconds) representing this time, with seconds as [`f64`]
188    #[must_use]
189    pub fn as_hms_f64(&self) -> (u32, u32, f64) {
190        let hours = self.as_hours();
191        let minutes = self.as_minutes() - hours * MINUTES_IN_HOUR;
192        let seconds = self.get_seconds() - hours * SECONDS_IN_HOUR - minutes * SECONDS_IN_MINUTE;
193        let seconds = seconds as f64 + self.get_secondsfrac();
194        (hours, minutes, seconds)
195    }
196
197    ///
198    /// Returns ONLY the fractional seconds component of the timestamp
199    #[must_use]
200    pub fn get_secondsfrac(&self) -> f64 {
201        self.nanoseconds as f64 * NANOS_TO_SEC
202    }
203
204    ///
205    /// Formats this Time using the specified formatter
206    #[must_use]
207    pub fn format<F: Format<Self>>(&self, format: &F) -> String {
208        format.format(self)
209    }
210
211    ///
212    /// Tries to parse a Time from the string using the specified Formatter
213    pub fn parse_from<F: FormatParser<Self>>(
214        format: &F,
215        string: &str,
216    ) -> Result<Self, FormatError> {
217        format.try_from(string)
218    }
219
220    ///
221    /// Adds the duration to this time, returning a new value of 'time'.  If the duration is longer
222    /// than a single day, returns the number of days that got consumed in the second 'duration'
223    /// parameter
224    /// # Example:
225    /// ```
226    /// # use std::error::Error;
227    /// # use irox_time::Time;
228    /// # use irox_units::bounds::GreaterThanEqualToValueError;
229    /// # use irox_units::units::duration::Duration;
230    /// # pub fn test() -> Result<(), GreaterThanEqualToValueError<u32>> {
231    ///     let time = Time::new(500, 0)?;
232    ///     let duration_to_add = Duration::from_seconds(129600); // 1.5 days
233    ///     let (time, excess) = time.wrapping_add(duration_to_add);
234    ///
235    ///     assert_eq!(time, Time::new(43700, 0)?);
236    ///     assert_eq!(excess, Duration::from_days(1));
237    /// #   Ok(())
238    /// # }
239    /// ```
240    #[must_use]
241    pub fn wrapping_add(&self, duration: Duration) -> (Time, Duration) {
242        let add_seconds = duration.as_seconds();
243        let add_nanos = (duration - Duration::from_seconds(add_seconds)).as_nanos();
244        let mut new_seconds = self.second_of_day as u64 + add_seconds;
245        let mut new_nanos = add_nanos + self.nanoseconds as u64;
246        if new_nanos >= NANOS_IN_SECOND as u64 {
247            new_nanos -= NANOS_IN_SECOND as u64;
248            new_seconds += 1;
249        }
250        let mut rollover = Duration::default();
251        if new_seconds >= SECONDS_IN_DAY as u64 {
252            let days = new_seconds / SECONDS_IN_DAY as u64;
253            new_seconds -= days * SECONDS_IN_DAY as u64;
254            rollover += Duration::from_days(days);
255        }
256        (
257            Time {
258                second_of_day: new_seconds as u32,
259                nanoseconds: new_nanos as u32,
260            },
261            rollover,
262        )
263    }
264}
265
266impl Display for Time {
267    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
268        let (h, m, s) = self.as_hms();
269        if f.alternate() {
270            let s = s as f64 + self.get_secondsfrac();
271            f.write_fmt(format_args!("{h:02}:{m:02}:{s:09.6}"))
272        } else {
273            f.write_fmt(format_args!("{h:02}:{m:02}:{s:02}"))
274        }
275    }
276}
277
278impl From<Time> for Duration {
279    fn from(value: Time) -> Self {
280        value.as_duration()
281    }
282}
283
284/// 24 Hours in a Day
285pub const HOURS_IN_DAY: u32 = 24;
286
287/// 60 Minutes in an Hour
288pub const MINUTES_IN_HOUR: u32 = 60;
289
290/// 60 Seconds in a Minute
291pub const SECONDS_IN_MINUTE: u32 = 60;
292
293/// 1440 Minutes in a Day
294pub const MINUTES_IN_DAY: u32 = 1440;
295
296/// 3600 Seconds in an Hour
297pub const SECONDS_IN_HOUR: u32 = 3600;
298
299///
300/// Generally 86400, but occasionally 86401 for leap seconds.
301pub const SECONDS_IN_DAY: u32 = 86400;
302
303///
304/// Nanoseconds in a Microsecond
305pub const NANOS_IN_MICRO: u32 = 1000;
306///
307/// Nanoseconds in a Millisecond
308pub const NANOS_IN_MILLI: u32 = 1_000_000;
309///
310/// Nanoseconds in a Second
311pub const NANOS_IN_SECOND: u32 = 1_000_000_000;
312///
313/// Nanoseconds in a Day
314pub const NANOS_IN_DAY: u64 = 86_400_000_000_000_u64;
315
316///
317/// 32 Bit Fixed Precision Time Format, storing 16 bits of Seconds, and 16 bits
318/// of Fractional Seconds.  This is the equivalent of Q16.16, and is semantically
319/// equivalent to the NTP Short Format if using the [`epoch::NTP_EPOCH`].
320///
321/// The 16-bit seconds field can resolve a little over 18 hours, and the
322/// 16-bit fractional seconds field can resolve a little over 15 microseconds.
323#[derive(Debug, Copy, Clone)]
324pub struct Time32 {
325    /// The Reference Epoch
326    epoch: Epoch,
327
328    inner: FixedU32,
329}
330
331impl Time32 {
332    ///
333    /// Returns the value of this Time32 as a Q16.16
334    #[must_use]
335    pub const fn as_u32(&self) -> u32 {
336        self.inner.raw_value()
337    }
338}
339
340///
341/// 64 Bit Fixed Precision Time Format, storing 32 bits of Seconds, and 32 bits
342/// of Fractional Seconds.  This is the equivalent of Q32.32, and is semantically
343/// equivalent to the NTP Timestamp Format if using the [`epoch::NTP_EPOCH`].
344///
345/// The 32-bit seconds field can resolve 136 years, and the 32-bit fractional field
346/// can resolve down to 232 picoseconds.
347///
348/// The raw value is 64 bits wide, if you take the middle 32
349/// bits, this is identical to a [`Time32`] - (lower 16 of `seconds`, upper 16 of
350/// `fractional_seconds`).
351#[derive(Debug, Copy, Clone)]
352pub struct Time64 {
353    /// The Reference Epoch
354    epoch: Epoch,
355    inner: FixedU64,
356}
357impl Default for Time64 {
358    fn default() -> Self {
359        Time64 {
360            epoch: UNIX_EPOCH,
361            inner: FixedU64::default(),
362        }
363    }
364}
365impl Time64 {
366    ///
367    /// Returns the value of this Time64 as a Q32.32
368    #[must_use]
369    pub const fn as_u64(&self) -> u64 {
370        self.inner.raw_value()
371    }
372
373    ///
374    /// Creates a new timestamp with a unix epoch
375    pub fn from_unix_u64(value: u64) -> Self {
376        Self {
377            epoch: UNIX_EPOCH,
378            inner: value.into(),
379        }
380    }
381
382    pub fn from_unix_raw(value: u64) -> Self {
383        Self {
384            epoch: UNIX_EPOCH,
385            inner: FixedU64::from_raw_value(value),
386        }
387    }
388}
389
390///
391/// 128 Bit Fixed Precision Time Format, storing 64 bits of Seconds, and 64 bits
392/// of Fractional Seconds.  This is the equivalent of Q64.64, and is semantically
393/// equivalent to the NTP Datestamp Format if using the [`epoch::NTP_EPOCH`].
394///
395/// The 64-bit seconds field can resolve 584 million years, and the 64-bit
396/// fractional field can resolve down to 54 zepto-seconds (5.4e-20).
397///
398/// 580 million years ago, multicellular life started.  580 million years from,
399/// now, the average temperature of the Earth will be 25C higher - 40C.
400///
401/// The raw value is 128 bits wide, if you take the middle 64 bits, this is
402/// identical to a [`Time64`] - (lower 32 of `seconds`, upper 32 of
403/// `fractional_seconds`).
404#[derive(Debug, Copy, Clone)]
405pub struct Time128 {
406    ///
407    /// Reference Epoch Date
408    epoch: Epoch,
409
410    inner: FixedU128,
411}
412
413impl Time128 {
414    ///
415    /// Returns the value of this Time128 as a Q64.64
416    #[must_use]
417    pub const fn as_u128(&self) -> u128 {
418        self.inner.raw_value()
419    }
420}
421
422macro_rules! impls {
423    ($strukt:ty, $prim:ty, $inner:ty) => {
424        impl $strukt {
425            #[must_use]
426            pub const fn new(epoch: Epoch, seconds: $prim, fractional_seconds: $prim) -> Self {
427                let inner = <$inner>::from_parts(seconds, fractional_seconds);
428                Self { epoch, inner }
429            }
430            #[must_use]
431            pub fn new_f64(epoch: Epoch, seconds: f64) -> Self {
432                let inner = <$inner>::from(seconds);
433                Self { epoch, inner }
434            }
435            ///
436            /// Returns the reference epoch of this Time
437            #[must_use]
438            pub const fn get_epoch(&self) -> Epoch {
439                self.epoch
440            }
441            #[must_use]
442            pub const fn get_seconds(&self) -> $prim {
443                self.inner.whole()
444            }
445            #[must_use]
446            pub const fn get_fractional_seconds(&self) -> $prim {
447                self.inner.fract()
448            }
449            #[must_use]
450            pub fn as_f64(&self) -> f64 {
451                self.into()
452            }
453            pub fn try_from_iso8601(val: &str) -> Result<Self, FormatError> {
454                let v = ISO8601_DATE_TIME.try_from(val)?;
455                Ok(v.into())
456            }
457
458            #[cfg(feature = "std")]
459            pub fn now() -> Self {
460                UnixTimestamp::now().into()
461            }
462            #[must_use]
463            pub fn as_only_seconds(&self) -> Self {
464                Self::new(self.epoch, self.inner.whole(), 0)
465            }
466            #[must_use]
467            pub fn as_only_fractional(&self) -> Self {
468                Self::new(self.epoch, 0, self.inner.fract())
469            }
470            #[must_use]
471            pub fn as_epoch(&self, other: Epoch) -> Self {
472                let offset = other.0 - self.epoch.0;
473                *self + offset
474            }
475        }
476        impl From<$strukt> for f64 {
477            fn from(value: $strukt) -> Self {
478                value.inner.as_f64()
479            }
480        }
481        impl From<&$strukt> for f64 {
482            fn from(value: &$strukt) -> Self {
483                value.inner.as_f64()
484            }
485        }
486        impl From<&mut $strukt> for f64 {
487            fn from(value: &mut $strukt) -> Self {
488                value.inner.as_f64()
489            }
490        }
491        impl<T> From<Timestamp<T>> for $strukt {
492            fn from(value: Timestamp<T>) -> Self {
493                let val = value.get_offset().as_seconds_f64();
494
495                <$strukt>::new_f64(value.get_epoch(), val)
496            }
497        }
498        impl<T> From<&Timestamp<T>> for $strukt {
499            fn from(value: &Timestamp<T>) -> Self {
500                let val = value.get_offset().as_seconds_f64();
501
502                <$strukt>::new_f64(value.get_epoch(), val)
503            }
504        }
505        impl<T> From<&mut Timestamp<T>> for $strukt {
506            fn from(value: &mut Timestamp<T>) -> Self {
507                let val = value.get_offset().as_seconds_f64();
508
509                <$strukt>::new_f64(value.get_epoch(), val)
510            }
511        }
512        impl<T> From<$strukt> for Timestamp<T> {
513            fn from(value: $strukt) -> Self {
514                let dur = Duration::new(value.as_f64(), DurationUnit::Second);
515                Timestamp::<T>::new(value.get_epoch(), dur)
516            }
517        }
518        impl<T> From<&$strukt> for Timestamp<T> {
519            fn from(value: &$strukt) -> Self {
520                let dur = Duration::new(value.as_f64(), DurationUnit::Second);
521                Timestamp::<T>::new(value.get_epoch(), dur)
522            }
523        }
524        impl<T> From<&mut $strukt> for Timestamp<T> {
525            fn from(value: &mut $strukt) -> Self {
526                let dur = Duration::new(value.as_f64(), DurationUnit::Second);
527                Timestamp::<T>::new(value.get_epoch(), dur)
528            }
529        }
530        impl From<UTCDateTime> for $strukt {
531            fn from(value: UTCDateTime) -> Self {
532                let ts = UnixTimestamp::from(value);
533                <$strukt>::from(ts)
534            }
535        }
536        impl From<&UTCDateTime> for $strukt {
537            fn from(value: &UTCDateTime) -> Self {
538                let ts = UnixTimestamp::from(value);
539                <$strukt>::from(ts)
540            }
541        }
542        impl From<&mut UTCDateTime> for $strukt {
543            fn from(value: &mut UTCDateTime) -> Self {
544                let ts = UnixTimestamp::from(*value);
545                <$strukt>::from(ts)
546            }
547        }
548        impl core::ops::Deref for $strukt {
549            type Target = $inner;
550
551            fn deref(&self) -> &Self::Target {
552                &self.inner
553            }
554        }
555        impl core::ops::Sub for $strukt {
556            type Output = Self;
557
558            fn sub(self, rhs: Self) -> Self::Output {
559                //TODO: verify epochs.
560                let v = self.inner - rhs.inner;
561                Self {
562                    epoch: self.epoch,
563                    inner: v,
564                }
565            }
566        }
567        impl core::ops::Sub for &$strukt {
568            type Output = $strukt;
569
570            fn sub(self, rhs: Self) -> Self::Output {
571                //TODO: verify epochs.
572                let v = self.inner - rhs.inner;
573                Self::Output {
574                    epoch: self.epoch,
575                    inner: v,
576                }
577            }
578        }
579        impl core::ops::Add<Duration> for $strukt {
580            type Output = Self;
581
582            fn add(self, rhs: Duration) -> Self::Output {
583                let v = self.inner + rhs.as_seconds_f64();
584                Self {
585                    epoch: self.epoch,
586                    inner: v,
587                }
588            }
589        }
590        impl core::ops::AddAssign<Duration> for $strukt {
591            fn add_assign(&mut self, rhs: Duration) {
592                self.inner += rhs.as_seconds_f64();
593            }
594        }
595        impl core::ops::AddAssign<Duration> for &mut $strukt {
596            fn add_assign(&mut self, rhs: Duration) {
597                self.inner += rhs.as_seconds_f64();
598            }
599        }
600        impl PartialEq for $strukt {
601            fn eq(&self, other: &Self) -> bool {
602                other.as_epoch(self.epoch).inner == self.inner
603            }
604        }
605        impl Eq for $strukt {}
606        impl PartialOrd for $strukt {
607            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
608                Some(self.cmp(&other))
609            }
610        }
611        impl Ord for $strukt {
612            fn cmp(&self, other: &Self) -> Ordering {
613                other.as_epoch(self.epoch).inner.cmp(&self.inner)
614            }
615        }
616        impl Hash for $strukt {
617            fn hash<H: Hasher>(&self, state: &mut H) {
618                self.epoch.hash(state);
619                self.inner.hash(state);
620            }
621        }
622    };
623}
624impls!(Time32, u16, FixedU32);
625impls!(Time64, u32, FixedU64);
626impls!(Time128, u64, FixedU128);