DEV Community

RobL
RobL

Posted on

January 1st 0001 is actually January 3rd 0001 or is it?

I came across a rather strange bug in some client code yesterday which had me stumped for quite some time. They're using a date_select to present the day and month and storing that day with just any arbitrary year. It doesn't really matter just that you can retrieve the date.

f.date_select :starts_on, { order: %i[day month], include_blank: true } 
Enter fullscreen mode Exit fullscreen mode

As we might expect the date select creates MultiParameterAttributes which will result in our parameters containing values like so.

{ "starts_on(1i)": "1", # year "starts_on(2i)": "1", # month "starts_on(3i)": "1". # day } 
Enter fullscreen mode Exit fullscreen mode

The resulting html from the date select produces 2 selects for the month and day, but since we're excluding the year we get a hidden field for the year since we need starts_on(1i), starts_on(2i) and starts_on(3i) parameters to construct a Date/Time.

<input type="hidden" id="_starts_on_1i" name="[starts_on(1i)]" value="1" autocomplete="off" /> 
Enter fullscreen mode Exit fullscreen mode

It's worth noting that the year defaults "1" because we are using include_blank: true because we want the the date to be set or not. This has interesting side-effects. In part because we're using Mongoid, Mongoid stores dates as times so a Time object is being created when we mass-assign it. We're effectively doing this.

Time.new(1,1,1) => 0001-01-01 00:00:00 +0100 
Enter fullscreen mode Exit fullscreen mode

Which seems fine, until you call .to_date on it

 Time.new(1,1,1).to_date => Mon, 03 Jan 0001 
Enter fullscreen mode Exit fullscreen mode

Oh. Erm... I was all ready to start reporting this as a bug. It's not really a bug but an anomaly. The first thing I can find out about it is on this Quora thread

Ok, calendars at that time varied and who am I to argue, it was 2000 years ago. I am by no means a calendar expert.

Image description

What I do know is that I need a solution to my problem, suddenly moving from Rails 6.0 to Rails 6.1 (in Ruby 3.2.2) this has appeared. The fastest solution without getting bogged down and changing 100 different selects is to start thinking in terms of another default year.

This would be easy if we didn't have to use include_blank

helper.date_select '', :starts_on, { order: %i[day month], default: Date.new(1970,1,1) } # or  helper.date_select '', :starts_on, { order: %i[day month], default: { year: 1970 } } 
Enter fullscreen mode Exit fullscreen mode

outputs

<input type="hidden" id="_starts_on_1i" name="[starts_on(1i)]" value="1970" autocomplete="off" /> 
Enter fullscreen mode Exit fullscreen mode

and 1970 onwards uses a calendar we can rely on,

Time.new(1970, 1, 1).to_date => 1970-01-01 00:00:00 +0100 Time.new(1970,1,1).to_date => Thu, 01 Jan 1970 
Enter fullscreen mode Exit fullscreen mode

However since we're using include_blank I need to just hack date_select to use "1970" instead of "1" and be done with it.

module ActionView module Helpers class DateTimeSelector def select_year if !year || @datetime == 0 val = "1" # hardcoded to "1" middle_year = Date.today.year else val = middle_year = year end if @options[:use_hidden] || @options[:discard_year] build_hidden(:year, val.to_i < 1800 ? "1970" : val) # don't use "1" please. else options = {} options[:start] = @options[:start_year] || middle_year - 5 options[:end] = @options[:end_year] || middle_year + 5 options[:step] = options[:start] < options[:end] ? 1 : -1 options[:leading_zeros] = false options[:max_years_allowed] = @options[:max_years_allowed] || 1000 if (options[:end] - options[:start]).abs > options[:max_years_allowed] raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter." end build_select(:year, build_year_options(val, options)) end end end end end 
Enter fullscreen mode Exit fullscreen mode

It's heavy handed but there was no other way to change that hardcoded "1" default.

build_hidden(:year, val.to_i < 1800 ? "1970" : val) 
Enter fullscreen mode Exit fullscreen mode

Additionally, I change the value to 1970 if the date happend to be less than 1800 (either by default or if it from the database), so if the value in the database is already 0001 or 0000, then the date_select will use the values for year that are already set which means we could potentially start poisoning the data in the database unless we go and correct all values to use 1970 in advance of rolling this out.

Top comments (3)

Collapse
 
katafrakt profile image
Paweł Świątkowski • Edited

Heh, on my machine Time.new(1,1,1) gives:

=> 0001-01-01 00:00:00 +0124 
Enter fullscreen mode Exit fullscreen mode

WTF is +0124 timezone 😅

Collapse
 
simongreennet profile image
Simon Green • Edited

The standard 15 minute offsets from UTC is only around a century old. Wikipedia states +1:24 offset was used in Warsaw before they moved to CET in 1915 (108 years ago).

en.wikipedia.org/wiki/UTC%2B01:24

Collapse
 
braindeaf profile image
RobL

Ow wow, I love this. Time in a an endless wonder.