«Дати й час - те, чого ми вчимо малих дітей. Що тут може бути складного?»
Багато програмістів припускалися цієї помилки, і набутий досвід зазвичай погано позначається на їхньому здоровʼї та щасті.
Той, хто займається серйозним програмуванням із датами й часом, має принаймні бути готовим розуміти потенційні проблеми та зменшувати їхній вплив.
У цьому документі ми спробуємо дати загальний огляд того, як R працює з датами й часом, але для подробиць доведеться звертатися до повної документації.
У R, як у мові для науки про дані, дати й час мають особливе значення. Багато даних надходить у вигляді часових рядів: щоденна кількість пацієнтів під час спалаху хвороби, щомісячні показники зайнятості, які повідомляють уряди, популяції птахів у гніздовій колонії, річний ВВП великих економік і безліч інших прикладів.
Чи означає це, що R робить роботу з датами й часом простою?
На жаль, ні!
Функціональність для роботи з датами й часом у Base R має погану репутацію, і більшість її тепер використовують рідко.
Пакет lubridate - це велике покращення, але варто бути готовими до великої й складної бібліотеки, призначеної розвʼязувати великий і складний діапазон задач.
Спочатку незалежний, lubridate тепер входить до набору tidyverse.
Завантажити його можна за допомогою library(lubridate) або library(tidyverse).
Ресурси, які допоможуть у роботі з lubridate:
date позначає просто день, без жодної інформації про час.
Усередині дати зберігаються як (знакова) кількість днів від 1970-01-01.
today()
#> [1] "2026-04-22"
datetime поєднує дату, час і інформацію про часовий пояс.
Усередині він зберігається як кількість секунд від початку 1970 року в часовому поясі UTC.
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 є функції перетворення, які приймають рядок формату (англ. string), типово 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(), наявну в багатьох мовах.
Програмісти, знайомі з цим синтаксисом, можуть за бажанням скористатися функцією fast_strptime() у lubridate.
Ще зручніше те, що 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() приймає лише аргументи year, month і day.
Консоль R автоматично показує дати й час як рядкове подання. Зазвичай це зручно, але іноді вводить в оману.
Усередині дати й час - це числа (плюс кілька атрибутів), а не рядки тексту. Якщо повернути дату (чи дату й час) там, де очікується рядок тексту, це спричинить проблеми, тож нам потрібно знати, як виконати перетворення.
Простий підхід - скористатися функцією as.character() із Base R, яка виводить рядок тексту у форматі 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, де неділя - 0) |
| %u | Номер дня тижня (1-7, де понеділок - 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 позначають певний момент на часовій шкалі.
Як нам працювати з такими поняттями, як «учора цієї ж пори» або «за тиждень відтепер»?
Ті, хто вивчав інші мови, орієнтовані на науку, можливо, знайомі з datetime.timedelta у Python або типом Period у Julia.
На противагу цьому, в 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.
Ця концепція намагалася дати загальний огляд, але неминуче оминає безліч подробиць.
Ризикуючи повторитися: документація - наш друг, тож користаймося нею.
У цій вправі ми працюватимемо над планувальником записів на прийом для салону краси в Нью-Йорку, який відкрився 15 вересня 2012 року.
На нас чекає пʼять завдань, і всі вони повʼязані з датами прийомів. Час подано у 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(), яка приймає початковий прийом (як рядок тексту (англ. string) у порядку місяць/день/рік) і повертає перенесений прийом.
# a Saturday
reschedule("06/13/2026 12:30:00")
#> [1] "2026-06-19 12:30:00 UTC"
Якщо початковий прийом припадав на понеділок-четвер, перенесіть його на пʼятницю цього тижня. Для прийомів у пʼятницю або суботу перенесіть на пʼятницю наступного тижня.
Зареєструйтеся на Exercism, щоб вивчати й опановувати R, а також 21 концепція111 вправ та справжнє наставництво від людей, і все це безкоштовно.