Skip to content

Fix local/UTC confusion in dateTimeStamp conversions#156

Open
ajbufort wants to merge 1 commit into
Paligo:mainfrom
ajbufort:datetime-offset-consistency
Open

Fix local/UTC confusion in dateTimeStamp conversions#156
ajbufort wants to merge 1 commit into
Paligo:mainfrom
ajbufort:datetime-offset-consistency

Conversation

@ajbufort

Copy link
Copy Markdown

Fixes #117.

xs:date(current-dateTime()) could return a different date than current-date() near UTC midnight, and xs:dateTimeStamp("2025-04-01T23:00:00-01:00") round-tripped to 22:00:00-01:00.

Root cause

A NaiveDateTimeWithOffset stores a naive datetime that is local to its offset - that's the convention ToDateTimeStamp, display, and the parsers for xs:dateTime follow. A lexical datetime is likewise local to its timezone. But five sites interpreted these naive values as UTC, shifting values by their own offset:

Site Bug
date_time_stamp_parser from_utc_datetime on the lexical (local) datetime
cast_to_date_time_stamp (from xs:dateTime) from_naive_utc_and_offset on the stored (local) datetime
From<NaiveDateTimeWithOffset> for DateTime<FixedOffset> same; now delegates to the already-correct to_date_time_stamp
cast_to_date (from xs:dateTimeStamp) naive_utc().date() - the UTC date, not the date in the value's timezone
MapKey for xs:dateTimeStamp used the local naive datetime while offset-bearing xs:dateTime keys use the UTC-normalized one, so the same instant produced different map keys

The gYear/gMonth/gDay/time casts already used chrono's local accessors, and the adjust-*-to-timezone functions' naive_utc() is a correct shift idiom - those are untouched.

One wrinkle the local interpretation exposes: chrono stores DateTime<FixedOffset> UTC-normalized internally, so a value near chrono's range boundary can be representable as local-plus-offset but not as UTC (e.g. 262142-12-31T23:30:00-01:00). from_local_datetime returns None there, so the parser and the cast raise FODT0001 rather than unwrap - "262142-12-31T23:30:00-01:00" cast as xs:dateTimeStamp is a dynamic error, not a panic, with a regression test. (ToDateTimeStamp::to_date_time_stamp has a pre-existing unwrap() with the same theoretical reach through comparisons of extreme xs:dateTime values - that predates this change and is untouched here; happy to file it separately.)

Testing

  • New xee-xpath/tests/datetime_context.rs with an injected current_datetime reproduces every case from Oddness surrounding xs:date(xs:current-dateTime()) / xs:dateTimeStamp #117 deterministically (no faketime needed): xs:date(current-dateTime()) vs current-date() on both sides of UTC midnight, the xs:dateTimeStamp string and cast round trips, and map-key equivalence between xs:dateTime and xs:dateTimeStamp for the same instant.
  • Three existing snapshots had captured the buggy shifted values (an identity round trip through xs:dateTimeStamp gaining an hour) and are updated to the now-correct output.
  • XPath conformance: 20221 passing, no changes; XSLT conformance: 1098 passing, no changes; cargo fmt, clippy -D warnings, full cargo test clean.

One note for review: the From<NaiveDateTimeWithOffset> for DateTime<FixedOffset> impl turns out to have no callers anywhere in the workspace (verified by removing it and compiling all targets) - so that particular change can't have broken anything, and equally the reported bugs never flowed through it. I fixed it anyway (delegating to the already-correct to_date_time_stamp) so the public conversion agrees with the type's local-naive semantics rather than silently shifting for the next caller; happy to drop it to a doc comment or remove the impl instead if you prefer.

Generated with Claude Code

xs:date(current-dateTime()) could return a different date than
current-date() near UTC midnight, as reported in Paligo#117: the cast from
xs:dateTimeStamp to xs:date took the UTC date rather than the date in
the value's own timezone.

The underlying problem was wider: a NaiveDateTimeWithOffset stores a
naive datetime that is local to its offset, and a lexical datetime is
also local to its timezone, but several conversions interpreted these
naive values as UTC, shifting the value by its offset:

- parsing an xs:dateTimeStamp literal used from_utc_datetime, so
  xs:dateTimeStamp("2025-04-01T23:00:00-01:00") came back as
  22:00:00-01:00

- casting xs:dateTime to xs:dateTimeStamp used
  from_naive_utc_and_offset, shifting the same way

- the From<NaiveDateTimeWithOffset> conversion to a chrono DateTime
  did the same; it now goes through to_date_time_stamp, which was
  already correct (this conversion currently has no callers in the
  workspace, so it is fixed for consistency of the public API)

- casting xs:dateTimeStamp to xs:date took naive_utc().date()

- a dateTimeStamp map key used the local naive datetime while the
  equivalent dateTime-with-offset key used the UTC-normalized one, so
  the same instant produced different map keys depending on its type

Because chrono stores DateTime<FixedOffset> UTC-normalized internally,
a value near chrono's range boundary can be representable as
local-plus-offset but not as UTC. The parser and the cast raise
FODT0001 for these instead of panicking, with a regression test.

Three existing snapshots had captured the buggy shifted values (an
identity round trip through xs:dateTimeStamp gaining an hour) and are
updated to the now correct output.

Fixes Paligo#117.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Oddness surrounding xs:date(xs:current-dateTime()) / xs:dateTimeStamp

1 participant