Skip to content

Preserve sub-second precision when coercing a DateTime in Time.at - #58085

Merged
byroot merged 1 commit into
rails:mainfrom
55728:fix-time-at-datetime-subsecond-precision
Jul 16, 2026
Merged

Preserve sub-second precision when coercing a DateTime in Time.at#58085
byroot merged 1 commit into
rails:mainfrom
55728:fix-time-at-datetime-subsecond-precision

Conversation

@55728

@55728 55728 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Time.at accepts an ActiveSupport::TimeWithZone or a DateTime as a single argument (ActiveSupport layers this on top of Time.at). The TimeWithZone branch converts via an exact rational timestamp (to_r), but the DateTime branch converted via a Float (to_f). A Float cannot represent every microsecond exactly, so the resulting Time#usec was wrong for roughly half of all sub-second DateTime values.

Before / After

dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 1_000_000)) # .000001s
Time.at(dt).usec
# before => 0
# after  => 1

dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(123_457, 1_000_000))
Time.at(dt).usec
# before => 123456
# after  => 123457

Why this is correct

  • It brings the DateTime branch in line with the TimeWithZone sibling, which already passes an exact rational timestamp so precision survives the round-trip. The exact fractional second is datetime.to_i + datetime.sec_fraction.
  • All other behavior is unchanged, verified across whole-second, offset (incl. DST), microsecond-boundary, and pre-epoch (negative timestamp) values: the resulting instant matches datetime.to_i + datetime.sec_fraction exactly, and the returned Time is still local (getlocal).

Time.at(datetime) round-trips a DateTime through a Float unix timestamp, which
cannot represent every microsecond exactly, so the resulting Time.usec was wrong
for roughly half of all sub-second values. The sibling TimeWithZone branch already
passes an exact rational timestamp; the DateTime branch now does the same, using
the exact fractional second.

  dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 1_000_000)) # .000001s
  Time.at(dt).usec
  # before => 0
  # after  => 1

Whole-second, offset, and pre-epoch values are unaffected.
@55728
55728 force-pushed the fix-time-at-datetime-subsecond-precision branch from a2e7517 to 1432277 Compare July 15, 2026 15:04
@byroot
byroot merged commit 93c3569 into rails:main Jul 16, 2026
4 checks passed
@55728
55728 deleted the fix-time-at-datetime-subsecond-precision branch July 16, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants