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 }
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
}
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" />
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
Which seems fine, until you call .to_date
on it
Time.new(1,1,1).to_date
=> Mon, 03 Jan 0001
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.
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 } }
outputs
<input type="hidden" id="_starts_on_1i" name="[starts_on(1i)]" value="1970" autocomplete="off" />
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
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
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)
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)
Heh, on my machine
Time.new(1,1,1)
gives:WTF is
+0124
timezone 😅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
Ow wow, I love this. Time in a an endless wonder.