Skip to main content

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