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}