lubridate包ceiling_date函数夏令时处理异常问题咨询
lubridate::ceiling_date on DST transition days (Australia/Adelaide timezone) 我之前也踩过这个坑,这确实是lubridate::ceiling_date在处理夏令时(DST)转换日时的一个反直觉行为,算不上完全意义上的bug,但确实和我们预期的结果不符。咱们先复现你提到的场景,再聊聊原因和解决办法:
复现问题
首先运行你提供的代码,构造目标时间向量并查看函数表现:
library(lubridate) library(magrittr) # 夏令时开始日(2017-10-01 02:00调快1小时,直接跳到03:00) dt_1 <- ymd("2017-10-01", tz = "Australia/Adelaide") %>% add(hours(c(0,1,23,24))) # 夏令时结束日(2017-04-02 03:00调慢1小时,回到02:00) dt_2 <- ymd("2017-04-02", tz = "Australia/Adelaide") %>% add(hours(c(0,1,23,24))) # floor_date表现符合预期:都返回当日午夜 floor_date(dt_1, "day") #> [1] "2017-10-01 00:00:00 ACDT" "2017-10-01 00:00:00 ACDT" #> [3] "2017-10-01 00:00:00 ACDT" "2017-10-02 00:00:00 ACDT" floor_date(dt_2, "day") #> [1] "2017-04-02 00:00:00 ACST" "2017-04-02 00:00:00 ACST" #> [3] "2017-04-02 00:00:00 ACST" "2017-04-03 00:00:00 ACST" # ceiling_date的异常表现:非午夜时间结果偏移1小时 ceiling_date(dt_1, "day") #> [1] "2017-10-01 00:00:00 ACDT" "2017-10-01 23:00:00 ACST" #> [3] "2017-10-02 00:00:00 ACDT" "2017-10-03 00:00:00 ACDT" ceiling_date(dt_2, "day") #> [1] "2017-04-02 00:00:00 ACST" "2017-04-03 01:00:00 ACDT" #> [3] "2017-04-03 00:00:00 ACST" "2017-04-04 00:00:00 ACST"
可以看到,ceiling_date处理转换日的非午夜时间(比如dt_1里的2017-10-01 01:00、dt_2里的2017-04-02 01:00)时,结果并没有返回次日午夜,而是偏移了1小时,只有转换日午夜和次日午夜的结果是正确的。
问题原因
这个问题的根源在于ceiling_date的内部逻辑:它会先将本地时区的时间转换为UTC进行天花板计算,再转回原时区。而在DST转换日,本地时区和UTC的小时偏移发生了变化(比如Australia/Adelaide在夏令时是+10:30,非夏令时是+09:30),这种转换就会导致计算结果出现偏移。
而floor_date则是直接基于本地时区的午夜时间进行计算,不需要经过UTC转换,所以表现符合预期。
解决办法
有两个可靠的替代方案可以绕过这个问题:
方案1:用
floor_date + days(1)替代ceiling_date
既然floor_date表现正常,我们可以先取当日的地板时间(午夜),再加一天,就能得到正确的次日午夜时间:# 替代ceiling_date的写法 floor_date(dt_1, "day") + days(1) #> [1] "2017-10-02 00:00:00 ACDT" "2017-10-02 00:00:00 ACDT" #> [3] "2017-10-02 00:00:00 ACDT" "2017-10-03 00:00:00 ACDT" floor_date(dt_2, "day") + days(1) #> [1] "2017-04-03 00:00:00 ACST" "2017-04-03 00:00:00 ACST" #> [3] "2017-04-03 00:00:00 ACST" "2017-04-04 00:00:00 ACST"方案2:手动构造次日午夜时间
如果需要更灵活的处理,也可以手动构造目标时区的次日午夜时间:# 手动构造次日午夜 as.POSIXct(paste0(as.Date(dt_1) + 1, " 00:00:00"), tz = "Australia/Adelaide")
另外,你可以检查一下你的lubridate版本,这个问题在较新的版本中已经有部分修复,但对于Australia/Adelaide这种带半小时偏移的时区,还是可能出现上述情况,所以用替代方案会更稳妥。
内容的提问来源于stack exchange,提问作者user3646834

