商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > Ruby日期时间的比较,日期转换等时间日期处理方法大全

Ruby日期时间的比较,日期转换等时间日期处理方法大全

  发布于2026-06-29 阅读(0)

扫一扫,手机访问

Ruby 中和日期时间相关的类主要有三个:DateTimeDateTime。它们各有所长,也各有局限,但组合起来几乎能覆盖所有日期时间的处理需求。

Ruby日期时间的比较,日期转换等时间日期处理方法大全

Date 只负责日期,不碰时间
Time 日期时间都能管
DateTime 是 Date 的子类,补齐了时间部分

要使用 Date 和 DateTime,只需要导入 date 库;要使用 Time,导入 time 库即可(虽然 Time 本身不用导也能用,但导入后能获得更多方法)。

require 'date'   # 提供 Date 和 DateTime 类
require 'time'   # 提供 Time 类(可直接使用,但导入后有更多方法)

日常开发中,常见的日期时间操作无非是这些:

  • 创建(构建)日期、时间对象
  • 在字符串和日期时间对象之间互相转换
  • 比较两个日期时间
  • 对日期时间做加减运算
  • 处理时区
  • 等等……

这三个类的方法有不少重叠。从实际测试来看,DateTime 是效率最高的那个,如果你在性能敏感的场景下需要频繁构建日期时间对象,可以优先考虑它。

下面就把它们放在一起,按功能挨个聊透。

Ruby 构建日期时间对象

这三个类都能直接构造日期时间对象,不过 Date 只能构造日期,传了时间部分也会被忽略。

Date 构造日期对象

Date 只造日期,不认时间,自然也不需要时区。

# 1. 当前日期:Date.today
>> Date.today
=> #

>> puts Date.today
2019-08-05

# 2. 指定日期:Date.new
## 可以构造 2038 年之后的日期(Time 类就有这个限制)
## 没给月、日,默认为 1
## 完全不传参,默认是 -4712 年 1 月 1 日,基本用不上
## 如果传了时间部分,会直接报错
>> Date.new(2018,3,20)
=> #
>> Date.new(2078,3,20)
=> #
>> Date.new(1900)
=> #
>> Date.new
=> #

# 3. 解析字符串:Date.parse
## 自动识别常见格式,时间部分会被忽略
## 两位年份:>=69 的加 1900,0~68 的加 2000
>> Date.parse('2007/09/12')
=> #
>> Date.parse('2007/9/12')
=> #
>> Date.parse('2007-9-12')
=> #
>> Date.parse('2007-9-12 12:30:59')
=> #

>> Date.parse('08-9-12')
=> #
>> Date.parse('68-9-12')
=> #
>> Date.parse('69-9-12')
=> #

# 4. 按格式解析:Date.strptime
>> Date.strptime('2001-02-03', '%Y-%m-%d')
=> #
>> Date.strptime('02-03-2001', '%m-%d-%Y')
=> #

Time 构造日期时间对象

Time 的常用构造方法有 new(别名 now)、at、local(别名 mktime)、parse。注意,Time 没有 strptime 方法。

# 1. new() / now():当前时间
## 也可以指定参数,无参时等价于 now
>> Time.now
=> 2019-08-05 14:12:30 +0800
>> Time.new
=> 2019-08-05 14:12:31 +0800
>> Time.new(2007,11,6,17,10,0, "+08:00")  # 指定时区
=> 2007-11-06 17:10:00 +0800

# 2. at():从 epoch 转成时间对象
## 支持时区、小数秒、毫秒、微秒、纳秒
>> Time.at(1553488199)
=> 2019-03-25 12:29:59 +0800
>> Time.at(1553488199, in: '+08:00')
=> 2019-03-25 12:29:59 +0800
>> Time.at(1553488199.3).usec
=> 299999
>> Time.at(1553488199,123.345,:millisecond).usec
=> 123344
>> Time.at(1553488199,123.345).nsec
=> 123344
>> Time.at(1553488199,123.345,:usec).nsec
=> 123344
>> Time.at(1553488199,123,:nsec).nsec
=> 123

# 3. mktime / local:构建本地时区时间
## 要构建 UTC 时间,用 gm() 或 utc()
>> Time.mktime(2009,10,23,14,3,6)
=> 2009-10-23 14:03:06 +0800
>> Time.mktime(2009,10,23)
=> 2009-10-23 00:00:00 +0800
>> Time.mktime(2009,10)
=> 2009-10-01 00:00:00 +0800

# 4. parse:字符串转时间
>> Time.parse("2009/12/25 12:29:59")
=> 2009-12-25 12:29:59 +0800
>> Time.parse("2009/12/25")
=> 2009-12-25 00:00:00 +0800
>> Time.parse("2009/12")
=> 2009-12-01 00:00:00 +0800
>> Time.parse("2009/12/25 12:29:59 +00:00")
=> 2009-12-25 12:29:59 +0000
>> Time.parse('2009-07-12 16:32:40.00123').nsec
=> 1230000

上面几种 Time 方法构造出来的对象,默认时区是 +08:00(东八区),所以不指定时区也没问题。但下面 DateTime 的默认时区是 +00:00,通常需要手动指定。

DateTime 构造日期时间对象

DateTime 是 Date 的子类,重写了部分方法,参数略有不同。常用构造器有 new、now、parse 和 strptime。另外,DateTime 是包含纳秒的。

# 1. new()
>> DateTime.new(2009,1,2,3,4,5)
=> #
>> DateTime.new(2009,1,2,3,4,5,'+7')
=> #
>> DateTime.new(2009,1,2,3,4,5,'+08:00')
=> #
>> DateTime.new(2009,1,2,3,4,5.3,'+08:00')  # 小数秒
=> #
>> DateTime.new(2009,1,2,3,4,5.3,'+08:00').sec_fraction
=> (3/10)

# 2. now()
>> DateTime.now
=> #

# 3. parse()
>> DateTime.parse('2009-12-20 12:03:30')
=> #
>> DateTime.parse('2009-12-20 12:03:30 +8')
=> #

# 4. strptime()
>> DateTime.strptime('2009-12-20 12:03:30 +8','%Y-%m-%d %H:%M:%S %z')
=> #

日期时间有效性检测

有些日期时间本身是无效的,比如 11 月 31 号、第 61 秒(除非是闰秒)。不同类的处理方式不太一样。

Time 能检测“超范围”的错误(比如 11 月 32 号会报错),但对“范围内但无效”的日期只会自动进位——比如 11 月 31 号会被当成 12 月 1 号。

# 有效日期
>> Time.new(2007,11,30,17,10,30)
=> 2007-11-30 17:10:30 +0800

# 范围内无效日期,进位
>> Time.new(2007,11,31,17,10,30)
=> 2007-12-01 17:10:30 +0800

# 有效秒
>> Time.new(2007,11,31,17,10,59)
=> 2007-12-01 17:10:59 +0800

# 第 61 秒进位
>> Time.new(2007,11,31,17,10,60)
=> 2007-12-01 17:11:00 +0800

# 错误秒,报错
>> Time.new(2007,11,31,17,10,61)
ArgumentError: sec out of range

# 错误日期,报错
>> Time.new(2007,11,32,17,10,59)
ArgumentError: argument out of range

Time 的这个特性有时挺方便,但做有效性校验时就容易翻车。好在 Date 和 DateTime 很严格——只要是无效的日期或时间,直接抛异常。

# Date 检测无效日期
>> Date.new(2007,11,30)  # 正常
>> Date.new(2007,11,31)  # 报错
ArgumentError: invalid date

# DateTime 检测无效日期和时间
>> DateTime.new(2007,11,31,11,12,13)  # 报错
>> DateTime.new(2007,11,30,11,12,60)  # 报错

既然它们会报错,那就可以用异常捕获来写一个通用的有效性检测方法。比如给 Time 加一个 valid? 方法,背后借用 DateTime 来做检测:

class Time
  def self.valid?(y,m=1,d=1,H=0,M=0,S=0,us=0)
    require 'date'
    begin
      dt = DateTime(y,m,d,H,M,S,us)
    rescue
      return nil
    end
    dt
  end
end

Date、Time、DateTime 对象之间的类型转换

这三个类各有长短,实际开发中经常需要在它们之间切换。好在它们都提供了 to_dateto_timeto_datetime 三个方法。

# Date 对象转换
>> Date.today.to_date
>> Date.today.to_time      # 得到 2019-08-05 00:00:00 +0800
>> Date.today.to_datetime  # 得到 2019-08-05T00:00:00+00:00

# Time 对象转换
>> Time.now.to_date
>> Time.now.to_time
>> Time.now.to_datetime

# DateTime 对象转换
>> DateTime.now.to_date
>> DateTime.now.to_time
>> DateTime.now.to_datetime

这里有个细节值得注意:Date 不包含时间和时区,所以从 Time/DateTime 转成 Date 后会丢失时间部分;反过来,Date 转成 Time 时默认时区是 +08:00,转成 DateTime 时默认时区是 +00:00。但 Time 和 DateTime 之间互转不会改变时区。

d = Date.new(2019,5,23)
d.to_time         #=> 2019-05-23 00:00:00 +0800
d.to_datetime.to_s #=> "2019-05-23T00:00:00+00:00"

日期时间和字符串、数值之间的转换

这也是高频操作。

  • 日期时间→字符串to_s 输出固定格式,strftime 自定义格式
  • 日期时间→数值:只对包含时间部分的类有意义(Date 不行),通常转成 epoch 时间,支持整数、浮点数、分数
  • 字符串→日期时间:前面构造对象时已经覆盖了
  • 数值→日期时间:同样在构造部分说过

日期时间对象还能转成数组。

先看 Time:

# 转字符串
>> Time.now.to_s
=> "2019-08-05 15:29:49 +0800"

# 转整数 epoch
>> Time.now.to_i
=> 1564990192

# 转浮点数(微秒级)
>> Time.now.to_f
=> 1564990197.671165

# 转分数
>> Time.now.to_r
=> (3912475500217301/2500000)

# 转数组: [sec,min,hour,day,month,year,wday,yday,isdst,zone]
>> Time.now.to_a
=> [4, 30, 15, 5, 8, 2019, 1, 217, false, "DST"]

注意 Time 的时区显示为 DST、CST 这样的缩写,而 DateTime 显示的是 +08:00 这种偏移量。

Date 和 DateTime 只支持 to_s,其他转换需要先 to_time 再调 to_i 等。

>> Date.today.to_s
=> "2019-08-05"

>> Date.today.to_time.to_i
=> 1564934400

>> DateTime.now.to_s
=> "2019-08-05T15:55:05+08:00"
>> DateTime.now.to_time.to_f
=> 1564991712.1981702

strftime 方法

strptime 是把字符串按格式解析成日期时间,strftime 正好反过来——把日期时间按格式输出成字符串。Date、Time、DateTime 都支持这个方法。

>> d = Date.today
>> d.strftime("%Y-%m-%d %H:%M:%S")
=> "2019-08-05 00:00:00"

>> dt = DateTime.now
>> dt.strftime("%Y-%m-%d %H:%M:%S")
=> "2019-08-05 16:00:03"

>> t = Time.now
>> t.strftime("%Y-%m-%d %H:%M:%S")
=> "2019-08-05 16:01:07"

百分号后面的占位符和 strptime 是统一的。以下是常用格式指令(摘抄自官方文档):

%
Flags:
-  don't pad a numerical output.
_  use spaces for padding.
0  use zeros for padding.
^  upcase the result string.
#  change case.

Format directives (部分):
%Y - 4位年份
%m - 两位数月份 (01..12)
%d - 两位数月中天 (01..31)
%H - 24小时 (00..23)
%M - 分钟 (00..59)
%S - 秒 (00..60)
%L - 毫秒 (000..999)
%N - 小数秒 (默认9位纳秒)
%z - 时区偏移 (+0900)
%:z - 时区偏移 (+09:00)
%F - ISO 8601 日期 (%Y-%m-%d)
%T - 24小时时间 (%H:%M:%S)
...(完整列表可参考 Ruby 官方手册)

查看日期时间各部分信息

比如获取年份、月份、秒数、时区等。Date 和 DateTime 提供了这些方法:

  • year
  • month / mon
  • day
  • hour
  • minute / min
  • second / sec
  • sec_fraction / second_fraction(返回分数表示的小数秒)
  • zone(时区)
  • cweek(一年中的第几周,1-53)
  • yday(一年中的第几天,1-366)
  • mday(一个月中的第几天,1-31)
  • wday(一周中的第几天,0-6,0=周日)
  • day_fraction(一天已过去几分之几,比如中午12点是 1/2)
  • 以及 monday?tuesday? 等判断方法
  • leap?(是否闰年)

Time 除了上述方法(但不支持 sec_fraction/second_fraction),还额外提供了:

  • subsec(等价于 sec_fraction,分数表示小数秒)
  • usec(微秒)
  • nsec(纳秒)
# DateTime 示例
>> dt = DateTime.new(2009,7,12,16,32,40.00123)
>> dt.year           #=> 2009
>> dt.mon            #=> 7
>> dt.day            #=> 12
>> dt.cweek          #=> 28
>> dt.hour           #=> 16
>> dt.sec_fraction   #=> (123/100000)
>> dt.yday           #=> 193
>> dt.wday           #=> 0  周日
>> dt.sunday?        #=> true

# DateTime 的 day_fraction
>> DateTime.new(2009,7,12).day_fraction              #=> (0/1)
>> DateTime.new(2009,7,12,12).day_fraction           #=> (1/2)
>> DateTime.new(2009,7,12,23,59,59).day_fraction    #=> (86399/86400)

# Time 示例
>> t = Time.parse('2009-07-12 16:32:40.00123')
>> t.year      #=> 2009
>> t.subsec    #=> (123/100000)
>> t.usec      #=> 1230
>> t.nsec      #=> 1230000

日期时间的运算

加几天、减几小时,这类需求非常常见。Ruby 的运算符和方法让这件事变得很轻松。

日期时间加减法运算

Date/DateTime/Time 都实现了 +- 运算符,返回新的对象:

  • 对于 Time:加减的是秒数(可以是小数)
  • 对于 Date/DateTime:加减的是天数(也可以是小数)
# Date/DateTime 示例
>> d = Date.parse('2019-02-26')
>> dt = DateTime.parse('2019-02-26 12:30:30')
>> d + 1          #=> 2019-02-27
>> d + 3          #=> 2019-03-01
>> dt + 3          #=> 2019-03-01T12:30:30+00:00
>> dt + 2.5        #=> 2019-03-01T00:30:30+00:00

# Time 示例
>> t = Time.new(2019,2,26,12,30,30)
>> t + 20          #=> 2019-02-26 12:30:50 +0800
>> t + 86400       #=> 2019-02-27 12:30:30 +0800
>> t + 86400 * 3   #=> 2019-03-01 12:30:30 +0800
>> (t + 10.32).nsec #=> 320000000

月份运算

Date/DateTime 还支持用 <<>> 做月份加减。前者表示前几个月,后者表示后几个月(负数则反向)。

>> dt.to_s       # 2月26
=> "2019-02-26T12:30:30+00:00"

>> (dt << 1).to_s  # 1月26
=> "2019-01-26T12:30:30+00:00"
>> (dt >> -1).to_s # 同上
=> "2019-01-26T12:30:30+00:00"

>> (dt << -1).to_s # 3月26
=> "2019-03-26T12:30:30+00:00"

不过月份操作有一个“陷阱”:如果目标月份的天数比当前月份少,会自动取最后一天。比如 3 月 31 号往前一个月是 2 月 28 号(或 29 号),连续操作两次结果可能和一次性操作不同。

>> d = Date.new(2019,3,31)
>> (d << 1).to_s       #=> "2019-02-28"
>> (d << 2).to_s       #=> "2019-01-31"   # 直接减两个月,正确
>> (d << 1 << 1).to_s  #=> "2019-01-28"   # 分两次减,结果不同

所以,如果你对结果的精确性有要求,最好还是用天数加减来替代月份运算。

日期时间的 prev 和 next

Date/DateTime 还提供了一系列 next_*prev_* 方法:

  • next / succ
  • next_day
  • next_month
  • next_year
  • prev_day
  • prev_month
  • prev_year

它们本质上和 + - << >> 是一样的,而且 next_month/prev_month 同样存在上述“陷阱”。

日期时间的比较

Date/Time/DateTime 都实现了 <=> 运算符,并且混入了 Comparable 模块,所以可以直接用 <>between? 等方法进行比较,非常方便。

另外,Date 重写了 === 运算符,等价于 ==,只要日期相同就返回 true。DateTime 作为子类,也继承了这个行为——哪怕时间部分不同,日期相同也视为相等,这一点在实际使用时需要注意。

>> d1 = Date.new(2019,5,23)
>> d2 = Date.new(2019,5,24)
>> d1 < d2  #=> true

>> dt1 = DateTime.new(2019,5,23,12,30,30.123)
>> dt2 = DateTime.new(2019,5,23,12,30,30.234)
>> dt1 < dt2    #=> true
>> dt1 === dt2  #=> true(尽管时间不同)
>> d1 === dt1   #=> true(日期相同)

>> t1 = Time.new(2019,5,23,12,30,30.123)
>> t2 = Time.new(2019,5,23,12,30,30.234)
>> t1 === t2    #=> false
>> t1 < t2      #=> true

Date 和 DateTime 之间可以互相比较,但它们都不能直接和 Time 比较。

日期时间的迭代

Date/DateTime 支持 downtoupto 两种迭代方式,Time 不支持。默认每次迭代一天。

此外还有 step 方法,可以指定步长。

>> d.to_s      #=> "2019-03-31"
>> d.upto(d + 7) {|date| puts date}
2019-03-31
2019-04-01
...
2019-04-07

>> (d + 7).downto(d) {|date| puts date}
2019-04-07
...
2019-03-31

step 的用法:

step(limit[, step=1]) → enumerator
step(limit[, step=1]){|date| ...} → self

d = Date.new(2019, 5, 23)
d1 = Date.new(2019, 5, 28)
d.step(d1) { |date| puts date }
d.step(d1, 2) { |date| puts date }

输出:

2019-05-23
2019-05-24
...
2019-05-28
--------------------
2019-05-23
2019-05-25
2019-05-27

几种构建日期时间对象的效率对比

虽然功能都一样,但不同构造方式的性能差距可能超出你的想象。下面分别对 Date、Time、DateTime 做了 100 万次构建测试,结论先摆出来:

  • new 方法几乎总是最快的选择
  • 手动指定时区对 Time.new 有奇效(效率提高好几倍),但对其他类影响不大
  • parse 的效率最差,甚至差了一个数量级

Date 类的几种构建方式

# Date.new
$ time ruby -r'date' -e '1000000.times {|x| Date.new(2017,3,23)}'
real    0m0.410s

# Date.parse
$ time ruby -r'date' -e '1000000.times {|x| Date.parse("2017-3-23")}'
real    0m2.604s

# Date.strptime
$ time ruby -r'date' -e '1000000.times {|x| Date.strptime("2017-3-23","%Y-%m-%d")}'
real    0m0.790s

Date.new 最快,parse 最慢,strptime 居中。

Time 类的几种构建方式

# Time.new 不指定时区
$ time ruby -r'time' -e '1000000.times {|x| Time.new(2017,3,23,16,30,15)}'
real    0m2.546s

# Time.new 指定时区
$ time ruby -r'time' -e '1000000.times {|x| Time.new(2017,3,23,16,30,15,"+08:00")}'
real    0m0.663s   # 快了将近 4 倍!

# Time.at 不指定时区
$ time ruby -e '1000000.times {|x| Time.at(1490257815)}'
real    0m0.358s   # 最快

# Time.at 指定时区
$ time ruby -e '1000000.times {|x| Time.at(1490257815,in: "+08:00")}'
real    0m0.941s

# Time.parse
$ time ruby -r'time' -e '1000000.times {|x| Time.parse("2017-03-23 16:30:15 +0800")}'
real    0m10.949s   # 最慢,慢得离谱

# Time.mktime
$ time ruby -r'time' -e '1000000.times {|x| Time.mktime(2017,3,23,16,30,15)}'
real    0m2.490s

Time.at 最快,但只适用于 epoch。Time.new 指定时区后性能大幅提升,几乎追平 Time.at。Time.parse 是绝对的性能杀手。

DateTime 类的几种构建方式

# DateTime.new 不指定时区
$ time ruby -r'date' -e '1000000.times {|x| DateTime.new(2017,3,23,16,30,15)}'
real    0m0.409s

# DateTime.new 指定时区
$ time ruby -r'date' -e '1000000.times {|x| DateTime.new(2017,3,23,16,30,15,"+08:00")}'
real    0m0.502s

# DateTime.strptime 不指定时区
$ time ruby -r'date' -e '1000000.times {|x| DateTime.strptime("2017-3-23 16:30:15","%Y-%m-%d %H:%M:%S")}'
real    0m0.994s

# DateTime.strptime 指定时区
$ time ruby -r'date' -e '1000000.times {|x| DateTime.strptime("2017-3-23 16:30:15 +08:00","%Y-%m-%d %H:%M:%S %z")}'
real    0m1.838s

# DateTime.parse 不指定时区
$ time ruby -r'date' -e '1000000.times {|x| DateTime.parse("2017-3-23 16:30:15")}'
real    0m6.206s

# DateTime.parse 指定时区
$ time ruby -r'date' -e '1000000.times {|x| DateTime.parse("2017-3-23 16:30:15 +08:00")}'
real    0m6.944s

DateTime.new 依然是最快的,parse 依然是最慢的。时间就是金钱,能省则省。

本文转载于:https://www.jb51.net/article/244915.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

产品推荐

热门关注