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}