Friday, December 9, 2011

Ruby Time and DateTime: parse a string, compute the difference, simple right?

Time in ruby is a confusing maze of suckiness.

The Ruby Time class doesn't include a method to convert a string into a Time object (like the C standard 'strptime'), despite having a Time.strftime function. What the?

It turns out that Time.parse and Time.strptime do exist, but confusingly they aren't in the core class, but in a separate library you have to require that overrides the class. WTF?

irb(main):001:0> Time.parse
NoMethodError: undefined method `parse' for Time:Class
 from (irb):1
irb(main):002:0> require 'time'
=> true
irb(main):003:0> Time.parse
ArgumentError: wrong number of arguments (0 for 1)
 from (irb):3:in `parse'
 from (irb):3

So it seems that the best way to read a time from a file and compare it to the current time is the following, which gives you an answer in seconds:
irb(main):010:0> a=Time.parse('2011-01-10 12:34:00 UTC')
=> Mon Jan 10 12:34:00 UTC 2011
irb(main):012:0> b=Time.now.utc
=> Fri Dec 09 20:47:32 UTC 2011
irb(main):013:0> b-a
=> 28800812.788154

The only downside of this is that the library never fails, it tries really hard to give you a Time object back, to the point where it will just return the current time if the string is rubbish:
irb(main):027:0> Time.parse('asdfasdfas')
=> Fri Dec 09 14:02:40 -0800 2011

whereas DateTime is actually more sensible:
irb(main):028:0> DateTime.parse('asdfasdfas')
ArgumentError: invalid date
 from /usr/lib/ruby/1.8/date.rb:1576:in `new_by_frags'
 from /usr/lib/ruby/1.8/date.rb:1621:in `parse'
 from (irb):28
 from  :0

So my final solution was:
  begin
    DateTime.parse(time_string)
  rescue ArgumentError
    return 'UNKNOWN'
  end
  earlier = Time.parse(time_string)

  now = Time.now.utc
  hours = (now-earlier)/3600
  return hours
Read on for an attempt to do the same with DateTime, which is a terrible, terrible API.

DateTime has a simple way to ingest strings (and also has a strptime):
irb(main):015:0> require 'date'
=> false
irb(main):023:0> a=DateTime.parse('2011-12-09 00:00:00 UTC')
=> #
irb(main):024:0> puts a
2011-12-09T00:00:00+00:00
=> nil

But you can't take a difference between Time and DateTime:
irb(main):027:0> a=DateTime.parse('2011-12-01 00:00:00 UTC')
=> #
irb(main):028:0> b=Time.now
=> Fri Dec 09 12:10:40 -0800 2011
irb(main):029:0> b-a
TypeError: can't convert DateTime into Float
 from (irb):29:in `-'
 from (irb):29
 from  :0

Subtracting two DateTimes works but gives you an answer that is a rational number of days (?!):
irb(main):031:0> b-a
=> Rational(76394798789, 8640000000)
irb(main):032:0> c=b-a
=> Rational(76394798789, 8640000000)
irb(main):033:0> puts c
76394798789/8640000000
=> nil
irb(main):034:0> puts c.to_f
8.8419906005787
=> nil

Since a fractional number of days is pretty useless you can then convert this to an array of [hours,minutes,seconds,frac]:
irb(main):036:0> DateTime.day_fraction_to_time(c)
=> [212, 12, 27, Rational(98789, 8640000000)]

Why the subtraction doesn't return another type of time object is beyond me.

No comments: