ixdtf/
records.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! The records that `ixdtf`'s contain the resulting values of parsing.
6
7use core::num::NonZeroU8;
8
9use crate::core::EncodingType;
10
11/// An `IxdtfParseRecord` is an intermediary record returned by `IxdtfParser`.
12#[non_exhaustive]
13#[derive(Default, Debug, PartialEq)]
14pub struct IxdtfParseRecord<'a, T: EncodingType> {
15    /// Parsed `DateRecord`
16    pub date: Option<DateRecord>,
17    /// Parsed `TimeRecord`
18    pub time: Option<TimeRecord>,
19    /// Parsed UtcOffset
20    pub offset: Option<UtcOffsetRecordOrZ>,
21    /// Parsed `TimeZone` annotation with critical flag and data (UTCOffset | IANA name)
22    pub tz: Option<TimeZoneAnnotation<'a, T>>,
23    /// The parsed calendar value.
24    pub calendar: Option<&'a [T::CodeUnit]>,
25}
26
27#[non_exhaustive]
28#[derive(Debug, Clone, PartialEq)]
29/// A record of an annotation.
30pub struct Annotation<'a, T: EncodingType> {
31    /// Whether this annotation is flagged as critical
32    pub critical: bool,
33    /// The parsed key value of the annotation
34    pub key: &'a [T::CodeUnit],
35    /// The parsed value of the annotation
36    pub value: &'a [T::CodeUnit],
37}
38
39#[allow(clippy::exhaustive_structs)] // DateRecord only allows for a year, month, and day value.
40#[derive(Default, Debug, Clone, Copy, PartialEq)]
41/// The record of a parsed date.
42pub struct DateRecord {
43    /// Date Year
44    pub year: i32,
45    /// Date Month
46    pub month: u8,
47    /// Date Day
48    pub day: u8,
49}
50
51/// Parsed Time info
52#[allow(clippy::exhaustive_structs)] // TimeRecord only allows for a hour, minute, second, and sub-second value.
53#[derive(Debug, Default, Clone, Copy, PartialEq)]
54pub struct TimeRecord {
55    /// An hour
56    pub hour: u8,
57    /// A minute value
58    pub minute: u8,
59    /// A second value.
60    pub second: u8,
61    /// A nanosecond value representing all sub-second components.
62    pub fraction: Option<Fraction>,
63}
64
65/// A `TimeZoneAnnotation` that represents a parsed `TimeZoneRecord` and its critical flag.
66#[non_exhaustive]
67#[derive(Debug, Clone, PartialEq)]
68pub struct TimeZoneAnnotation<'a, T: EncodingType> {
69    /// Critical flag for the `TimeZoneAnnotation`.
70    pub critical: bool,
71    /// The parsed `TimeZoneRecord` for the annotation.
72    pub tz: TimeZoneRecord<'a, T>,
73}
74
75/// Parsed `TimeZone` data, which can be either a UTC Offset value or IANA Time Zone Name value.
76#[non_exhaustive]
77#[derive(Debug, Clone, PartialEq)]
78pub enum TimeZoneRecord<'a, T: EncodingType> {
79    /// TimeZoneIANAName
80    Name(&'a [T::CodeUnit]),
81    /// TimeZoneOffset
82    Offset(MinutePrecisionOffset),
83}
84
85/// The parsed sign value, representing whether its struct is positive or negative.
86#[repr(i8)]
87#[allow(clippy::exhaustive_enums)] // Sign can only be positive or negative.
88#[derive(Debug, Clone, Copy, PartialEq)]
89pub enum Sign {
90    /// A negative value sign, representable as either -1 or false.
91    Negative = -1,
92    /// A positive value sign, representable as either 1 or true.
93    Positive = 1,
94}
95
96impl From<bool> for Sign {
97    fn from(value: bool) -> Self {
98        match value {
99            true => Self::Positive,
100            false => Self::Negative,
101        }
102    }
103}
104
105/// A `UtcOffsetRecord` that is either a minute precision or
106/// full precision UTC offset.
107#[non_exhaustive]
108#[derive(Debug, Clone, Copy, PartialEq)]
109pub enum UtcOffsetRecord {
110    // A UTC offset that is only precise to the minute
111    MinutePrecision(MinutePrecisionOffset),
112    // A UTC offset with full fractional second precision
113    FullPrecisionOffset(FullPrecisionOffset),
114}
115
116impl UtcOffsetRecord {
117    /// Returns whether the UTC offset is a minute precision offset.
118    pub fn is_minute_precision(&self) -> bool {
119        matches!(self, Self::MinutePrecision(_))
120    }
121
122    /// Returrns a zerod UTC Offset in minute precision
123    pub fn zero() -> Self {
124        Self::MinutePrecision(MinutePrecisionOffset::zero())
125    }
126
127    /// Returns the `Sign` of this UTC offset.
128    pub fn sign(&self) -> Sign {
129        match self {
130            Self::MinutePrecision(offset) => offset.sign,
131            Self::FullPrecisionOffset(offset) => offset.minute_precision_offset.sign,
132        }
133    }
134
135    /// Returns the hour value of this UTC offset.
136    pub fn hour(&self) -> u8 {
137        match self {
138            Self::MinutePrecision(offset) => offset.hour,
139            Self::FullPrecisionOffset(offset) => offset.minute_precision_offset.hour,
140        }
141    }
142
143    /// Returns the minute value of this UTC offset.
144    pub fn minute(&self) -> u8 {
145        match self {
146            Self::MinutePrecision(offset) => offset.minute,
147            Self::FullPrecisionOffset(offset) => offset.minute_precision_offset.minute,
148        }
149    }
150
151    /// Returns the second value of this UTC offset if it is a full precision offset.
152    pub fn second(&self) -> Option<u8> {
153        match self {
154            Self::MinutePrecision(_) => None,
155            Self::FullPrecisionOffset(offset) => Some(offset.second),
156        }
157    }
158
159    /// Returns the fraction value of this UTC offset if it is a full precision offset.
160    pub fn fraction(&self) -> Option<Fraction> {
161        match self {
162            Self::MinutePrecision(_) => None,
163            Self::FullPrecisionOffset(offset) => offset.fraction,
164        }
165    }
166}
167
168/// A minute preicision UTC offset
169#[derive(Debug, Clone, Copy, PartialEq)]
170#[allow(clippy::exhaustive_structs)] // Minute precision offset can only be a sign, hour and minute field
171pub struct MinutePrecisionOffset {
172    /// The `Sign` value of the `UtcOffsetRecord`.
173    pub sign: Sign,
174    /// The hour value of the `UtcOffsetRecord`.
175    pub hour: u8,
176    /// The minute value of the `UtcOffsetRecord`.
177    pub minute: u8,
178}
179
180/// A full precision UTC offset represented by a `MinutePrecisionOffset`
181/// with seconds and an optional fractional seconds
182#[derive(Debug, Clone, Copy, PartialEq)]
183#[allow(clippy::exhaustive_structs)] // Full precision offset is only these five component fields
184pub struct FullPrecisionOffset {
185    /// The minute precision offset of a `FullPrecisionOffset`.
186    pub minute_precision_offset: MinutePrecisionOffset,
187    /// The second value of a `FullPrecisionOffset`.
188    pub second: u8,
189    /// Any nanosecond value of a `FullPrecisionOffset`.
190    pub fraction: Option<Fraction>,
191}
192
193impl MinutePrecisionOffset {
194    /// +0000
195    pub const fn zero() -> Self {
196        Self {
197            sign: Sign::Positive,
198            hour: 0,
199            minute: 0,
200        }
201    }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq)]
205#[allow(clippy::exhaustive_enums)] // explicitly A or B
206pub enum UtcOffsetRecordOrZ {
207    Offset(UtcOffsetRecord),
208    Z,
209}
210
211impl UtcOffsetRecordOrZ {
212    /// Resolves to a [`UtcOffsetRecord`] according to RFC9557: "Z" == "-00:00"
213    pub fn resolve_rfc_9557(self) -> UtcOffsetRecord {
214        match self {
215            UtcOffsetRecordOrZ::Offset(o) => o,
216            UtcOffsetRecordOrZ::Z => UtcOffsetRecord::MinutePrecision(MinutePrecisionOffset {
217                sign: Sign::Negative,
218                hour: 0,
219                minute: 0,
220            }),
221        }
222    }
223}
224
225/// The resulting record of parsing a `Duration` string.
226#[allow(clippy::exhaustive_structs)]
227// A duration can only be a Sign, a DateDuration part, and a TimeDuration part that users need to match on.
228#[cfg(feature = "duration")]
229#[derive(Debug, Clone, Copy, PartialEq)]
230pub struct DurationParseRecord {
231    /// Duration Sign
232    pub sign: Sign,
233    /// The parsed `DateDurationRecord` if present.
234    pub date: Option<DateDurationRecord>,
235    /// The parsed `TimeDurationRecord` if present.
236    pub time: Option<TimeDurationRecord>,
237}
238
239/// A `DateDurationRecord` represents the result of parsing the date component of a Duration string.
240#[allow(clippy::exhaustive_structs)]
241// A `DateDurationRecord` by spec can only be made up of years, months, weeks, and days parts that users need to match on.
242#[cfg(feature = "duration")]
243#[derive(Default, Debug, Clone, Copy, PartialEq)]
244pub struct DateDurationRecord {
245    /// Years value.
246    pub years: u32,
247    /// Months value.
248    pub months: u32,
249    /// Weeks value.
250    pub weeks: u32,
251    /// Days value.
252    pub days: u64,
253}
254
255/// A `TimeDurationRecord` represents the result of parsing the time component of a Duration string.
256#[allow(clippy::exhaustive_enums)]
257// A `TimeDurationRecord` by spec can only be made up of the valid parts up to a present fraction that users need to match on.
258#[cfg(feature = "duration")]
259#[derive(Debug, Clone, Copy, PartialEq)]
260pub enum TimeDurationRecord {
261    // An hours Time duration record.
262    Hours {
263        /// Hours value.
264        hours: u64,
265        /// The parsed fractional digits.
266        fraction: Option<Fraction>,
267    },
268    // A Minutes Time duration record.
269    Minutes {
270        /// Hours value.
271        hours: u64,
272        /// Minutes value.
273        minutes: u64,
274        /// The parsed fractional digits.
275        fraction: Option<Fraction>,
276    },
277    // A Seconds Time duration record.
278    Seconds {
279        /// Hours value.
280        hours: u64,
281        /// Minutes value.
282        minutes: u64,
283        /// Seconds value.
284        seconds: u64,
285        /// The parsed fractional digits.
286        fraction: Option<Fraction>,
287    },
288}
289
290/// A fraction value in nanoseconds or lower value.
291///
292/// # Precision note
293///
294/// `ixdtf` parses a fraction value to a precision of 18 digits of precision, but
295/// preserves the fraction's digit length
296#[derive(Debug, Clone, Copy, PartialEq)]
297#[allow(clippy::exhaustive_structs)] // A fraction is only a value and its digit length.
298pub struct Fraction {
299    // The count of fraction digits, i.e. the fraction's digit length.
300    pub(crate) digits: NonZeroU8,
301    // The parsed fraction value.
302    pub(crate) value: u64,
303}
304
305impl Fraction {
306    /// Returns Some(`u32`) representing the `Fraction` as it's computed
307    /// nanosecond value or `None` if the digits exceeds 9 digits.
308    ///
309    /// ```rust
310    /// use ixdtf::parsers::IxdtfParser;
311    ///
312    /// // Fraction is below 9 digits.
313    /// let fraction_str = "2025-02-17T05:41:32.12345678";
314    /// let result = IxdtfParser::from_str(fraction_str).parse().unwrap();
315    ///
316    /// let time = result.time.unwrap();
317    /// let fraction = time.fraction.unwrap();
318    ///
319    /// assert_eq!(fraction.to_nanoseconds(), Some(123456780));
320    ///
321    /// // Fraction is 10 digits.
322    /// let fraction_str = "2025-02-17T05:41:32.1234567898";
323    /// let result = IxdtfParser::from_str(fraction_str).parse().unwrap();
324    /// let time = result.time.unwrap();
325    /// let fraction = time.fraction.unwrap();
326    ///
327    /// assert_eq!(fraction.to_nanoseconds(), None);
328    /// ```
329    pub fn to_nanoseconds(&self) -> Option<u32> {
330        if self.digits.get() <= 9 {
331            Some(10u32.pow(9 - u32::from(self.digits.get())) * (self.value as u32))
332        } else {
333            None
334        }
335    }
336
337    /// Returns a `u32` representing the `Fraction` as it's computed
338    /// nanosecond value, truncating any value beyond 9 digits to
339    /// nanoseconds.
340    ///
341    /// This method will return `None` if the value exceeds a represented
342    /// range or the underlying `Fraction` is malformed.
343    ///
344    /// ```rust
345    /// use ixdtf::parsers::IxdtfParser;
346    ///
347    /// // Fraction is 13 digits.
348    /// let fraction_str = "2025-02-17T05:41:32.1234567898765";
349    /// let result = IxdtfParser::from_str(fraction_str).parse().unwrap();
350    ///
351    /// let time = result.time.unwrap();
352    /// let fraction = time.fraction.unwrap();
353    ///
354    /// assert_eq!(fraction.to_truncated_nanoseconds(), 123456789);
355    /// assert_eq!(fraction.to_nanoseconds(), None);
356    /// ```
357    pub fn to_truncated_nanoseconds(&self) -> u32 {
358        self.to_nanoseconds()
359            .unwrap_or((self.value / 10u64.pow(u32::from(self.digits.get() - 9))) as u32)
360    }
361}