Треки
/
R
R
/
Вправи
/
Запис на красу
Запис на красу

Запис на красу

Навчальна вправа

Вступ

«Дати й час - те, чого ми вчимо малих дітей. Що тут може бути складного?»

Багато програмістів припускалися цієї помилки, і набутий досвід зазвичай погано позначається на їхньому здоровʼї та щасті.

Той, хто займається серйозним програмуванням із датами й часом, має принаймні бути готовим розуміти потенційні проблеми та зменшувати їхній вплив.

У цьому документі ми спробуємо дати загальний огляд того, як 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 не привʼязаний до часового поясу.
  • Конструктори дати й часу типово використовують 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.

Ця концепція намагалася дати загальний огляд, але неминуче оминає безліч подробиць.

Ризикуючи повторитися: документація - наш друг, тож користаймося нею.

Вказівки

У цій вправі ми працюватимемо над планувальником записів на прийом для салону краси в Нью-Йорку, який відкрився 15 вересня 2012 року.

На нас чекає пʼять завдань, і всі вони повʼязані з датами прийомів. Час подано у 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(), яка приймає початковий прийом (як рядок тексту (англ. string) у порядку місяць/день/рік) і повертає перенесений прийом.

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

Якщо початковий прийом припадав на понеділок-четвер, перенесіть його на пʼятницю цього тижня. Для прийомів у пʼятницю або суботу перенесіть на пʼятницю наступного тижня.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
R Exercism

Час розпочати Запис на красу?

Зареєструйтеся на Exercism, щоб вивчати й опановувати R, а також 21 концепція111 вправ та справжнє наставництво від людей, і все це безкоштовно.