Dave Allie
Dave Allie

Reputation: 51

Rails Date#strptime parsing dates incorrectly before year 200

Why does Rails' Date#strptime parse "13/08" as August 15 or August 14 before the year 200?

Date.strptime('13/08/99', '%d/%m/%Y')  #=> Thu, 15 Aug 0099
Date.strptime('13/08/100', '%d/%m/%Y') #=> Fri, 14 Aug 0100
Date.strptime('13/08/199', '%d/%m/%Y') #=> Tue, 14 Aug 0199
Date.strptime('13/08/200', '%d/%m/%Y') #=> Wed, 13 Aug 0200

Upvotes: 4

Views: 383

Answers (2)

Eric Duminil
Eric Duminil

Reputation: 54313

To sum up :

If you don't use timecop gem, Date#strptime seems to work fine for year < 200.

If you use timecop, Date#strptime is overwritten and uses Time#to_date, which seems to return wrong values for year < 200.

Easy solutions, either :

  • don't use timecop
  • use Date#strptime_without_mock_date if you do use timecop
  • use Date.new + Time#strptime

Harder solution : understand what's wrong with the implementation of Time#to_date (see Stefan's explanation.)

[0] pry(main)> Time.local(99,8,13).to_date
=> #<Date: 0099-08-15 ((1757444j,0s,0n),+0s,2299161j)>
[1] pry(main)> Date.strptime('13/08/99', '%d/%m/%Y')
=> #<Date: 0099-08-13 ((1757442j,0s,0n),+0s,2299161j)>
[2] pry(main)> require 'timecop'
=> true
[3] pry(main)> Date.strptime('13/08/99', '%d/%m/%Y')
=> #<Date: 0099-08-15 ((1757444j,0s,0n),+0s,2299161j)>
[4] pry(main)> Date.strptime_without_mock_date('13/08/99', '%d/%m/%Y')
=> #<Date: 0099-08-13 ((1757442j,0s,0n),+0s,2299161j)>
[5] pry(main)> time = Time.strptime('13/08/99', '%d/%m/%Y')
=> 0099-08-13 00:00:00 +0053
[6] pry(main)> Date.new(time.year,time.month,time.day)
=> #<Date: 0099-08-13 ((1757442j,0s,0n),+0s,2299161j)>

Upvotes: 4

Stefan
Stefan

Reputation: 114268

Using github.com/daveallie/where_is:

Where.is(Date, :strptime)
#=> ["/home/deployer/.rvm/gems/ruby-2.3.1/gems/timecop-0.7.0/lib‌​/timecop/time_extens‌​ions.rb", 46]

which leads to timecop/time_extensions.rb#L46:

def strptime_with_mock_date(str, fmt)
  Time.strptime(str, fmt).to_date
end

That's correct, Timecop turns:

Date.strptime('13/08/99', '%d/%m/%Y')
#=> #<Date: 0099-08-13 ((1757442j,0s,0n),+0s,2299161j)>

into:

Time.strptime('13/08/99', '%d/%m/%Y').to_date
#=> #<Date: 0099-08-15 ((1757444j,0s,0n),+0s,2299161j)>

So it's not Date, that's buggy, it's Time?

It's not exactly buggy, just different. Time#to_date interprets the values as proleptic Gregorian calendar dates:

Date.new(99, 8, 13, Date::GREGORIAN).new_start
#=> #<Date: 0099-08-15 ((1757444j,0s,0n),+0s,2299161j)>

versus:

Date.new(99, 8, 13, Date::ITALY).new_start
#=> #<Date: 0099-08-13 ((1757442j,0s,0n),+0s,2299161j)>

Here's a summary from Wikipedia:

Julian range             | Proleptic Gregorian range | Gregorian ahead by:
-------------------------|---------------------------|--------------------
03/03/4    - 01/03/100   | 01/03/4    - 28/02/100    | −2 days
02/03/100  - 29/02/200   | 01/03/100  - 28/02/200    | −1 days
01/03/200  - 28/02/300   | 01/03/200  - 28/02/300    |  0 days
29/02/300  - 27/02/500   | 01/03/300  - 28/02/500    |  1 day
28/02/500  - 26/02/600   | 01/03/500  - 28/02/600    |  2 days
27/02/600  - 25/02/700   | 01/03/600  - 28/02/700    |  3 days
26/02/700  - 24/02/900   | 01/03/700  - 28/02/900    |  4 days
25/02/900  - 23/02/1000  | 01/03/900  - 28/02/1000   |  5 days
24/02/1000 - 22/02/1100  | 01/03/1000 - 28/02/1100   |  6 days
23/02/1100 - 21/02/1300  | 01/03/1100 - 28/02/1300   |  7 days
22/02/1300 - 20/02/1400  | 01/03/1300 - 28/02/1400   |  8 days
21/02/1400 - 19/02/1500  | 01/03/1400 - 28/02/1500   |  9 days
20/02/1500 - 04/10/1582  | 01/03/1500 - 14/10/1582   | 10 days

Upvotes: 2

Related Questions