Ruby中DateTime和Time的区别

242

DateTimeTime类在Ruby中有什么区别,我该如何选择使用哪个?


文档中有一个章节,解释了何时应该使用哪个(DateTime和Time)。 - x-yuri
7个回答

186

新版本的Ruby(2.0+)在这两个类之间没有显着的区别。出于历史原因,一些库将使用其中之一,但新代码不一定需要关注。为了保持一致性,最好选择一个类,并尽量与你的库相匹配。例如,ActiveRecord更喜欢DateTime。

在Ruby 1.9之前的版本和许多系统中,Time被表示为描述自1970年1月1日UTC以来秒数的32位有符号值,它是围绕POSIX标准中time_t值的薄包装,并被限制:

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

新版本的Ruby能够处理更大的值而不会出错。

DateTime是一种基于日历的方法,其中年、月、日、小时、分钟和秒分别存储。这是一个Ruby on Rails构造,它作为SQL标准DATETIME字段的包装器。它们包含任意日期,并且通常可以表示几乎任何时间点,因为表达范围通常非常大。

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

DateTime可以处理来自亚里士多德的博客文章,这让人感到放心。

在选择DateTime和Time之间,现在差异有些主观。历史上,DateTime提供了更好的选项来按日历方式操作它,但是在Rails环境中,其中许多方法也已经移植到了Time中。


6
我应该总是使用 DateTime 吗? - Tom Lehman
4
如果您需要处理日期,我建议使用DateTime。Time适合表示当前时间或未来某个时刻(例如10分钟后),它更加方便。这两者有很多共同点,但是DateTime可以表示更广泛的值范围。 - tadman
3
我认为这是因为当Ruby发生整数溢出时,它会从32位Fixnum转换为任意长度的Bignum。超出该范围的数字可能不受外部应用程序支持。是的,在2038年之前,我们基本上陷入了困境,除非我们可以就适当的64位时间格式达成一致。目前还没有定论。 - tadman
28
这个答案早于1.9.2版本。忽略它关于Time在posix方面的限制所说的一切内容,根据Time和DateTime的API来做出选择。 - Ben Nagy
9
这个回答已经过时了吧?现在建议使用 Time 或 Rail 的 ActiveSupport::WithTimeZone。不再需要使用 DateTime,DateTime 只是为了向后兼容而存在。 - Donato
显示剩余8条评论

113

[2018年7月编辑]

所有以下内容在Ruby 2.5.1中仍然有效。根据参考文档:

DateTime不考虑任何闰秒,也不跟踪任何夏令时规则。

此前这个主题中没有注意到的是DateTime的少数优点之一:它知道历法改革,而Time则不知道:

[…] Ruby的Time类实现一个连续的Gregorian日历,并没有历法改革的概念 […].

参考文献总结建议,在只处理近期、当前或未来日期/时间时使用Time,只有在需要准确转换例如莎士比亚的生日时才使用DateTime:(强调添加)

几乎肯定你需要使用 Time,因为你的应用程序可能涉及当前日期和时间。然而,如果你需要处理历史背景下的日期和时间,你将需要使用 DateTime[...] 如果你还需要处理时区,那么祝你好运 - 请记住,你可能会处理当地的太阳时间,因为直到19世纪铁路的引入才需要标准时间和最终的时区。[/编辑于2018年7月]截至ruby 2.0,其他答案中的大部分信息已经过时。特别是,Time现在几乎是无限制的。它可以比Epoch更多或者少于63位:
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

使用更大的值唯一的后果应该是性能,当使用Integer时性能更好(而不是Bignum(超出 Integer 范围的值)或Rational(跟踪纳秒时)):
自 Ruby 1.9.2 以来,时间实现使用带符号的 63 位整数、Bignum 或 Rational。整数是自 Epoch 以来的纳秒数,可以表示 1823-11-12 至 2116-02-20。当使用 Bignum 或 Rational(在 1823 年之前、2116 年之后、纳秒以下)时,时间运行速度较慢,与使用整数时相比。(http://www.ruby-doc.org/core-2.1.0/Time.html
换句话说,据我所知,DateTime 不再覆盖比Time 更广泛的潜在值。 此外,还应该注意DateTime的两个先前未提到的限制:
首先,DateTime不考虑闰秒:。它也不跟踪任何夏令时规则。(http://www.ruby-doc.org/stdlib-2.1.0/libdoc/date/rdoc/Date.html#class-Date-label-DateTime)
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

对于上面的示例要与Time一起使用,操作系统需要支持闰秒,并且需要正确设置时区信息,例如通过TZ=right/UTC irb(在许多Unix系统上)。
其次,DateTime对时区的理解非常有限,特别是没有夏令时概念。它基本上将时区处理为简单的UTC + X偏移量:
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>

这可能会在将夏令时时间输入并转换为非夏令时时区时引起麻烦,如果没有跟踪正确的偏移量(许多操作系统实际上可能已经为您处理了此问题),请注意保留DateTime之外的正确偏移量。
总的来说,我认为现在对于大多数应用程序来说,Time是更好的选择。
还要注意加法的一个重要区别:当您将数字添加到Time对象时,它以秒为计数,但当您将数字添加到DateTime时,它以天为计数。

这在2018年仍然正确吗? - Qqwy
2
在 Ruby 2.5.1 中这仍然是正确的。https://ruby-doc.org/stdlib-2.5.1/libdoc/date/rdoc/DateTime.html:"DateTime 不考虑任何闰秒,也不跟踪任何夏令时规则。"但是,需要注意的是当你需要处理公历改革前的日期/时间时,DateTime 有其优势。这之前没有提到过,但在参考文献中有所记录:"[...] Ruby 的 Time 类实现了一个推算格里高利日历,并且没有日历改革的概念 [...]." 我将编辑我的答案,提供更多细节。 - Niels Ganser
“Time”也没有闰秒的概念,所以它与“DateTime”没有区别。我不知道你在哪里运行你的示例,但我在不同的Ruby版本(从2.0到2.7)中尝试了Time.new(2012,6,30,23,59,60,0),并且始终得到 2012-07-01 00:00:00 +0000 - michau
1
@michau 无论Time是否支持闰秒都取决于您的操作系统和时区配置。例如:TZ=right/UTC ruby -e 'p Time.new(2012,6,30,23,59,60,0)' => 2012-06-30 23:59:60 +0000,而TZ=UTC ruby -e 'p Time.new(2012,6,30,23,59,60,0)' => 2012-07-01 00:00:00 +0000 - Niels Ganser
@NielsGanser 很好,谢谢!我建议修改一下以澄清这个点。 - michau
1
请注意,[Ruby样式指南](https://rubystyle.guide/#no-datetime)现在已经声明**_除非需要考虑历史日历改革,否则不要使用DateTime_**。 - pjvleeuwen

45
我认为“有什么区别”这个问题的答案是Ruby标准库中常见的不幸答案之一:两个类/库是由不同的人在不同的时间创建的。这是Ruby演进的社区性质相比于像Java这样精心规划的开发的不幸后果之一。开发者想要新功能,但又不想影响现有API,所以他们只是创建了一个新类 - 对于最终用户来说,这两个类存在的原因并不明显。
总的来说,这对软件库来说是真实的:通常某些代码或API之所以是这样的,原因往往是历史而非逻辑。
诱惑是从DateTime开始,因为它似乎更通用。日期...和时间,对吗?错了。Time也更好地处理日期,并且实际上可以解析DateTime无法解析的时区。此外,它的性能更好。
我最终到处都使用Time。
但为了安全起见,我倾向于允许将DateTime参数传递到我的Timey API中,并进行转换。此外,如果我知道两者都具有我感兴趣的方法,我会接受任何一个,就像我为将时间转换为XML(用于XMLTV文件)编写的这个方法一样。
# 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

8
同时,Time.new和DateTime.new在处理时区时有所不同。因为我处于GMT+7时区,所以Time.new(2011, 11, 1, 10, 30)会产生2011-11-01 10:30:00 +0700,而DateTime.new(2011, 11, 1, 10, 30)会产生Tue, 01 Nov 2011 10:30:00 +0000 - Phương Nguyễn
23
正如我们所知,Java经过精心规划的开发导致其API非常简单、逻辑清晰。 - pje
@PhươngNguyễn 你好,有没有任何关于时区差异的想法和原因?偏移量从哪里获取? - Joel Blum

10

我发现在不同时区解析和计算一天的开始/结束时间使用DateTime会更容易,假设您正在使用ActiveSupport扩展

在我的情况下,我需要根据用户的本地时间(字符串形式,例如 "2012-10-10 10:10 +0300")来计算用户所在时区(任意)的当天结束时间。

使用DateTime非常简单:

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

现在让我们用时间来尝试同样的方法:

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.

实际上,在解析时间之前,需要知道时区(注意使用 Time.zone.parse 而不是 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

所以,在这种情况下,使用DateTime肯定更容易。


1
现在在生产中使用这个技术,是否存在任何缺点? - Alex Moore-Niemi
1
DateTime 不考虑夏令时。因此,在夏令时更改时,它将无法正确工作。DateTime.parse('2014-03-30 01:00:00 +0100').end_of_day 会产生 Sun, 30 Mar 2014 23:59:59 +0100,但是 Time.zone = 'CET'; Time.zone.parse('2014-03-30 01:00:00').end_of_day 会产生 Sun, 30 Mar 2014 23:59:59 CEST +02:00(CET=+01:00,CEST=+02:00 - 注意偏移量已更改)。但要做到这一点,您需要更多有关用户时区的信息(不仅仅是偏移量,还包括是否使用夏令时)。 - Petr ''Bubák'' Šedivý
1
这可能是显而易见的,但是在解析具有不同时区的时间时,“Time.zone.parse”非常有用-它强制您考虑应该使用哪个时区。有时,“Time.find_zone”效果更好。 - prusswan
1
@Petr''Bubák''Šedivý DateTime 已经解析了您提供的偏移量,即 +0100。您没有给 Time 提供偏移量,而是时区(“CET”不描述偏移量,它是时区的名称)。时区在一年中可能具有不同的偏移量,但偏移量始终是相同的。 - Mecki

5
除了Niels Ganser的答案之外,你可能还要考虑这个观点:
请注意Ruby风格指南非常明确地表明了对此的立场:

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)

4

考虑它们如何以不同的方式处理时区问题:

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

这在创建时间范围等方面可能会有些棘手。

似乎只在纯 Ruby(即没有 Rails)中发生。 - deprecated

1

在某些情况下,行为非常不同:

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"(世界标准时间)

这是一个有趣的观察,但需要解释为什么会发生这种情况。Date(毫不意外地)仅解析日期,并且当 Date 转换为 Time 时,始终使用本地时区的午夜作为时间。TimeTimeDate 之间的差异源于 Time 不理解英国夏令时,这令人惊讶,因为通常 Time 更正确地处理时区(例如考虑到夏令时)。因此,在这种情况下,只有 DateTime 正确解析整个字符串。 - michau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接