R 中的日期是换了装扮的天数
要把字符串变成真正的日期,用 as.Date();要获取今天的日期,用 Sys.Date()。两者背后都是一个简单的技巧:R 的 Date 类只是一个数字,即自 1970 年 1 月 1 日以来的天数,外面套了一层装扮,让它打印出来像日历日期。用 unclass() 可以把这层装扮脱掉:
那个 20672 就是原始的天数,本页的一切都建立在它之上:比较、相减和序列在底层都只是整数运算。而那层装扮让它变得好用:同一个数字能打印成日期、格式化成日期,还能画在日期坐标轴上。
as.Date() 无需任何帮助就能解析 ISO 格式 "YYYY-MM-DD",这也是在文件中用这种格式存储日期的诸多好理由之一。有个习惯要尽早养成:解析失败会悄悄返回 NA,而不是报错,所以每次解析新数据后都要看一眼结果。
只是看起来像日期的字符串仍然只是字符串:"2026-08-07" < "2026-11-01" 碰巧能正确比较,因为 ISO 顺序和字母顺序一致,但 "07/08/2026" 排序起来就乱了。先转换,再比较。
用 format = 解析其他格式
现实世界并不都用 ISO 格式写日期。对于其他格式,as.Date() 接受一个 format = 参数:一个描述输入字符串的模板,由 % 代码组成。
这三个都解析成同一天。模板必须和字符串完全匹配,包括分隔符:"%d/%m/%Y" 解析不了 "07-08-2026"。你实际会用到的代码:
| 代码 | 含义 | 示例 |
|---|---|---|
%Y | 4 位年份 | 2026 |
%y | 2 位年份 | 26 |
%m | 月份数字 | 08 |
%d | 当月第几天 | 07 |
%b | 月份名称缩写 | Aug |
%B | 月份全称 | August |
%a | 星期缩写 | Fri |
%A | 星期全称 | Friday |
%H | 小时(00 至 23) | 14 |
%M | 分钟 | 30 |
%S | 秒 | 05 |
最经典的坑就是 "07/08/2026" 本身:世界上大部分地方把它读作日/月/年,美国读作月/日/年,而 R 猜不出你的文件用的是哪种。更糟的是,猜错了往往也能“正常运行”,因为两种读法都是有效日期,直到某个月的 13 号出现,被解析成 NA(没有第 13 个月)。写模板之前,先搞清楚你的数据用的是哪种约定。
格式化日期输出
format() 是 as.Date() 的镜像:同样的 % 代码,方向相反,输入 Date,输出字符串。
报告标题、标签和文件名都可以用它:format(Sys.Date(), "%Y-%m-%d") 生成的文件名能按时间顺序排序。有两个细节值得了解:%d 会补零(“August 07”),%B/%A 输出的名称取决于系统的语言设置,设置为德语的会话会打印 “Freitag” 而不是 “Friday”。格式化只放在脚本的输入输出边缘,给人看;在内部,日期就保持为 Date。
日期运算和序列
因为 Date 是天数,所以运算的结果正如你所料。两个日期相减会得到一个 difftime 对象:
从这段代码中要记住三个习惯。第一,对 difftime 做数学运算前,先用 as.numeric() 包起来,因为 difftime 带有一个单位属性,可能让后续计算出现意外。第二,当单位很重要时,用 difftime(units = ) 明确指定,而不是接受 R 自己选的单位。第三,加上一个普通数字表示加上若干天:base R 中没有 + 1 month,因为“一个月”并不是固定的天数。
要按日历步进,用 seq(),它能理解 "day"、"week"、"month"、"quarter" 和 "year":
按月的序列是划分报告周期和设置坐标轴刻度的标准工具。有一个怪癖:如果从 31 号开始生成按月序列,遇到没有 31 号的月份时 R 会按实际天数继续往后数,所以“二月”那一项会落在三月初。按月的序列要从 1 号开始,其他日期再由此推算。
提取日期的各个部分
base R 有两个方便的提取函数和一个通用写法。weekdays() 和 months() 返回名称;要得到数字,就借助 format() 转一圈:
先 format() 再 as.integer() 看起来有点绕,但这是 base R 的标准写法,而且是向量化的:传入一整列日期,就会得到一整列年份。从 CSV 文件导入带日期的数据集后,正需要这样做:先用 as.Date() 把日期列解析一次,再派生出年份和月份列用于分组。
天数不够用时:POSIXct 和 lubridate
Date 没有小时的概念。一旦你的数据带有时间戳,比如日志条目、传感器读数、“14:32 下单”,你就需要 POSIXct,它是 R 的日期时间类:存储自 1970 年以来的秒数而不是天数,并且能识别时区。
前面的知识全都适用:同样的 % 代码(现在还包括 %H、%M、%S)可用于解析和 format(),相减得到 difftime,Sys.time() 表示“现在”。新的负担是时区:解析时要显式传入 tz =,否则两个处于不同时区的会话会把同一个字符串读成两个不同的时间点。也不要为了“以防万一”把日历数据存成 POSIXct:存成某个时区午夜的生日,在另一个时区可能会显示成前一天,这种 bug 追查起来非常痛苦。整天用 Date,时间点用 POSIXct。
最后,大多数 tidyverse 代码会用 lubridate 包来更友好地完成上面这一切,等你弄懂了 base R 的机制之后,很值得采用:
library(lubridate)
ymd("2026-08-07") # order-based parsing: no format string
mdy("08/07/2026") # ...you just say which order the parts come in
dmy("7 August 2026")
floor_date(Sys.Date(), "month") # snap to the start of the period
ymd()、mdy() 和 dmy() 用一个简单的顺序声明取代了格式模板,floor_date() 一次调用就解决了“按月对这些时间戳分组”的问题。lubridate 底层用的还是同样的 Date/POSIXct 类:换了个更好用的方向盘,而不是换了台发动机。
要点总结
Date是自 1970-01-01 以来的天数,经过装扮后打印得像日期;unclass()能显示这个数字。as.Date("2026-08-07")直接解析 ISO 格式;其他格式需要一个由%代码组成的format =模板,而且解析失败会悄悄返回NA。format(date, "...")正好相反:用同样的代码生成显示用的字符串。- 相减得到 difftime:显式设置
units =,做数学运算前先as.numeric()。+ n加的是天数;按月要用seq(by = "month")。 weekdays()、months()和as.integer(format(...))写法可以提取日期的各个部分,并且都是向量化的。- 整天用
Date,时间戳用POSIXct(并显式指定tz);lubridate 让两者都更好用。
下一篇:当 R 冲你报错时该怎么办,包括如何阅读错误信息,以及如何有条理地调试。
常见问题
在 R 中怎么把字符串转换成日期?
用 as.Date()。如果字符串是 ISO 格式,直接就能用:as.Date("2026-08-07")。其他任何写法都需要一个由 % 代码组成的 format = 模板:as.Date("07/08/2026", format = "%d/%m/%Y")。解析失败会返回 NA 而不是报错,所以记得检查结果。
在 R 中怎么获取今天的日期?
Sys.Date() 以 Date 对象返回今天的日期;Sys.time() 以 POSIXct 返回当前的日期时间。两者都取自你电脑的时钟和时区设置。
在 R 中怎么计算两个日期之间的差?
直接相减:as.Date("2026-08-07") - as.Date("2026-01-01") 得到一个 218 天的 difftime。要在数学运算中使用这个数,就用 as.numeric() 包起来;或者调用 difftime(d1, d2, units = "weeks") 自己指定单位,而不是让 R 来选。
R 中 Date 和 POSIXct 有什么区别?
Date 存储整天(内部是自 1970-01-01 以来的天数),对小时和时区一无所知。POSIXct 存储一个时间点(自 1970 年以来的秒数),并且能识别时区。生日、截止日期这类日历数据用 Date;只有当一天中的具体时间很重要时,比如时间戳和日志,才用 POSIXct。