ChatGPT解决这个技术问题 Extra ChatGPT

Difference between DateTime and Time in Ruby

What's the difference between DateTime and Time classes in Ruby and what factors would cause me to choose one or the other?

The docs has a section, explaining when to use which.

t
tadman

Newer versions of Ruby (2.0+) do not really have significant differences between the two classes. Some libraries will use one or the other for historical reasons, but new code does not necessarily need to be concerned. Picking one for consistency is probably best, so try and mesh with what your libraries expect. For example, ActiveRecord prefers DateTime.

In versions prior to Ruby 1.9 and on many systems Time is represented as a 32-bit signed value describing the number of seconds since January 1, 1970 UTC, a thin wrapper around a POSIX-standard time_t value, and is bounded:

Time.at(0x7FFFFFFF)
# => Mon Jan 18 22:14:07 -0500 2038
Time.at(-0x7FFFFFFF)
# => Fri Dec 13 15:45:53 -0500 1901

Newer versions of Ruby are able to handle larger values without producing errors.

DateTime is a calendar-based approach where the year, month, day, hour, minute and second are stored individually. This is a Ruby on Rails construct that serves as a wrapper around SQL-standard DATETIME fields. These contain arbitrary dates and can represent nearly any point in time as the range of expression is typically very large.

DateTime.new
# => Mon, 01 Jan -4712 00:00:00 +0000

So it's reassuring that DateTime can handle blog posts from Aristotle.

When choosing one, the differences are somewhat subjective now. Historically DateTime has provided better options for manipulating it in a calendar fashion, but many of these methods have been ported over to Time as well, at least within the Rails environment.


So should I always use DateTime?
If you're working with dates, I'd say use DateTime. Time is convenient for representing things such as current time of day, or points in the near future such as 10.minutes.from_now. The two have a lot in common, though as noted DateTime can represent a much wider range of values.
I believe that's because Ruby switches to arbitrary-length Bignum from 32-bit Fixnum when it experiences an overflow. Numbers outside of that range may not be supported by external applications. Yes, in 2038 we're basically screwed until we can all agree on a proper 64-bit time format. Jury is still out.
This answer predates 1.9.2. Ignore everything it says about the posix limitations of Time, and make your choice based on the APIs of Time and DateTime.
This answer is outdated right? Now it is recommended to use Time or Rail's ActiveSupport::WithTimeZone. No longer do you need to use DateTime. It is there for backward compatibility.
m
michau

[Edit July 2018]

All of the below still holds true in Ruby 2.5.1. From the reference documentation:

DateTime does not consider any leap seconds, does not track any summer time rules.

What hasn't been noted in this thread before is one of the few advantages of DateTime: it is aware of calendar reforms whereas Time is not:

[…] Ruby's Time class implements a proleptic Gregorian calendar and has no concept of calendar reform […].

The reference documentation concludes with the recommendation to use Time when exclusively dealing with near-past, current or future dates/times and only use DateTime when, for example, Shakespeare's birthday needs to be accurately converted: (emphasis added)

So when should you use DateTime in Ruby and when should you use Time? Almost certainly you'll want to use Time since your app is probably dealing with current dates and times. However, if you need to deal with dates and times in a historical context you'll want to use DateTime […]. If you also have to deal with timezones then best of luck - just bear in mind that you'll probably be dealing with local solar times, since it wasn't until the 19th century that the introduction of the railways necessitated the need for Standard Time and eventually timezones.

[/Edit July 2018]

As of ruby 2.0, most of the information in the other answers is out of date.

In particular, Time is now practically unbound. It can be more or less than even 63 bits away from Epoch:

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
irb(main):002:0> Time.at(2**62-1).utc # within Integer range
=> 146138514283-06-19 07:44:38 UTC
irb(main):003:0> Time.at(2**128).utc # outside of Integer range
=> 10783118943836478994022445751222-08-06 08:03:51 UTC
irb(main):004:0> Time.at(-2**128).utc # outside of Integer range
=> -10783118943836478994022445747283-05-28 15:55:44 UTC

The only consequence of using larger values should be performance, which is better when Integers are used (vs. Bignums (values outside of Integer range) or Rationals (when nanoseconds are tracked)):

Since Ruby 1.9.2, Time implementation uses a signed 63 bit integer, Bignum or Rational. The integer is a number of nanoseconds since the Epoch which can represent 1823-11-12 to 2116-02-20. When Bignum or Rational is used (before 1823, after 2116, under nanosecond), Time works slower as when integer is used. (http://www.ruby-doc.org/core-2.1.0/Time.html)

In other words, as far as I understand, DateTime no longer covers a wider range of potential values than Time.

In addition, two previously unmentioned restrictions of DateTime should probably be noted:

DateTime does not consider any leapseconds, does not track any summer time rules. (http://www.ruby-doc.org/stdlib-2.1.0/libdoc/date/rdoc/Date.html#class-Date-label-DateTime)

First, DateTime has no concept of leap seconds:

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
irb(main):002:0> require "date"
=> true
irb(main):003:0> t = Time.new(2012,6,30,23,59,60,0)
=> 2012-06-30 23:59:60 +0000
irb(main):004:0> dt = t.to_datetime; dt.to_s
=> "2012-06-30T23:59:59+00:00"
irb(main):005:0> t == dt.to_time
=> false
irb(main):006:0> t.to_i
=> 1341100824
irb(main):007:0> dt.to_time.to_i
=> 1341100823

For the above example to work with Time, the OS needs to support leap seconds and timezone information needs to be set correctly, e.g. through TZ=right/UTC irb (on many Unix systems).

Second, DateTime has very limited understanding of time zones and in particular has no concept of daylight savings. It pretty much handles time zones as simple UTC + X offsets:

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
irb(main):002:0> require "date"
=> true
irb(main):003:0> t = Time.local(2012,7,1)
=> 2012-07-01 00:00:00 +0200
irb(main):004:0> t.zone
=> "CEST"
irb(main):005:0> t.dst?
=> true
irb(main):006:0> dt = t.to_datetime; dt.to_s
=> "2012-07-01T00:00:00+02:00"
irb(main):007:0> dt.zone
=> "+02:00"
irb(main):008:0> dt.dst?
NoMethodError: undefined method `dst?' for #<DateTime:0x007f34ea6c3cb8>

This may cause trouble when times are entered as DST and then converted into a non-DST time zone without keeping track of the correct offsets outside of DateTime itself (many operating systems may actually already take care of this for you).

Overall, I'd say that nowadays Time is the better choice for most applications.

Also note an important difference on addition: when you add a number to a Time object, it is counted in seconds, but when you add a number to a DateTime, it is counted in days.


Is this still true in 2018?
It is still true in Ruby 2.5.1. ruby-doc.org/stdlib-2.5.1/libdoc/date/rdoc/DateTime.html: "DateTime does not consider any leap seconds, does not track any summer time rules." However, note that DateTime has advantages when you need to deal with pre-Gregorian calendar dates/times. This wasn't mentioned here before but is documented in the reference documentation: "[…] Ruby's Time class implements a proleptic Gregorian calendar and has no concept of calendar reform […]." I will edit my answer to provide some more details.
Time has no concept of leap seconds either, so it's not different from DateTime. I don't know where you ran your examples, but I tried Time.new(2012,6,30,23,59,60,0) in different Ruby versions, from 2.0 to 2.7, and always got 2012-07-01 00:00:00 +0000.
@michau Whether Time supports leap seconds or not depends on your OS & timezone configuration. For example: TZ=right/UTC ruby -e 'p Time.new(2012,6,30,23,59,60,0)' => 2012-06-30 23:59:60 +0000 whereas TZ=UTC ruby -e 'p Time.new(2012,6,30,23,59,60,0)' => 2012-07-01 00:00:00 +0000.
Note that The Ruby Style Guide now states Don’t use DateTime unless you need to account for historical calendar reform
R
Rhubarb

I think the answer to "what's the difference" is one of the unfortunate common answers to this question in the Ruby standard libraries: the two classes/libs were created differently by different people at different times. It's one of the unfortunate consequences of the community nature of Ruby's evolution compared to carefully planned development of something like Java. Developers want new functionality but don't want to step on existing APIs so they just create a new class - to the end user there's no obvious reason for the two to exist.

This is true for software libraries in general: often the reason some code or API is the way it is turns out to be historical rather than logical.

The temptation is to start with DateTime because it seems more generic. Date... and Time, right? Wrong. Time also does dates better, and in fact can parse timezones where DateTime can't. Also it performs better.

I've ended up using Time everywhere.

To be safe though, I tend to allow for DateTime arguments to be passed into my Timey APIs, and either convert. Also if I know that both have the method I'm interested in I accept either, like this method I wrote for converting times to XML (for XMLTV files)

# Will take a date time as a string or as a Time or DateTime object and
# format it appropriately for xmtlv. 
# For example, the 22nd of August, 2006 at 20 past midnight in the British Summertime
# timezone (i.e. GMT plus one hour for DST) gives: "20060822002000 +0100"
def self.format_date_time(date_time)
  if (date_time.respond_to?(:rfc822)) then
    return format_time(date_time)
  else 
    time = Time.parse(date_time.to_s)
    return format_time(time)
  end    
end

# Note must use a Time, not a String, nor a DateTime, nor Date.
# see format_date_time for the more general version
def self.format_time(time)
  # The timezone feature of DateTime doesn't work with parsed times for some reason
  # and the timezone of Time is verbose like "GMT Daylight Saving Time", so the only
  # way I've discovered of getting the timezone in the form "+0100" is to use 
  # Time.rfc822 and look at the last five chars
  return "#{time.strftime( '%Y%m%d%H%M%S' )} #{time.rfc822[-5..-1]}"
end

Also, Time.new and DateTime.new are dealing with time zone differently. I'm on GMT+7, so Time.new(2011, 11, 1, 10, 30) produces 2011-11-01 10:30:00 +0700 while DateTime.new(2011, 11, 1, 10, 30) produces Tue, 01 Nov 2011 10:30:00 +0000.
And as we all know, the carefully planned development of Java resulted in exclusively simple, logical APIs.
@PhươngNguyễn Hi, any ideas how/why this difference in time zones? where is it taking the offset from ?
Y
Yuriy Kharchenko

I found such things like parsing and calculating the beginning/end of a day in different timezones are easier to do with DateTime, assuming you are using the ActiveSupport extensions.

In my case I needed to calculate the end of the day in a user's timezone (arbitrary) based on the user's local time which I received as a string, e.g. "2012-10-10 10:10 +0300"

With DateTime it's as simple as

irb(main):034:0> DateTime.parse('2012-10-10 10:10 +0300').end_of_day
=> Wed, 10 Oct 2012 23:59:59 +0300
# it preserved the timezone +0300

Now let's try it the same way with Time:

irb(main):035:0> Time.parse('2012-10-10 10:10 +0300').end_of_day
=> 2012-10-10 23:59:59 +0000
# the timezone got changed to the server's default UTC (+0000), 
# which is not what we want to see here.

Actually, Time needs to know the timezone before parsing (also note it's Time.zone.parse, not Time.parse):

irb(main):044:0> Time.zone = 'EET'
=> "EET"
irb(main):045:0> Time.zone.parse('2012-10-10 10:10 +0300').end_of_day
=> Wed, 10 Oct 2012 23:59:59 EEST +03:00

So, in this case it's definitely easier to go with DateTime.


Using this in production now, are there any downsides to the technique?
DateTime doesn't take into account daylight saving time. Thus on saving time change, it won't work correctly. DateTime.parse('2014-03-30 01:00:00 +0100').end_of_day produces Sun, 30 Mar 2014 23:59:59 +0100, but Time.zone = 'CET'; Time.zone.parse('2014-03-30 01:00:00').end_of_day produces Sun, 30 Mar 2014 23:59:59 CEST +02:00 (CET=+01:00, CEST=+02:00 - notice the offset changed). But to do this, you need more information on user's timezone (not just offset but also whether saving time is used).
It may be stating the obvious, but Time.zone.parse is very useful when parsing times with different zones - it forces you to think about which zone you should be using. Sometimes, Time.find_zone works even better.
@Petr''Bubák''Šedivý DateTime did parse the offset that you gave it, which was +0100. You didn't give Time an offset, but a timezone ("CET" doesn't describe an offset, it's the name of a timezone). Timezones can have different offsets throughout the year, but an offset is an offset and it's always the same.
g
gr8scott06

Consider how they handle timezones differently with custom instantiations:

irb(main):001:0> Time.new(2016,9,1)
=> 2016-09-01 00:00:00 -0400
irb(main):002:0> DateTime.new(2016,9,1)
=> Thu, 01 Sep 2016 00:00:00 +0000
irb(main):003:0> Time.new(2016,9,1).to_i
=> 1472702400
irb(main):004:0> DateTime.new(2016,9,1).to_i
=> 1472688000

This can be tricky when creating time ranges, etc.


Only seems to happen with plain ruby (i.e. no Rails)
P
Paul van Leeuwen

In addition to the answer of Niels Ganser you might consider this argument:

Note that The Ruby Style Guide quite clearly states a position on this:

No DateTime Don’t use DateTime unless you need to account for historical calendar reform - and if you do, explicitly specify the start argument to clearly state your intentions. # bad - uses DateTime for current time DateTime.now # good - uses Time for current time Time.now # bad - uses DateTime for modern date DateTime.iso8601('2016-06-29') # good - uses Date for modern date Date.iso8601('2016-06-29') # good - uses DateTime with start argument for historical date DateTime.iso8601('1751-04-23', Date::ENGLAND)


T
Tim Diekmann

It seems that in some cases the behavior is very different:

Time.parse("Ends from 28 Jun 2018 12:00 BST").utc.to_s

"2018-06-28 09:00:00 UTC"

Date.parse("Ends from 28 Jun 2018 12:00 BST").to_time.utc.to_s

"2018-06-27 21:00:00 UTC"

DateTime.parse("Ends from 28 Jun 2018 12:00 BST").to_time.utc.to_s

"2018-06-28 11:00:00 UTC"


That's an interesting observation, but it should be explained why it happens. Date (unsurprisingly) parses only dates, and when a Date is converted to Time, it always uses midnight in the local timezone as the time. The difference between Time and TimeDate stems from the fact that Time doesn't understand BST, which is surprising, given that timezones are usually handled more correctly by Time (with regard to daylight saving time, for example). So in this case, only DateTime parses the whole string correctly.