Skip to content

fix: serialize epoch datetimes on Windows - #242

Closed
ryanduguid wants to merge 4 commits into
XeroAPI:masterfrom
ryanduguid:agent/fix-windows-datetime-serialization
Closed

fix: serialize epoch datetimes on Windows#242
ryanduguid wants to merge 4 commits into
XeroAPI:masterfrom
ryanduguid:agent/fix-windows-datetime-serialization

Conversation

@ryanduguid

@ryanduguid ryanduguid commented Aug 12, 2026

Copy link
Copy Markdown

Summary

  • retain datetime.timestamp() as the normal path so platform local-time, DST gap and fold semantics remain unchanged
  • fall back to datetime arithmetic only when the platform rejects an out-of-range value, using Windows timezone data that supports pre-epoch values
  • preserve declared offsets for aware datetimes and UTC-midnight semantics for dates
  • add aware, naive and date negative-epoch regressions plus normal, ambiguous and nonexistent local-time parity cases

Why

On Windows in a positive UTC offset, serialising datetime.fromtimestamp(0) currently raises OSError: [Errno 22] Invalid argument because the local representation crosses before the platform epoch. The same test passes on Linux, so the existing Linux-only workflow did not expose it.

Validation

  • focused serializer suite: 81 passed on Windows
  • full suite with all nine Prism mocks: 177 passed on Windows
  • fatal-error flake8 selection passed
  • git diff --check

@ryanduguid
ryanduguid marked this pull request as ready for review August 14, 2026 10:33
tz.tzwinlocal() reports the DST rule in force today for every date, so naive
pre-epoch datetimes were converted with the wrong UTC offset and output
differed by platform. Serialising 1960-01-01 12:30 in Sydney gave
/Date(-315613800000)/, an hour off the true instant -315610200000, while
Linux read the tz database and was correct. Resolve the machine's IANA zone
name instead and take the offsets from the database bundled with
python-dateutil, which adds tzlocal to the runtime requirements.

Epoch conversion stayed in float seconds and truncated towards zero, losing
a millisecond on values that are not exactly representable and flipping the
rounding direction either side of the epoch. Use integer arithmetic.

Deserialisation still went through datetime.fromtimestamp, which raises
OSError on Windows for pre-epoch values, so neither Xero's own
/Date(-2208988800000)/ nor this branch's new output could be read back.
Offset the epoch by a timedelta instead.

Replace the naive pre-epoch test, which recomputed its expectation with a
copy of the implementation and so could not detect the hour shift, with
cases that pin a fixed timezone to known absolute instants. Add regression
cases that fail without the fix: exact millisecond values either side of the
epoch, pre-epoch deserialisation, and a serialise/deserialise round trip.
@ryanduguid

Copy link
Copy Markdown
Author

Closing this as part of cleaning up an unsolicited batch I opened across several Xero repositories. I will not reopen unless a maintainer asks for a single focused change.

@ryanduguid ryanduguid closed this Aug 20, 2026
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.

1 participant