Converting Between Time and DateTime Objects
Problem
You're working with both DateTime and Time objects, created from Ruby's two standard date/time libraries. You can't mix these objects in comparisons, iterations, or date arithmetic because they're incompatible. You want to convert all the objects into one form or another so that you can treat them all the same way.
Solution
To convert a Time object to a DateTime, you'll need some code like this:
require 'date' class Time def to_datetime # Convert seconds + microseconds into a fractional number of seconds seconds = sec + Rational(usec, 10**6) # Convert a UTC offset measured in minutes to one measured in a # fraction of a day. offset = Rational(utc_offset, 60 * 60 * 24) DateTime.new(year, month, day, hour, min, seconds, offset) end end time = Time.gm(2000, 6, 4, 10, 30, 22, 4010) # => Sun Jun 04 10:30:22 UTC 2000 time.to_datetime.to_s # => "2000-06-04T10:30:22Z"
Converting a DateTime to a Time is similar; you just need to decide whether you want the Time object to use local time or GMT. This code adds the conversion method to Date, the superclass of DateTime, so it will work on both Date and DateTime objects.
class Date def to_gm_time to_time(new_offset, :gm) end def to_local_time to_time(new_offset(DateTime.now.offset-offset), :local) end private def to_time(dest, method) #Convert a fraction of a day to a number of microseconds usec = (dest.sec_fraction * 60 * 60 * 24 * (10**6)).to_i Time.send(method, dest.year, dest.month, dest.day, dest.hour, dest.min, dest.sec, usec) end end (datetime = DateTime.new(1990, 10, 1, 22, 16, Rational(41,2))).to_s # => "1990-10-01T22:16:20Z" datetime.to_gm_time # => Mon Oct 01 22:16:20 UTC 1990 datetime.to_local_time # => Mon Oct 01 17:16:20 EDT 1990
Discussion
Ruby's two ways of representing dates and times don't coexist very well. But since neither can be a total substitute for the other, you'll probably use them both during your Ruby career. The conversion methods let you get around incompatibilities by simply converting one type to the other:
time < datetime # ArgumentError: comparison of Time with DateTime failed time.to_datetime < datetime # => false time < datetime.to_gm_time # => false time - datetime # TypeError: can't convert DateTime into Float (time.to_datetime - datetime).to_f # => 3533.50973962975 # Measured in days time - datetime.to_gm_time # => 305295241.50401 # Measured in seconds
The methods defined above are reversible: you can convert back and forth between Date and DateTime objects without losing accuracy.
time # => Sun Jun 04 10:30:22 UTC 2000 time.usec # => 4010' time.to_datetime.to_gm_time # => Sun Jun 04 10:30:22 UTC 2000 time.to_datetime.to_gm_time.usec # => 4010 datetime.to_s # => "1990-10-01T22:16:20Z" datetime.to_gm_time.to_datetime.to_s # => "1990-10-01T22:16:20Z"
Once you can convert between Time and DateTime objects, it's simple to write code that normalizes a mixed array, so that all its elements end up being of the same type. This method tries to turn a mixed array into an array containing only Time objects. If it encounters a date that won't fit within the constraints of the Time class, it starts over and converts the array into an array of DateTime objects instead (thus losing anyinformation about Daylight Saving Time):
def normalize_time_types(array) # Don't do anything if all the objects are already of the same type. first_class = array[0].class first_class = first_class.super if first_class == DateTime return unless array.detect { |x| !x.is_a?(first_class) } normalized = array.collect do |t| if t.is_a?(Date) begin t.to_local_time rescue ArgumentError # Time out of range; convert to DateTimes instead. convert_to = DateTime break end else t end end unless normalized normalized = array.collect { |t| t.is_a?( Time) ? t.to_datetime : t } end return normalized end
When all objects in a mixed array can be represented as either Time or DateTime objects, this method makes them all Time objects:
mixed_array = [Time.now, DateTime.now] # => [Sat Mar 18 22:17:10 EST 2006, # #] normalize_time_types(mixed_array) # => [Sat Mar 18 22:17:10 EST 2006, Sun Mar 19 03:17:10 EST 2006]
If one of the DateTime objects can't be represented as a Time, normalize_time_types turns all the objects into DateTime instances. This code is run on a system with a 32-bit time counter:
mixed_array << DateTime.civil(1776, 7, 4) normalize_time_types(mixed_array).collect { |x| x.to_s } # => ["2006-03-18T22:17:10-0500", "2006-03-18T22:17:10-0500", # => "1776-07-04T00:00:00Z"]
See Also
- Recipe 3.1, "Finding Today's Date"