January 1st 0001 is actually January 3rd 0001 or is it?
Tags: ruby, rails
Published: 2023-11-18 15:20:32

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. 

```ruby
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.

```ruby
{
  "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.

```html
<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.

```ruby
Time.new(1,1,1)
=> 0001-01-01 00:00:00 +0100
```

Which seems fine, until you call `.to_date` on it

```ruby
 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](https://www.quora.com/What-was-the-day-on-1st-Jan-0001-AD) 

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rq488vg5grteua55ehu2.png)

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`

```ruby
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

```html
<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, 

```ruby
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.

```ruby
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.