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:

(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