Skip to content

Commit 2356542

Browse files
authored
Fix incorrect decimal digits bug (#679)
This closes #677. We are not correctly throwing on invalid time records when the decimal digits exceed 9 digits. This PR addresses that problem and adds unit tests.
1 parent efc1467 commit 2356542

File tree

4 files changed

+116
-2
lines changed

4 files changed

+116
-2
lines changed

src/builtins/core/instant.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,13 @@ impl Instant {
303303
UtcOffsetRecordOrZ::Offset(offset) => {
304304
let ns = offset
305305
.fraction()
306-
.and_then(|x| x.to_nanoseconds())
306+
.map(|x| {
307+
x.to_nanoseconds().ok_or(
308+
TemporalError::range()
309+
.with_enum(ErrorMessage::FractionalTimeMoreThanNineDigits),
310+
)
311+
})
312+
.transpose()?
307313
.unwrap_or(0);
308314
(offset.hour() as i64 * NANOSECONDS_PER_HOUR
309315
+ i64::from(offset.minute()) * NANOSECONDS_PER_MINUTE
@@ -317,7 +323,13 @@ impl Instant {
317323
let time_nanoseconds = ixdtf_record
318324
.time
319325
.fraction
320-
.and_then(|x| x.to_nanoseconds())
326+
.map(|x| {
327+
x.to_nanoseconds().ok_or(
328+
TemporalError::range()
329+
.with_enum(ErrorMessage::FractionalTimeMoreThanNineDigits),
330+
)
331+
})
332+
.transpose()?
321333
.unwrap_or(0);
322334
let (millisecond, rem) = time_nanoseconds.div_rem_euclid(&1_000_000);
323335
let (microsecond, nanosecond) = rem.div_rem_euclid(&1_000);

src/builtins/core/zoned_date_time/tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,34 @@ fn test_same_date_reverse_wallclock() {
11751175
})
11761176
}
11771177

1178+
#[test]
1179+
fn test_invalid_fractional_offset_digits() {
1180+
test_all_providers!(provider: {
1181+
let test = "2020-01-01T00:00:00.123456789+02:30:00.1234567890[UTC]";
1182+
let result = ZonedDateTime::from_utf8_with_provider(
1183+
test.as_bytes(),
1184+
crate::options::Disambiguation::Compatible,
1185+
crate::options::OffsetDisambiguation::Use,
1186+
&provider,
1187+
);
1188+
assert!(result.is_err(), "ZonedDateTime should be invalid");
1189+
})
1190+
}
1191+
1192+
#[test]
1193+
fn test_valid_fractional_offset_digits() {
1194+
test_all_providers!(provider: {
1195+
let test = "2020-01-01T00:00:00.123456789+02:30:00.123456789[UTC]";
1196+
let result = ZonedDateTime::from_utf8_with_provider(
1197+
test.as_bytes(),
1198+
crate::options::Disambiguation::Compatible,
1199+
crate::options::OffsetDisambiguation::Use,
1200+
&provider,
1201+
);
1202+
assert!(result.is_ok(), "ZonedDateTime should be valid");
1203+
})
1204+
}
1205+
11781206
#[test]
11791207
fn test_relativeto_back_transition() {
11801208
// intl402/Temporal/Duration/prototype/round/relativeto-dst-back-transition

src/builtins/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,46 @@ pub static TZ_PROVIDER: LazyLock<CompiledTzdbProvider> =
1919

2020
#[cfg(all(test, feature = "compiled_data"))]
2121
pub(crate) static FS_TZ_PROVIDER: LazyLock<FsTzdbProvider> = LazyLock::new(FsTzdbProvider::default);
22+
23+
#[cfg(test)]
24+
mod tests {
25+
use super::{Instant, PlainDate, PlainDateTime};
26+
#[test]
27+
fn builtins_from_str_10_digit_fractions() {
28+
// Failure case with 10 digits
29+
let test = "2020-01-01T00:00:00.1234567890Z";
30+
let result = Instant::from_utf8(test.as_bytes());
31+
assert!(result.is_err(), "Instant fraction should be invalid");
32+
let result = PlainDate::from_utf8(test.as_bytes());
33+
assert!(result.is_err(), "PlainDate fraction should be invalid");
34+
let result = PlainDateTime::from_utf8(test.as_bytes());
35+
assert!(result.is_err(), "PlainDateTime fraction should be invalid");
36+
}
37+
38+
#[test]
39+
fn instant_based_10_digit_offset() {
40+
let test = "2020-01-01T00:00:00.123456789+02:30:00.1234567890[UTC]";
41+
let result = Instant::from_utf8(test.as_bytes());
42+
assert!(result.is_err(), "Instant should be invalid");
43+
}
44+
45+
#[test]
46+
fn instant_based_9_digit_offset() {
47+
let test = "2020-01-01T00:00:00.123456789+02:30:00.123456789[UTC]";
48+
let result = Instant::from_utf8(test.as_bytes());
49+
assert!(result.is_ok(), "Instant should be valid");
50+
}
51+
52+
#[test]
53+
fn builtin_from_str_9_digit_fractions() {
54+
// Success case with 9 digits
55+
let test = "2020-01-01T00:00:00.123456789Z";
56+
let result = Instant::from_utf8(test.as_bytes());
57+
assert!(result.is_ok(), "Instant fraction should be valid");
58+
let test = "2020-01-01T00:00:00.123456789";
59+
let result = PlainDate::from_utf8(test.as_bytes());
60+
assert!(result.is_ok(), "PlainDate fraction should be valid");
61+
let result = PlainDateTime::from_utf8(test.as_bytes());
62+
assert!(result.is_ok(), "PlainDateTime fraction should be valid");
63+
}
64+
}

src/parsed_intermediates.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ impl ParsedDate {
4848
// Assertion: PlainDate must exist on a DateTime parse.
4949
let record = parse_record.date.temporal_unwrap()?;
5050

51+
// TODO: Potentially, remove this check in favor of type guarantee of a
52+
// Temporal parser.
53+
// NOTE: The check here is to confirm that the time component is
54+
// valid.
55+
let _time = parse_record
56+
.time
57+
.map(IsoTime::from_time_record)
58+
.transpose()?
59+
.unwrap_or_default();
60+
5161
Ok(Self { record, calendar })
5262
}
5363
/// Converts a UTF-8 encoded YearMonth string into a `ParsedDate`.
@@ -59,8 +69,19 @@ impl ParsedDate {
5969
// Assertion: PlainDate must exist on a DateTime parse.
6070
let record = parse_record.date.temporal_unwrap()?;
6171

72+
// TODO: Potentially, remove this check in favor of type guarantee of a
73+
// Temporal parser.
74+
// NOTE: The check here is to confirm that the time component is
75+
// valid.
76+
let _time = parse_record
77+
.time
78+
.map(IsoTime::from_time_record)
79+
.transpose()?
80+
.unwrap_or_default();
81+
6282
Ok(Self { record, calendar })
6383
}
84+
6485
/// Converts a UTF-8 encoded MonthDay string into a `ParsedDate`.
6586
pub fn month_day_from_utf8(s: &[u8]) -> TemporalResult<Self> {
6687
let parse_record = parsers::parse_month_day(s)?;
@@ -70,6 +91,16 @@ impl ParsedDate {
7091
// Assertion: PlainDate must exist on a DateTime parse.
7192
let record = parse_record.date.temporal_unwrap()?;
7293

94+
// TODO: Potentially, remove this check in favor of type guarantee of a
95+
// Temporal parser.
96+
// NOTE: The check here is to confirm that the time component is
97+
// valid.
98+
let _time = parse_record
99+
.time
100+
.map(IsoTime::from_time_record)
101+
.transpose()?
102+
.unwrap_or_default();
103+
73104
Ok(Self { record, calendar })
74105
}
75106
}

0 commit comments

Comments
 (0)