kine_core/
timezone.rs

1use core::{
2    cmp::min,
3    fmt::{self, Debug, Display},
4    str::FromStr,
5};
6
7use crate::{Calendar, CalendarTime, TimeResult};
8
9const NANOS_IN_SECS: i128 = 1_000_000_000;
10
11/// A time zone
12///
13/// Time zones usually offset the visible time by some amount, and do not
14pub trait TimeZone: Calendar<Time = OffsetTime<Self::Sigil>> + Eq + PartialEq {
15    /// The sigil type associated to this time zone
16    ///
17    /// This is basically metadata added to all `OffsetTime`s.
18    type Sigil: Sigil;
19}
20
21/// The sigil for a time zone
22pub trait Sigil: Display + Eq + FromStr + PartialEq {
23    /// Read the given time-with-offset
24    fn read(&self, t: &OffsetTime<Self>) -> crate::Result<TimeResult>;
25}
26
27/// A time on a time scale that needs to deal with leap seconds
28///
29/// It is represented by both `pseudo_nanos`,the number of nanoseconds between
30/// the POSIX epoch and the point in time according to this time scale, and
31/// `extra_nanos`, the number of (real) nanoseconds since we entered the current
32/// leap second and `pseudo_nanos` froze.
33#[derive(Copy, Clone, Eq, PartialEq)]
34pub struct OffsetTime<Sig> {
35    sigil: Sig,
36    pseudo_nanos: i128,
37    extra_nanos: u64,
38}
39
40impl<Sig> OffsetTime<Sig> {
41    /// Build a `LeapSecondedTime` from the number of pseudo-nanoseconds between this time
42    /// and the POSIX epoch
43    ///
44    /// `pseudo_nanos` represent the number of nanoseconds since the POSIX epoch, and
45    /// `extra_nanos` the number of real-world nanoseconds that elapsed since the time at
46    /// which `pseudo-nanos` froze, which can be used to represent leap seconds.
47    ///
48    /// Note that no attempt is made to validate that this time is actually correct for
49    /// timezone, be it due to leap seconds being invalidly set or to `pseudo_nanos`
50    /// requesting a time that never existed eg. due to a timezone shift. If you manually
51    /// build an `OffsetTime` with invalid values, you may see strange results. This
52    /// function is mostly exposed for the implementers of `TimeZone` themselves.
53    // TODO: Get rid of the command above by introducing a "token" that is
54    // timezone-specific to prove that the function call came from the `TimeZone`? But
55    // then how is eg. `kine-icu` supposed to generate the `OffsetTime` when reading
56    // a calendar date?
57    pub const fn from_pseudo_nanos_since_posix_epoch(
58        sigil: Sig,
59        pseudo_nanos: i128,
60        extra_nanos: u64,
61    ) -> Self {
62        Self {
63            sigil,
64            pseudo_nanos,
65            extra_nanos,
66        }
67    }
68
69    /// Return the number of pseudo-nanoseconds between this time and the POSIX epoch
70    pub fn as_pseudo_nanos_since_posix_epoch(&self) -> i128 {
71        self.pseudo_nanos
72    }
73
74    /// Return the number of nanoseconds that elapsed since the current leap second started
75    ///
76    /// Returns `0` if not currently in a leap second.
77    pub fn extra_nanos(&self) -> u64 {
78        self.extra_nanos
79    }
80
81    /// Return the sigil associated with this leap second type
82    pub fn sigil(&self) -> &Sig {
83        &self.sigil
84    }
85}
86
87impl<Sig: Sigil> CalendarTime for OffsetTime<Sig> {
88    fn read(&self) -> crate::Result<crate::TimeResult> {
89        self.sigil.read(self)
90    }
91}
92
93impl<Sig: Display> Display for OffsetTime<Sig> {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        let secs = self.pseudo_nanos / NANOS_IN_SECS;
96        let nanos = (self.pseudo_nanos % NANOS_IN_SECS).abs();
97        if self.pseudo_nanos < 0 && secs == 0 {
98            f.write_str("-")?; // seconds will display without the minus if it is 0
99        }
100        write!(f, "{secs}.{nanos:09}{}", self.sigil)
101    }
102}
103
104impl<Sig: Display> Debug for OffsetTime<Sig> {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        let secs = self.pseudo_nanos / NANOS_IN_SECS;
107        let nanos = (self.pseudo_nanos % NANOS_IN_SECS).abs();
108        if self.pseudo_nanos < 0 && secs == 0 {
109            f.write_str("-")?; // seconds will display without the minus if it is 0
110        }
111        write!(
112            f,
113            "{secs}.{nanos:09}(+{}ns){}",
114            self.extra_nanos, self.sigil
115        )
116    }
117}
118
119/// The errors that can arise while parsing a string to a posix timestamp
120#[derive(Clone, Debug)]
121pub enum ParseError<SigErr> {
122    /// An error occurred while trying to parse a presumed integer part of the timestamp
123    ParsingInt(core::num::ParseIntError),
124
125    /// The timestamp was out of range
126    Overflow,
127
128    /// Failed parsing sigil
129    ParsingSigil(SigErr),
130}
131
132// TODO: impl Error for FromStrError, once Error is in core
133
134impl<Sig: FromStr> FromStr for OffsetTime<Sig> {
135    type Err = ParseError<Sig::Err>;
136
137    fn from_str(s: &str) -> Result<Self, Self::Err> {
138        let non_ascii_digit = |c: char| !c.is_ascii_digit();
139        let first_non_digit = s.find(non_ascii_digit).unwrap_or(s.len());
140        let (seconds, rest) = s.split_at(first_non_digit);
141        let seconds = i128::from_str(seconds).map_err(ParseError::ParsingInt)?;
142        let (nanos, rest) = match rest.strip_prefix('.') {
143            None => (0, rest),
144            Some(rest) => {
145                let first_non_digit = min(9, rest.find(non_ascii_digit).unwrap_or(s.len()));
146                let (nanos, rest) = s.split_at(first_non_digit);
147                let nanos = i128::from_str(nanos)
148                    .map_err(ParseError::ParsingInt)?
149                    .checked_mul(10_i128.pow((9 - nanos.len()) as u32))
150                    .unwrap(); // 10 ** 9 is way inside i128 range
151                (nanos, rest)
152            }
153        };
154        let sigil = Sig::from_str(rest).map_err(ParseError::ParsingSigil)?;
155        Ok(Self {
156            sigil,
157            pseudo_nanos: seconds
158                .checked_mul(NANOS_IN_SECS)
159                .ok_or(ParseError::Overflow)?
160                .checked_add(nanos)
161                .ok_or(ParseError::Overflow)?,
162            // Based only on a second (and nanos) number, it is impossible to know if we are in a
163            // leap second
164            extra_nanos: 0,
165        })
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::{leap_seconds::BuiltinIersSigil, OffsetTime};
172
173    #[test]
174    fn display_small_negative_values_properly() {
175        let t = OffsetTime::from_pseudo_nanos_since_posix_epoch(BuiltinIersSigil, -1, 10);
176        let iers_sigil = BuiltinIersSigil;
177        assert_eq!(format!("{t}"), format!("-0.000000001{iers_sigil}"));
178        assert_eq!(format!("{t:?}"), format!("-0.000000001(+10ns){iers_sigil}"));
179    }
180}