學習軌道
/
R
R
/
練習
/
為美預約
為美預約

為美預約

學習練習

簡介

「日期和時間這種東西,我們連小孩子都在教。能有多難?」

許多程式設計師都犯過這種錯,而隨之而來的經歷,往往對他們的身心健康造成不小的傷害。

任何在日期和時間上做非平凡程式設計的人,至少都該準備好理解並減輕潛在的問題。

本文件會試著概述 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 則不具時區感知。
  • 日期時間建構函式一般預設為 UTC。
  • 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。

1. 解析全數字的預約日期

實作schedule_appointment()函式,把以文字表示的預約日期解析成對應的DateTime格式。

請注意,輸入採用美式的月/日/年順序。

julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00

2. 檢查預約是否已經過了

實作has_passed()函式,它接受一個預約日期,並檢查該預約是否已成為過去:

julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true

3. 檢查預約是否在下午

實作is_afternoon_appointment()函式,它接受一個預約日期,並檢查該預約是否在下午(>= 12:00 且 < 18:00):

julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true

4. 檢查預約是星期幾

實作day_of_week()函式,它會回傳一週中_以數字表示_的那一天。 沙龍在週日休息,所以週一是第 1 天,週六是第 6 天。

# a Friday
day_of_week(make_datetime(2026, 06, 05, 12, 30, 00))
#> [1] 5

5. 重新安排預約

當顧客需要重新安排預約時,系統會自動安排到下一個星期五的同一時間(因為這是一週中最冷清的一天)。

實作reschedule()函式,它接受原始的預約(以月/日/年順序的文字字串表示),並回傳重新安排後的預約。

# a Saturday
reschedule("06/13/2026 12:30:00")
#> [1] "2026-06-19 12:30:00 UTC"

如果原始預約是在週一到週四,就重新安排到_本_週的星期五。 如果是週五或週六的預約,就重新安排到_下_週的星期五。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
R Exercism

準備好開始 為美預約 了嗎?

註冊 Exercism,透過 21 個概念111 個練習 和真人引導來學習並精通 R,全部免費。