You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
builder: apply fixed offset before relative adjustments
A fixed offset (e.g. the "UTC" keyword) was applied after relative items, so "1970/01/01 UTC N seconds" added N seconds to a local wall-clock that could drift across a DST boundary before being re-anchored at the offset, yielding an instant off by the DST gap. Anchor the offset first, then apply relative items. Fixes uutils/coreutils#12555.
This commit is contained in:
+13
-10
@@ -189,8 +189,8 @@ impl DateTimeBuilder {
|
||||
/// - b. Apply time. If time carries an explicit numeric offset, apply the
|
||||
/// offset before setting time.
|
||||
/// - c. Apply weekday (e.g., "next Friday" or "last Monday").
|
||||
/// - d. Apply relative adjustments (e.g., "+3 days", "-2 months").
|
||||
/// - e. Apply final fixed offset if present.
|
||||
/// - d. Apply fixed offset if present (anchors the instant).
|
||||
/// - e. Apply relative adjustments (e.g., "+3 days", "-2 months").
|
||||
pub(super) fn build(self) -> Result<ParsedDateTime, error::Error> {
|
||||
if let Some(date) = self.date.as_ref() {
|
||||
if date.year.unwrap_or(0) > 9999 {
|
||||
@@ -317,7 +317,17 @@ impl DateTimeBuilder {
|
||||
dt = dt.checked_add(Span::new().try_days(delta)?)?;
|
||||
}
|
||||
|
||||
// 4d. Apply relative adjustments.
|
||||
// 4d. Apply fixed offset. This anchors the instant before relative
|
||||
// adjustments so that "1970/01/01 UTC N seconds" adds N seconds to the
|
||||
// fixed-offset instant rather than to a local wall-clock that may drift
|
||||
// across a DST boundary (see issue uutils/coreutils#12555).
|
||||
if let Some(offset) = self.offset {
|
||||
let (offset, hour_adjustment) = offset.normalize();
|
||||
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
|
||||
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
|
||||
}
|
||||
|
||||
// 4e. Apply relative adjustments.
|
||||
for rel in self.relative {
|
||||
dt = match rel {
|
||||
relative::Relative::Years(_) | relative::Relative::Months(_) => {
|
||||
@@ -338,13 +348,6 @@ impl DateTimeBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
// 4e. Apply final fixed offset.
|
||||
if let Some(offset) = self.offset {
|
||||
let (offset, hour_adjustment) = offset.normalize();
|
||||
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
|
||||
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
|
||||
}
|
||||
|
||||
Ok(dt)
|
||||
}
|
||||
|
||||
|
||||
@@ -285,3 +285,31 @@ fn test_multiple_month_skip(#[case] base: &str, #[case] input: &str, #[case] exp
|
||||
fn test_embedded_timezone(#[case] input: &str, #[case] expected: &str) {
|
||||
check_absolute(input, expected);
|
||||
}
|
||||
|
||||
// Regression test for uutils/coreutils#12555.
|
||||
// A fixed offset (e.g. the "UTC" keyword) must anchor the instant *before*
|
||||
// relative adjustments are applied. Otherwise, when the base zone observes DST,
|
||||
// adding a large relative offset drifts the wall-clock across a DST boundary
|
||||
// and re-anchoring at the fixed offset bakes in the extra hour.
|
||||
// "1970-01-01 UTC + N seconds" must equal exactly N seconds after the epoch,
|
||||
// regardless of the base zone's DST rules.
|
||||
#[test]
|
||||
fn test_utc_keyword_plus_relative_seconds_across_dst() {
|
||||
// Base in a DST-observing zone (Europe/Berlin); the value only matters as
|
||||
// the default zone for interpreting the date, not the result.
|
||||
let base = "2020-06-15 12:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::get("Europe/Berlin").unwrap())
|
||||
.unwrap();
|
||||
|
||||
// A timestamp landing in summer (CEST, UTC+2) while the epoch base is in
|
||||
// winter (CET, UTC+1), so the DST offset differs between base and result.
|
||||
let seconds = 1_780_318_971_i64;
|
||||
let input = format!("1970-01-01 UTC {seconds} seconds");
|
||||
|
||||
let parsed = parse_datetime::parse_datetime_at_date(base, input)
|
||||
.unwrap()
|
||||
.expect_in_range();
|
||||
assert_eq!(parsed.timestamp().as_second(), seconds);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user