Duration

It looks as though if you ask a DateTime to add something to itself,
it looks at the something, and if it's an integer, it interprets it as
a duration in seconds, but if it's a Rational, it interprets it as a
duration in days.

It's inconsistent. Why didn't the designers settle on one type for
durations and one way to interpret them? What should one store in a
database for a duration? What rules should one follow to avoid being
tripped up by numbers being interpreted as different measures
depending on accidental features such as the numbers'
representations? This is very disappointing; engineering designs
should make logical sense.

x = DateTime.now

=> Tue, 21 Dec 2010 12:10:14 -0500

d1 = 1.second

=> 1 second

y = x + d1

=> Tue, 21 Dec 2010 12:10:15 -0500

y.class

=> DateTime

y - x

=> Rational(1, 86400)

d2 = y - x

=> Rational(1, 86400)

d1.class

=> Fixnum

d1

=> 1 second

0 + d1

=> 1

d1 == d2

=> false

(x + d1) == (x + d2)

=> true

It looks as though if you ask a DateTime to add something to itself,
it looks at the something, and if it's an integer, it interprets it as
a duration in seconds, but if it's a Rational, it interprets it as a
duration in days.

That's not actually the whole store - If you did DateTime.now + 1 then
you would get 1 day after now.
The key is that 1.second (and similarly 3.days, 2.hours + 6.minutes
etc) aren't integers - they're ActiveSupport::Duration objects.
They quite often pretend to be integers (the number of seconds
represented by the duration - although this is obviously dodgy if the
duration is something like 1 month), largely because that's quite
convenient when dealing with Time objects and I suspect because of
backwards compatibility (at one point in Rails' history
the .second/.day... methods were just convenience methods that
returned the corresponding number of seconds)

It's inconsistent. Why didn't the designers settle on one type for
durations and one way to interpret them? What should one store in a
database for a duration? What rules should one follow to avoid being
tripped up by numbers being interpreted as different measures
depending on accidental features such as the numbers'
representations? This is very disappointing; engineering designs
should make logical sense.

It depends on what the duration represents. Sometimes you really do
just mean some fixed length of time, so storing a integer number of
seconds is good enough. It's often more complicated though, for
example if a duration is 1 month then your users might expect the 3rd
october to be followed by 3rd november and then by the 3rd of december
even though those are actually different numbers of seconds. Similarly
users will often expect that things that are 1 day long will end/start
on the same time every day, even though when clocks go back/forward a
day in local time is actually 23/25 hours long.

Fred

Jack Waugh wrote in post #969867:

It looks as though if you ask a DateTime to add something to itself,
it looks at the something, and if it's an integer, it interprets it as
a duration in seconds, but if it's a Rational, it interprets it as a
duration in days.

It's inconsistent. Why didn't the designers settle on one type for
durations and one way to interpret them?

Adding a dimensionless number to a measurement with units is asking for
trouble. Just specify units and you'll be fine.

What should one store in a
database for a duration?

Anything you like. Use composed_of if necessary.

What rules should one follow to avoid being
tripped up by numbers being interpreted as different measures
depending on accidental features such as the numbers'
representations?

Use units.

This is very disappointing; engineering designs
should make logical sense.

It makes lots of sense to specify units (and as Fred said, 1.hour is a
Duration, not a Fixnum). What doesn't make sense is expecting the sum
of a Time and a Fixnum to be anything but undefined.

Best,

Frederick Cheung wrote in post #969871:

It looks as though if you ask a DateTime to add something to itself,
it looks at the something, and if it's an integer, it interprets it as
a duration in seconds, but if it's a Rational, it interprets it as a
duration in days.

That's not actually the whole store - If you did DateTime.now + 1 then
you would get 1 day after now.
The key is that 1.second (and similarly 3.days, 2.hours + 6.minutes
etc) aren't integers - they're ActiveSupport::Duration objects.
They quite often pretend to be integers (the number of seconds
represented by the duration - although this is obviously dodgy if the
duration is something like 1 month), largely because that's quite
convenient when dealing with Time objects and I suspect because of
backwards compatibility (at one point in Rails' history
the .second/.day... methods were just convenience methods that
returned the corresponding number of seconds)

I am actually curious how I could see from the object
itself in a Rails context that it's not "really" a Fixnum,
but an ActiveSupport::Duration object

$ rails c
Loading development environment (Rails 3.0.3)
001:0> weekend = 2.days
=> 2 days
002:0> weekend.class
=> Fixnum # but there is more functionality than a "Fixnum"
003:0> weekend.singleton_class
=> #<Class:#<ActiveSupport::Duration:0xb727e068>> # a hint ?

Also, here I did use specific units (in-casu "days"),
but still I had naively expected a different result
on line 005.

004:0> weekend.seconds
=> 172800 seconds # this is correct
005:0> weekend.days
=> 172800 days # I had expected '2' or '2 days'

Thanks,

Peter

“Just specify units and you’ll be fine.”

That appeals to my notion of what is logical. Let’s try it.

x = DateTime.now
=> Wed, 22 Dec 2010 01:30:36 -0500

y = x + 1.day
=> Thu, 23 Dec 2010 01:30:36 -0500

So far, so good.

(y - x) / 1.second
=> Rational(28799745541, 28800000000)

Since y is one day later than x, y - x should be a day. Dividing that by 1 second should yield a pure number equal to the number of seconds in a day.

((y - x) / 1.second).to_i
=> 0

Even if coerced to an integer, the number of seconds in a day should be much more than zero.

I used units, but the result differs from what seems to me to make sense. How can I safely represent a duration in the database and then use the duration in date arithmetic?

Frederick Cheung wrote in post #969871:

I am actually curious how I could see from the object
itself in a Rails context that it's not "really" a Fixnum,
but an ActiveSupport::Duration object

ActiveSupport::Duration === foo would return true (or Fixnum === foo
would return false)

Also, here I did use specific units (in-casu "days"),
but still I had naively expected a different result
on line 005.

004:0> weekend.seconds
=> 172800 seconds # this is correct
005:0> weekend.days
=> 172800 days # I had expected '2' or '2 days'

a Duration object doesn't have days (or seconds etc.) methods that
return the invidual components (the parts method does that), so when
you call days on it, it ends up calling days on its value in seconds.

"Just specify units and you'll be fine."

That appeals to my notion of what is logical. Let's try it.

>> x = DateTime.now

=> Wed, 22 Dec 2010 01:30:36 -0500>> y = x + 1.day

=> Thu, 23 Dec 2010 01:30:36 -0500

So far, so good.

>> (y - x) / 1.second

=> Rational(28799745541, 28800000000)

Since y is one day later than x, y - x should be a day. Dividing that by 1
second should yield a pure number equal to the number of seconds in a day.

The problem here is that y-x is a unitless number. Even if someone had
thought to override / for datetime or durations (they haven't) you
can't know that this is a number that has come from subtracting two
datetimes, two dates, two times or something completely different

>> ((y - x) / 1.second).to_i

=> 0

Even if coerced to an integer, the number of seconds in a day should be much
more than zero.

I used units, but the result differs from what seems to me to make sense.
How can I safely represent a duration in the database and then use the
duration in date arithmetic?

When you subtract or divide datetime objects you get unitless
Rationals - the rest of the code doesn't know if that's a number of
seconds, days, years etc. ActiveSupport::Duration assumes seconds
which happens to be wrong. If you used Time instead of DateTime I
think things would work as you expect. If that's not an option, I
can't think of a really nice way of handling this short of adding
quite a lot of extra stuff to DateTime / wrapping DateTime in
something else or just being really meticulous about always being very
aware that some numbers are numbers of days and others numbers of
seconds. Even then I think you could always find some operation
(squareroot, gamma function, whatever) that exposed that something was
thinking in days and the other was thinking in seconds.

Fred

Frederick Cheung wrote in post #970028:

Frederick Cheung wrote in post #970025:

a Duration object doesn't have days (or seconds etc.) methods that
return the invidual components (the parts method does that)

Many thanks. I found that very useful to understand the behaviour.

$ rails c
Loading development environment (Rails 3.0.3)
001:0> a_long_time = 1.year + 2.months + 3.weeks + 4.days + 5.hours +
6.minutes + 7.seconds
=> 1 year, 2 months, 25 days, and 18367 seconds
002:0> a_long_time.parts
=> [[:years, 1], [:months, 2], [:days, 21], [:days, 4], [:seconds,
18000], [:seconds, 360], [:seconds, 7]]

Peter

Frederick Cheung wrote in post #970028:

>> So far, so good.

>> >> (y - x) / 1.second

>> => Rational(28799745541, 28800000000)

There's one gotcha here: the '+ 1.day' part appears to drop the
microseconds part of the DateTime. For instance, this will show the
same sort of behavior:

d = DateTime.now
(d - Time.parse(d.inspect).to_datetime)*86400 # => a number smaller than one; actually the undisplayed fractional seconds

Here the Time.parse chunk basically zeroes out everything but the
displayed parts of d, causing the difference. If I repeat your test
with this change:

x = DateTime.now

=> Wed, 22 Dec 2010 16:11:47 -0500

x_flat = Time.parse(x.inspect).to_datetime

=> Wed, 22 Dec 2010 16:11:47 -0500

y = x_flat + 1.day

=> Thu, 23 Dec 2010 16:11:47 -0500

y - x_flat

=> Rational(1, 1)

Which matches the definition of DateTime#-, returning a result in
days.

As to the division issue, I don't think that's the intended reading
of .days and friends. It *used* to be (back when 1.day => 86400), but
that hasn't been the behavior since 2007 in:

https://github.com/rails/rails/blob/276c9f29cde80fafa23814b0039f67504255e0fd/activesupport/lib/active_support/duration.rb

(would link to the old Trac ticket, but dev.rubyonrails.org seems to
be down). The ActiveSupport::Duration class was explicitly designed to
make + and - work correctly (corresponding to passing the given number
and unit to DateTime#adjust), not to do any other unit-related
conversions. If you want the old behavior, you can call .to_i on a
Duration:

1.day.to_i

# => 86400

--Matt Jones