Skip to content

Commit 2deb42f

Browse files
authored
Add TimeZoneParser to ixdtf and adjustments to UtcOffsetRecord (#6283)
This PR contains a few adjustments to `ixdtf` along with related changes to `icu_time` for the adjustments The main changes are: 1. A `TimeZoneParser` was added to `ixdtf` to expose the time zone parsing logic. Currently some small parsers were implemented in `temporal_rs`, which leads to differing parsing behavior, so the main goal is to centralize the parsing logic. 2. Change `UtcOffsetRecord` to an enum, and add `MinutePrecisionOffset` and `FullPrecisionOffset`. Currently, its technically impossible to tell the difference between minute precision offsets and full precision offsets with the current representation. This is primarily meant to address that issue. 3. A little more clean up of parsing logic on offsets for 1 and 2, plus some additional unit tests. 4. Adds a `timezone` feature flag to `ixdtf` to gate the `TimeZoneParser`.
1 parent 3cc9cbc commit 2deb42f

File tree

12 files changed

+311
-107
lines changed

12 files changed

+311
-107
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ databake = { version = "0.2.0", path = "utils/databake", default-features = fals
191191
databake-derive = { version = "0.2.0", path = "utils/databake/derive", default-features = false }
192192
deduplicating_array = { version = "0.1.6", path = "utils/deduplicating_array", default-features = false }
193193
fixed_decimal = { version = "0.7.0", path = "utils/fixed_decimal", default-features = false }
194-
ixdtf = { version = "0.4.0", path = "utils/ixdtf", default-features = false }
194+
ixdtf = { version = "0.5.0-dev", path = "utils/ixdtf", default-features = false }
195195
litemap = { version = "0.7.3", path = "utils/litemap", default-features = false }
196196
potential_utf = { version = "0.1.1", path = "utils/potential_utf", default-features = false }
197197
tzif = { version = "0.4.0", path = "utils/tzif", default-features = false }

components/time/src/ixdtf.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ impl From<icu_calendar::ParseError> for ParseError {
116116

117117
impl UtcOffset {
118118
fn try_from_utc_offset_record(record: UtcOffsetRecord) -> Result<Self, ParseError> {
119-
let hour_seconds = i32::from(record.hour) * 3600;
120-
let minute_seconds = i32::from(record.minute) * 60;
119+
let hour_seconds = i32::from(record.hour()) * 3600;
120+
let minute_seconds = i32::from(record.minute()) * 60;
121121
Self::try_from_seconds(
122-
i32::from(record.sign as i8)
123-
* (hour_seconds + minute_seconds + i32::from(record.second)),
122+
i32::from(record.sign() as i8)
123+
* (hour_seconds + minute_seconds + i32::from(record.second().unwrap_or(0))),
124124
)
125125
.map_err(Into::into)
126126
}
@@ -164,7 +164,7 @@ impl<'a> Intermediate<'a> {
164164
..
165165
}),
166166
..
167-
} => (Some(*offset), false, None),
167+
} => (Some(UtcOffsetRecord::MinutePrecision(*offset)), false, None),
168168
// -0800[-0800]
169169
IxdtfParseRecord {
170170
offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
@@ -175,7 +175,8 @@ impl<'a> Intermediate<'a> {
175175
}),
176176
..
177177
} => {
178-
if offset != offset1 {
178+
let annotation_offset = UtcOffsetRecord::MinutePrecision(*offset1);
179+
if offset != &annotation_offset {
179180
return Err(ParseError::InconsistentTimeUtcOffsets);
180181
}
181182
(Some(*offset), false, None)
@@ -199,7 +200,7 @@ impl<'a> Intermediate<'a> {
199200
..
200201
}),
201202
..
202-
} => (Some(*offset), true, None),
203+
} => (Some(UtcOffsetRecord::MinutePrecision(*offset)), true, None),
203204
// Z[America/Los_Angeles]
204205
IxdtfParseRecord {
205206
offset: Some(UtcOffsetRecordOrZ::Z),

utils/ixdtf/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[package]
66
name = "ixdtf"
77
description = "Parser for Internet eXtended DateTime Format"
8-
version = "0.4.0"
8+
version = "0.5.0-dev"
99

1010
authors.workspace = true
1111
categories.workspace = true

utils/ixdtf/README.md

+16-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/ixdtf/src/lib.rs

+16-16
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
//! assert_eq!(date.day, 2);
3939
//! assert_eq!(time.hour, 8);
4040
//! assert_eq!(time.minute, 48);
41-
//! assert_eq!(offset.sign, Sign::Negative);
42-
//! assert_eq!(offset.hour, 5);
43-
//! assert_eq!(offset.minute, 0);
44-
//! assert_eq!(offset.second, 0);
45-
//! assert_eq!(offset.fraction, None);
41+
//! assert_eq!(offset.sign(), Sign::Negative);
42+
//! assert_eq!(offset.hour(), 5);
43+
//! assert_eq!(offset.minute(), 0);
44+
//! assert_eq!(offset.second(), None);
45+
//! assert_eq!(offset.fraction(), None);
4646
//! assert!(!tz_annotation.critical);
4747
//! assert_eq!(
4848
//! tz_annotation.tz,
@@ -92,11 +92,11 @@
9292
//! assert_eq!(date.day, 2);
9393
//! assert_eq!(time.hour, 8);
9494
//! assert_eq!(time.minute, 48);
95-
//! assert_eq!(offset.sign, Sign::Negative);
96-
//! assert_eq!(offset.hour, 0);
97-
//! assert_eq!(offset.minute, 0);
98-
//! assert_eq!(offset.second, 0);
99-
//! assert_eq!(offset.fraction, None);
95+
//! assert_eq!(offset.sign(), Sign::Negative);
96+
//! assert_eq!(offset.hour(), 0);
97+
//! assert_eq!(offset.minute(), 0);
98+
//! assert_eq!(offset.second(), None);
99+
//! assert_eq!(offset.fraction(), None);
100100
//! assert!(!tz_annotation.critical);
101101
//! assert_eq!(
102102
//! tz_annotation.tz,
@@ -148,11 +148,11 @@
148148
//!
149149
//! // The offset is `Z`/`-00:00`, so the application can use the rules of
150150
//! // "America/New_York" to calculate the time for IXDTF string.
151-
//! assert_eq!(offset.sign, Sign::Negative);
152-
//! assert_eq!(offset.hour, 0);
153-
//! assert_eq!(offset.minute, 0);
154-
//! assert_eq!(offset.second, 0);
155-
//! assert_eq!(offset.fraction, None);
151+
//! assert_eq!(offset.sign(), Sign::Negative);
152+
//! assert_eq!(offset.hour(), 0);
153+
//! assert_eq!(offset.minute(), 0);
154+
//! assert_eq!(offset.second(), None);
155+
//! assert_eq!(offset.fraction(), None);
156156
//! assert!(tz_annotation.critical);
157157
//! assert_eq!(
158158
//! tz_annotation.tz,
@@ -302,7 +302,7 @@
302302
//! // resolved by the user.
303303
//! assert!(tz_annotation.critical);
304304
//! assert_eq!(tz_annotation.tz, TimeZoneRecord::Name("America/New_York".as_bytes()));
305-
//! assert_eq!(offset.hour, 1);
305+
//! assert_eq!(offset.hour(), 1);
306306
//! ```
307307
//!
308308
//! #### Implementing Annotation Handlers

utils/ixdtf/src/parsers/datetime.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ fn parse_date_time(cursor: &mut Cursor) -> ParserResult<DateTimeRecord> {
159159
let time = parse_time_record(cursor)?;
160160

161161
let time_zone = if cursor.check_or(false, |ch| is_ascii_sign(ch) || is_utc_designator(ch)) {
162-
Some(timezone::parse_date_time_utc(cursor)?)
162+
Some(timezone::parse_date_time_utc_offset(cursor)?)
163163
} else {
164164
None
165165
};

utils/ixdtf/src/parsers/mod.rs

+99-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{ParseError, ParserResult};
88

99
#[cfg(feature = "duration")]
1010
use records::DurationParseRecord;
11-
use records::IxdtfParseRecord;
11+
use records::{IxdtfParseRecord, UtcOffsetRecord};
1212

1313
use self::records::Annotation;
1414

@@ -61,9 +61,11 @@ macro_rules! assert_syntax {
6161
/// assert_eq!(date.day, 2);
6262
/// assert_eq!(time.hour, 8);
6363
/// assert_eq!(time.minute, 48);
64-
/// assert_eq!(offset.sign, Sign::Negative);
65-
/// assert_eq!(offset.hour, 5);
66-
/// assert_eq!(offset.minute, 0);
64+
/// assert_eq!(offset.sign(), Sign::Negative);
65+
/// assert_eq!(offset.hour(), 5);
66+
/// assert_eq!(offset.minute(), 0);
67+
/// assert_eq!(offset.second(), None);
68+
/// assert_eq!(offset.fraction(), None);
6769
/// assert!(!tz_annotation.critical);
6870
/// assert_eq!(
6971
/// tz_annotation.tz,
@@ -202,9 +204,9 @@ impl<'a> IxdtfParser<'a> {
202204
/// assert_eq!(time.hour, 12);
203205
/// assert_eq!(time.minute, 1);
204206
/// assert_eq!(time.second, 4);
205-
/// assert_eq!(offset.sign, Sign::Negative);
206-
/// assert_eq!(offset.hour, 5);
207-
/// assert_eq!(offset.minute, 0);
207+
/// assert_eq!(offset.sign(), Sign::Negative);
208+
/// assert_eq!(offset.hour(), 5);
209+
/// assert_eq!(offset.minute(), 0);
208210
/// assert!(!tz_annotation.critical);
209211
/// assert_eq!(
210212
/// tz_annotation.tz,
@@ -228,6 +230,95 @@ impl<'a> IxdtfParser<'a> {
228230
}
229231
}
230232

233+
/// A parser for time zone offset and IANA identifier strings.
234+
///
235+
/// ✨ *Enabled with the `timezone` Cargo feature.*
236+
///
237+
#[derive(Debug)]
238+
pub struct TimeZoneParser<'a> {
239+
cursor: Cursor<'a>,
240+
}
241+
242+
impl<'a> TimeZoneParser<'a> {
243+
/// Creates a new `TimeZoneParser` from a slice of utf-8 bytes.
244+
#[inline]
245+
#[must_use]
246+
pub fn from_utf8(source: &'a [u8]) -> Self {
247+
Self {
248+
cursor: Cursor::new(source),
249+
}
250+
}
251+
252+
/// Creates a new `TimeZoneParser` from a source `&str`.
253+
#[inline]
254+
#[must_use]
255+
#[allow(clippy::should_implement_trait)]
256+
pub fn from_str(source: &'a str) -> Self {
257+
Self::from_utf8(source.as_bytes())
258+
}
259+
260+
/// Parse a UTC offset from the provided source.
261+
///
262+
/// This method can parse both a minute precision and full
263+
/// precision offset.
264+
///
265+
/// ## Minute precision offset example
266+
///
267+
/// ```rust
268+
/// use ixdtf::parsers::{TimeZoneParser, records::Sign};
269+
///
270+
/// let offset_src = "-05:00";
271+
/// let parse_result = TimeZoneParser::from_str(offset_src).parse_offset().unwrap();
272+
/// assert_eq!(parse_result.sign(), Sign::Negative);
273+
/// assert_eq!(parse_result.hour(), 5);
274+
/// assert_eq!(parse_result.minute(), 0);
275+
/// assert_eq!(parse_result.second(), None);
276+
/// assert_eq!(parse_result.fraction(), None);
277+
/// ```
278+
///
279+
/// ## Full precision offset example
280+
///
281+
/// ```rust
282+
/// use ixdtf::parsers::{TimeZoneParser, records::Sign};
283+
///
284+
/// let offset_src = "-05:00:30.123456789";
285+
/// let parse_result = TimeZoneParser::from_str(offset_src).parse_offset().unwrap();
286+
/// assert_eq!(parse_result.sign(), Sign::Negative);
287+
/// assert_eq!(parse_result.hour(), 5);
288+
/// assert_eq!(parse_result.minute(), 0);
289+
/// assert_eq!(parse_result.second(), Some(30));
290+
/// let fraction = parse_result.fraction().unwrap();
291+
/// assert_eq!(fraction.to_nanoseconds(), Some(123456789));
292+
/// ```
293+
#[inline]
294+
pub fn parse_offset(&mut self) -> ParserResult<UtcOffsetRecord> {
295+
let result = timezone::parse_utc_offset(&mut self.cursor)?;
296+
self.cursor.close()?;
297+
Ok(result)
298+
}
299+
300+
/// Parse an IANA identifier name.
301+
///
302+
///
303+
/// ```rust
304+
/// use ixdtf::parsers::{TimeZoneParser, records::Sign};
305+
///
306+
/// let iana_identifier = "America/Chicago";
307+
/// let parse_result = TimeZoneParser::from_str(iana_identifier).parse_iana_identifier().unwrap();
308+
/// assert_eq!(parse_result, iana_identifier.as_bytes());
309+
///
310+
/// let iana_identifier = "Europe/Berlin";
311+
/// let parse_result = TimeZoneParser::from_str(iana_identifier).parse_iana_identifier().unwrap();
312+
/// assert_eq!(parse_result, iana_identifier.as_bytes());
313+
/// ```
314+
#[inline]
315+
pub fn parse_iana_identifier(&mut self) -> ParserResult<&'a [u8]> {
316+
let result = timezone::parse_tz_iana_name(&mut self.cursor)?;
317+
self.cursor.close()?;
318+
Ok(result)
319+
}
320+
}
321+
231322
/// A parser for ISO8601 Duration strings.
232323
///
233324
/// ✨ *Enabled with the `duration` Cargo feature.*
@@ -406,7 +497,7 @@ impl<'a> Cursor<'a> {
406497
/// # Errors
407498
/// - Returns an AbruptEnd error if cursor ends.
408499
fn next_digit(&mut self) -> ParserResult<Option<u8>> {
409-
let ascii_char = self.next_or(ParseError::InvalidEnd)?;
500+
let ascii_char = self.next_or(ParseError::AbruptEnd { location: "digit" })?;
410501
if ascii_char.is_ascii_digit() {
411502
Ok(Some(ascii_char - 48))
412503
} else {

0 commit comments

Comments
 (0)