「日期和時間這種東西,我們連小孩子都在教。能有多難?」
許多程式設計師都犯過這種錯,而隨之而來的經歷,往往對他們的身心健康造成不小的傷害。
任何在日期和時間上做非平凡程式設計的人,至少都該準備好理解並減輕潛在的問題。
本文件會試著概述 R 如何處理日期和時間,但若想了解更進一步的細節,還是得查閱完整的文件。
作為一門資料科學語言,日期和時間在 R 裡格外重要。 許多資料都是以時間序列的形式出現:疾病爆發期間的每日病患人數、政府公布的每月就業數據、築巢聚落裡的鳥類數量、主要經濟體的年度國內生產毛額(GDP),還有數不清的其他例子。
這代表 R 讓日期和時間的處理變簡單了嗎?
很遺憾,並沒有!
Base R 的日期與時間功能風評不佳,如今大多已很少使用。
lubridate 套件是一大躍進,但你得先有心理準備:它是一套龐大又複雜的函式庫,為的是處理既龐大又複雜的各種問題。
lubridate 原本是獨立套件,現在已隨 tidyverse 集合一起提供。
用library(lubridate)或library(tidyverse)載入即可。
以下資源可以幫助你使用 lubridate:
date 就只是一天,不帶任何時間資訊。
在內部,日期是以自 1970-01-01 起算的(帶正負號的)天數儲存。
today()
#> [1] "2026-04-22"
datetime 則結合了日期、時間與時區資訊。
在內部,它是以 UTC 時區中自 1970 年初起算的秒數儲存。
now()
#> [1] "2026-04-22 15:37:11 MST" # mid-afternoon in Arizona
或許出乎意料的是,R 並沒有為只有時間的值提供內部表示法。
date與datetime值最常見的日期時間需求大概是:
today())與/或日期時間(用now())。日期和時間的寫法有很多種,而且往往因文化而異。 像「7/6/23」這種全數字的日期既模糊又令人困惑,還曾在跨國組織裡造成許多代價高昂的錯誤(作者本人就有過慘痛的親身經歷)。
國際標準格式(年-月-日)定義於 ISO 8601,它有兩大優點:
不過,資料科學就是要處理你手邊能找到的資料,抱怨別人怎麼格式化往往沒有意義。
Base R 有轉換函式,可接受格式字串(預設為 ISO 8601)。
as.Date("2026-04-22")
#> [1] "2026-04-22"
as.Date("4/22/26", format = "%m/%d/%y")
#> [1] "2026-04-22"
as.POSIXct("2026-04-22 15:30:09 MST")
#> [1] "2026-04-22 15:30:09 MST"
這些格式字串類似已有數十年歷史的 strptime() 函式,這個函式存在於_許多_程式語言中。
熟悉這套語法的程式設計師,可以選擇使用 lubridate 裡的 fast_strptime() 函式。
更貼心的是,lubridate 提供各式各樣的輔助函式,名稱本身就代表了欄位順序。
只要函式知道要找哪些值、以及它們的順序,解析就靈活得令人印象深刻。
mdy("4/22/26")
#> [1] "2026-04-22"
mdy("April 22nd 26")
#> [1] "2026-04-22"
mdy("Wednesday, April 22nd 2026")
#> [1] "2026-04-22"
mdy_hm("Wednesday, April 22nd 2026, 4:14 pm")
#> [1] "2026-04-22 16:14:00 UTC" # defaults to your OS timezone
如果你的程式把日期/日期時間的各個部分拆成獨立數字,可以用 make_date() 或 make_datetime() 把它們組起來。
以下是可提供的元件,同時顯示它們的順序與預設值:
make_datetime(
year = 1970L,
month = 1L,
day = 1L,
hour = 0L,
min = 0L,
sec = 0,
tz = "UTC"
)
#> [1] "1970-01-01 UTC"
make_datetime(2026, 4, 22, 17)
#> [1] "2026-04-22 17:00:00 UTC"
當然,make_date() 只接受年、月、日這幾個引數。
R 主控台會自動以字串形式顯示日期/日期時間。 這通常很方便,但有時會造成誤導。
在內部,日期和日期時間是數字(外加一些屬性),不是字串。 在預期是字串的地方回傳日期(時間)會出問題,所以我們得知道怎麼做轉換。
一個簡單的做法是用 Base R 的 as.character(),它會輸出 ISO 8601 格式的字串。
另外還有給日期時間用的 format_ISO8601() 函式,它有一些選項可以四捨五入到想要的精確度。
today() |> as.character() # a date
#> [1] "2026-04-23"
now() |> as.character() # a datetime
#> [1] "2026-04-23 09:07:09.779319"
now() |> format_ISO8601() # defaults to nearest second
#> [1] "2026-04-23T09:07:38"
很少有非程式設計師想看到 ISO 8601 的輸出,因此處理特定文化的日期時間格式就很重要。
Base R 提供 format() 函式,它使用的格式字串,類似許多其他語言裡歷史悠久的 strftime() 函式。
format(today(), "%A, %d %B %Y")
#> [1] "Thursday, 23 April 2026"
這種 Base R 做法雖然醜,卻很可靠。 以下是一些較常見的代碼:
| 代碼 | 結果 |
|---|---|
| %d | 每月第幾天(5、17、28 等) |
| %a | 星期幾的縮寫(Mon、Tue、Wed 等) |
| %A | 完整的星期名稱(Monday、Tuesday 等) |
| %w | 星期編號(0 到 6,Sunday 為 0) |
| %u | 星期編號(1 到 7,Monday 為 1) |
| %b | 月份的縮寫(Jan、Feb 等) |
| %B | 完整的月份名稱(January、February 等) |
| %y | 兩位數年份(例如 89) |
| %Y | 四位數年份(例如 1989) |
| %h | 小時(24 小時制) |
| %m | 分鐘 |
| %s | 秒 |
| %z | 與 UTC 的偏移量 |
| %Z | 時區(字元) |
Lubridate 想用 stamp() 函式提供更友善的替代方案,另外還有範圍更窄的 stamp_date() 與 stamp_time()。
這些函式接受一個範本字串,也就是你想要輸出的範例,然後建立一個自訂函式來產生那個格式。
Lubridate 會嘗試猜測格式,但常常遇到模稜兩可的情況而產生錯誤結果。
要有心理準備,適時提供格式字串(例如"dmy")來釐清歧義。
在 Exercism 上,指定locale = "C"也很有用,這樣可以繞過系統的地區設定,改用英文運作(這也是測試所預期的)。
地區設定很複雜!
sf <- stamp("Saturday, June 27, 2026 at 14:45", locale = "C")
#> Multiple formats matched: "%A, %B %d, %Y at %H:%M"(1), "Saturday, %Om %d, %Y at %H:%M"(1), "Saturday, %B %d, %Y at %H:%M"(1), "%A, %Om %d, %Y at %H:%M"(0)
#> Using: "%A, %B %d, %Y at %H:%M"
sf(make_datetime(2023, 3, 29, 15, 0, 0))
#> [1] "Wednesday, March 29, 2023 at 15:00"
Lubridate 提供許多函式來取得或設定日期/日期時間的元件:像是 year() 和 second() 這種顯而易見的,也有 quarter() 和 epiweek() 這類更專門的。
這類函式全都可以用來取得值,而且大多數也能用來設定值。
dt <- now()
dt
#> [1] "2026-04-23 15:47:11 MST"
# get
month(dt)
#> [1] 4
# set
month(dt) <- 11
dt
#> [1] "2026-11-23 15:47:11 MST"
對 wday()(一週中的第幾天)和 month() 來說,預設輸出是整數,不過:
labels = TRUE會回傳對應目前地區設定的縮寫字串。abbr = FALSE,則會回傳完整名稱。d <- today()
d
#> [1] "2026-04-23"
wday(d)
#> [1] 5
wday(d, label = TRUE)
#> [1] Thu
#> Levels: Sun < Mon < Tue < Wed < Thu < Fri < Sat
wday(d, label = TRUE, abbr = FALSE)
#> [1] Thursday
#> Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < Friday < Saturday
這些標籤是categorical variables,會是另一個概念單元的主題。
值僅限於列出的Levels,這也顯示了排序順序。
一週預設從星期日開始,但可以用 week_start 引數改變(星期一用1)。
dt <- make_datetime(2026, 4, 23)
# default behavior
wday(dt)
#> [1] 5
wday(dt, label = T)
#> [1] Thu
# set week_start to Monday
wday(dt, week_start = 1)
#> [1] 4
少數工具函式會回傳TRUE/FALSE值,例如 leap_year() 和 dst()(目前是否為日光節約時間?)。
每個月的長度不一,所以 days_in_month() 會給出整數值。
date 和 datetime 值都代表時間軸上的某個瞬間。
那我們要怎麼處理「昨天的這個時候」或「一週後」這類概念?
熟悉其他科學導向語言的學生,可能知道 Python 的datetime.timedelta或 Julia 的Period型別。
相比之下,R 至少有三種表示時間跨度的方法,各有不同的長處與限制。 一如往常,挑最適合你情境的那一種。
各種跨度之間可以輕鬆互轉,用 as.period()、as_duration() 和 as.interval()。
可以用 is.period() 等函式來檢查跨度的型別。
一個 period,例如 hours(3),可以用來把鐘錶時間增加 3 小時。
日光節約時間(DST)之類的不規則情況大多會被忽略。
所有期間都是單位名稱的複數形:years、weeks 等等。
d <- today()
d
#> [1] "2026-04-25"
d + weeks(2)
#> [1] "2026-05-09"
# dates are coerced to datetimes at midnight if necessary
d - hours(20)
#> [1] "2026-04-24 04:00:00 UTC"
hours(3)
#> [1] "3H 0M 0S"
一個 duration,例如 dhours(3),會把實際時間增加 3 小時,並顧及日光節約時間這類會讓鐘錶時間偏離實際時間的不規則情況。
所有持續時間都是在對應的期間前面加上 d 前綴:dyears、dweeks 等等。
dhours(3)
#> [1] "10800s (~3 hours)"
一個 interval 是起始時間與結束時間的組合,可以用 interval() 函式或 %--% 中綴運算子建立。
# the next 2 lines were run about 33s apart, so now() is different
dt1 <- now()
dt2 <- now() + hours(1)
# interval(0 function)
ivl <- interval(dt1, dt2)
ivl
#> [1] 2026-04-24 16:24:54 MST--2026-04-24 17:25:27 MST
# infix operator
iv2 <- dt1 %--% dt2
iv2 == iv1
#> [1] TRUE
處理時區可能頗具挑戰。 下面這段引文出自 Python 的文件,但對 R 同樣適用:
「世界各地調整時間的規則,政治考量多於理性考量,而且經常變動;除了 UTC 之外,沒有哪一套標準適用於所有應用程式。」
關於時區的幾個通則:
datetime 一定會包含時區(tz);date 則不具時區感知。now() 函式是例外:它會從系統時鐘取得你的本地時間,並包含 tz。
預設會使用本地 tz,但也可以指定其他時區,例如 now("UTC")。# timezone defaults to UTC
local <- ymd_hm("2026-04-25 10:32")
local
#> [1] "2026-04-25 10:32:00 UTC"
# timezone can be specified
AZ <- ymd_hm("2026-04-25 10:32", tz = "America/Phoenix")
AZ
#> [1] "2026-04-25 10:32:00 MST"
# same clocktime, different tz
local - AZ
#> Time difference of -7 hours
用持續時間來做時區相關的運算,會比用期間更可靠。
處理日期和日期時間,用 lubridate 通常比用 Base R 輕鬆得多。
這個概念單元試著提供概述,但無可避免地漏掉非常多細節。
就算要冒著重複的風險,還是要說:文件是你的好朋友,請多加利用喔。
在這道練習中,你要為紐約一間在 2012 年 9 月 15 日開幕的美容沙龍製作預約排程系統。
你有五個任務,每個任務都和預約日期有關。 時間採用 24 小時制(沒有 AM/PM)。 為了盡量簡化時區問題,請假設所有時間都使用 UTC。
實作schedule_appointment()函式,把以文字表示的預約日期解析成對應的DateTime格式。
請注意,輸入採用美式的月/日/年順序。
julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00
實作has_passed()函式,它接受一個預約日期,並檢查該預約是否已成為過去:
julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true
實作is_afternoon_appointment()函式,它接受一個預約日期,並檢查該預約是否在下午(>= 12:00 且 < 18:00):
julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true
實作day_of_week()函式,它會回傳一週中_以數字表示_的那一天。
沙龍在週日休息,所以週一是第 1 天,週六是第 6 天。
# a Friday
day_of_week(make_datetime(2026, 06, 05, 12, 30, 00))
#> [1] 5
當顧客需要重新安排預約時,系統會自動安排到下一個星期五的同一時間(因為這是一週中最冷清的一天)。
實作reschedule()函式,它接受原始的預約(以月/日/年順序的文字字串表示),並回傳重新安排後的預約。
# a Saturday
reschedule("06/13/2026 12:30:00")
#> [1] "2026-06-19 12:30:00 UTC"
如果原始預約是在週一到週四,就重新安排到_本_週的星期五。 如果是週五或週六的預約,就重新安排到_下_週的星期五。