Bug in Time, it wraps some dates to next month

Hi,

···

In message “bug in Time, it wraps some dates to next month” on 04/03/21, Sam Roberts sroberts@uniserve.com writes:

[ensemble] ~/p/ruby/src/ruby $ ./ruby -e “p Time.local(2004, ‘feb’, 31, 1, 1, 1)”
Tue Mar 02 01:01:01 EST 2004

So it wraps into the next month if the day-of-month is past the end of
the month. Arguably a feature.

But:

[ensemble] ~/p/ruby/src/ruby $ ./ruby -e “p Time.local(2004, ‘feb’, 32, 1, 1, 1)”
-e:1:in `local’: argument out of range (ArgumentError) from -e:1

It doesn’t do it if the day-of-month is past 31?

So, I think it is a bug, not a feature.

This is caused by rather stupid (but simple) date range check. I’m
not sure what is the best way. No check? Or strict check?
Discussion welcome.

						matz.

Quoteing matz@ruby-lang.org, on Mon, Mar 22, 2004 at 01:45:22AM +0900:

>[ensemble] ~/p/ruby/src/ruby $ ./ruby -e "p Time.local(2004, 'feb', 31, 1, 1, 1)"
>Tue Mar 02 01:01:01 EST 2004
>
>So it wraps into the next month if the day-of-month is past the end of
>the month. Arguably a feature.
>
>But:
>
>[ensemble] ~/p/ruby/src/ruby $ ./ruby -e "p Time.local(2004, 'feb', 32, 1, 1, 1)"
>-e:1:in `local': argument out of range (ArgumentError) from -e:1
>
>It doesn't do it if the day-of-month is past 31?
>
>So, I think it is a bug, not a feature.

This is caused by rather stupid (but simple) date range check. I'm
not sure what is the best way. No check? Or strict check?
Discussion welcome.

Most APIs do strict check, I think they should all do strict check.
There are only some corner cases where the check isn't done, here's
another:

  irb(main):001:0> DateTime.civil(2004,2,29,23).to_s
  => "2004-02-29T23:00:00Z"
  irb(main):002:0> DateTime.civil(2004,2,29,24).to_s
  => "2004-03-01T00:00:00Z"
  irb(main):006:0> DateTime.civil(2004,2,29,25).to_s
  ArgumentError: invalid date

One hour past rolls over to the next day, but 2 hours past doesn't!

Allowing out-of-bounds is useful only to date-time arithmetic, but it
won't work that well, because subtracting will give negative values
inconsistent with how Date and DateTime treat negative values.

So, I think:

1 - Time, Date, and DateTime should all check their bounds, and
refuse all out-of-bounds values

2 - Time should allow negative (in-range) values, with the same meaning
as Date and DateTime.

3 - Time and DateTime both need methods to add and subtract units of
years, months, days, hours, minutes, secs, and usecs.

Here's my rational:

Doing less strict checks in Time would make it easier to do date/time
arithmetic:

  t = Time.now

  t_plus_month = Time.local(t.year, t.mon + 1, t.day)

  t_plus_week = Time.local(t.year, t.mon, t.day + 7)

  t_minus_week = Time.local(t.year, t.mon, t.day - 7)

But it only makes it easier in the forwards direction, in the backwards
direction it would introduce a strange and subtly incompatible
difference in behaviour between DateTime and Time:

  irb(main):008:0> DateTime.civil(2004,2, 3 - 7).to_s
  => "2004-02-26T00:00:00Z"

With DateTime, negatives are counted from the end of the unit, they
are not treated as if they were the result of subtracting a certain
number of days from the date! This is quite different from my t.day - 7
example above.

The meaning of negative is actually a fairly useful behaviour of
DateTime (at least, I use it), and I wouldn't want it changed, or to
introduce changes to Time that make negatives mean different things than
they do in DateTime.

So, we are stuck with no easy way to do date arithmetic, we only have:

Time#+,Time#-

  can be used for hours/minutes/seconds, but not usec, or
  days/months/years

Date#+,Date#-

  can be used for days, only

Date#<<,Date#>>

  can be used for months

I would like to see a number of plus_/minus_ methods added to Time and
Date/DateTime that return a new Time/Date/DateTime at the requested
offset. Here's what I do now to get the addition methods I need:

  class Time
    def plus_year(years)
      Time.local(year + years, month, day, hour, min, sec, usec)
    end

    def plus_month(months)
      d = Date.new(year, month, day)
      d >>= months
      Time.local(d.year, d.month, d.day, hour, min, sec, usec)
    end

    def plus_day(days)
      d = Date.new(year, month, day)
      d += days
      Time.local(d.year, d.month, d.day, hour, min, sec, usec)
    end
  end

It shouldn't be so hard!

Thanks,
Sam

···

On 04/03/21, Sam Roberts <sroberts@uniserve.com> writes: