Skip to content

Time.rfc3339 and DateTime.rfc3339 parse some inputs differently #73

Description

@yahonda

#59 added Time.rfc3339. Its behavior differs from the date gem's existing RFC 3339 parsers (DateTime.rfc3339 and Date.rfc3339, thin wrappers over Date._rfc3339 that accept and reject the same inputs).

Steps to reproduce

$ ruby -v
ruby 4.1.0dev (2026-07-22T21:52:01Z master 8645a948c0) +PRISM [x86_64-linux]

$ TZ=America/New_York ruby -rtime -e 'p Time.rfc3339("1999-12-31T19:00:00")'
$ TZ=America/New_York ruby -rdate -e 'p DateTime.rfc3339("1999-12-31T19:00:00")'
$ TZ=America/New_York ruby -rdate -e 'p Date.rfc3339("1999-12-31T19:00:00")'
$ ruby -rtime -e 'p Time.rfc3339("1999-12-31t19:00:00z")'
$ ruby -rdate -e 'p DateTime.rfc3339("1999-12-31t19:00:00z")'
$ ruby -rdate -e 'p Date.rfc3339("1999-12-31t19:00:00z")'

Expected behavior

The three methods accept and reject the same inputs: "1999-12-31T19:00:00" raises an exception because the date-time grammar in RFC 3339 Section 5.6 (https://datatracker.ietf.org/doc/html/rfc3339#section-5.6) requires time-offset, and "1999-12-31t19:00:00z" is accepted because the Section 5.6 NOTE permits the lower case t/z.

Actual behavior

Time.rfc3339 goes the other way on both inputs:

$ TZ=America/New_York ruby -rtime -e 'p Time.rfc3339("1999-12-31T19:00:00")'
1999-12-31 19:00:00 -0500

$ TZ=America/New_York ruby -rdate -e 'p DateTime.rfc3339("1999-12-31T19:00:00")'
-e:1:in 'DateTime.rfc3339': invalid date (Date::Error)

$ TZ=America/New_York ruby -rdate -e 'p Date.rfc3339("1999-12-31T19:00:00")'
-e:1:in 'Date.rfc3339': invalid date (Date::Error)

$ ruby -rtime -e 'p Time.rfc3339("1999-12-31t19:00:00z")'
/home/yahonda/.local/share/mise/installs/ruby/trunk/lib/ruby/4.1.0+4/time.rb:673:in 'Time#initialize': "+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: t19:00:00z (ArgumentError)

$ ruby -rdate -e 'p DateTime.rfc3339("1999-12-31t19:00:00z")'
#<DateTime: 1999-12-31T19:00:00+00:00 ((2451544j,68400s,0n),+0s,2299161j)>

$ ruby -rdate -e 'p Date.rfc3339("1999-12-31t19:00:00z")'
#<Date: 1999-12-31 ((2451544j,0s,0n),+0s,2299161j)>

Including the offset forms +04 / -0400, which RFC 3339 does not allow (time-numoffset has only the hh:mm form):

Input Time.rfc3339 DateTime.rfc3339 / Date.rfc3339
"1999-12-31T19:00:00" (no offset) accepted, returns local time raise
"1999-12-31T19:00:00+04", "1999-12-31T19:00:00-0400" accepted raise
"1999-12-31t19:00:00z" (lower case) raises accept

Additional context

The date gem's parser has behaved this way since 2011 (ruby/date@8361362) and documents its input as "a valid RFC 3339 format" (https://github.com/ruby/date/blob/3481cf7ce9149011f1a4eb2bfffc2a0b44c8bf07/ext/date/date_core.c#L4867-L4868).

One deployed use case relies on exactly that behavior: Active Support's Time.rfc3339 has been a thin wrapper around Date._rfc3339 since Rails 5.1 (rails/rails#28272, 2017): https://github.com/rails/rails/blob/fa8f0812160665bff083a089d2bb2fc1817ea03e/activesupport/lib/active_support/core_ext/time/calculations.rb#L69-L83. Its documented and tested contract — ArgumentError when the offset is missing — is the date gem's. On Ruby master the new method now redefines Active Support's (rails/rails#58172 proposes to silence the warning). If the behaviors matched, delegating to the Ruby implementation, as Active Support did with Range#overlap?, would become a possibility.

Since Time.rfc3339 is unreleased, this seems worth settling before Ruby 4.1 ships.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions