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