トラック
/
R
R
/
演習
/
美容の予約
美容の予約

美容の予約

学習演習

はじめに

「日付と時刻なんて、小さな子どもにだって教えられるものです。難しいはずがありませんよね?」

多くのプログラマーがこの思い違いをしてきました。そしてその後味は、健康と幸福に悪影響を及ぼしがちです。

日付と時刻を扱う、それなりの規模のプログラミングに携わるなら、少なくとも起こりうる問題を理解し、和らげる心構えが必要です。

このドキュメントでは、Rが日付と時刻をどのように扱うかの概要を説明します。ただし、さらに詳しい内容については公式ドキュメントを参照する必要があります。

データサイエンス向けの言語であるRでは、日付と時刻はとりわけ重要です。 データの多くは時系列の形で届きます。感染症の流行時の日々の患者数、政府が報告する毎月の雇用統計、営巣地の鳥の個体数、主要国の年間GDPなど、例を挙げればきりがありません。

それでは、Rを使えば日付と時刻の扱いが簡単になるのでしょうか?

残念ながら、そうではありません。

Base Rの日付と時刻の機能は評判が良くなく、そのほとんどは今ではめったに使われません。

lubridateパッケージは大きな改善ですが、大きく複雑な問題群を扱うために設計された、大きく複雑なライブラリだと覚悟しておきましょう。

もともとは独立したパッケージでしたが、現在のlubridateはtidyverseコレクションに同梱されています。 library(lubridate)またはlibrary(tidyverse)で読み込みます。

lubridateを使いこなすのに役立つ資料には、次のようなものがあります。

日付と日時

dateは時刻の情報を持たない、ただの1日です。

内部的には、日付は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の値を扱う

日付・時刻でよくあるニーズは、おそらく次のようなものです。

  • 適切な入力形式を解析して、日付や日時を組み立てる。
  • 日付や日時から、必要な数値や文字列の形式を取り出す。
  • 日付や日時の値にオフセットを適用する。
  • 2つの値の間の時間の幅を計算する。
  • 現在の日付(today())や日時(now())を取得する。

文字列からの解析

日付と時刻の書き方には多くの流儀があり、文化によって異なる傾向があります。 "7/6/23"のような数字だけの日付は曖昧で紛らわしく、多国籍企業で多くの高くつくミスの原因になってきました(著者は苦い実体験からそう言っています)。

国際規格(y-m-d)はISO 8601で定められており、主な利点が2つあります。

  • 解析が速く、曖昧さがない。
  • 日時を通常のテキストとして扱い、アルファベット順に並べ替えられるので、並べ替えが簡単。

とはいえ、データサイエンスとは手に入るデータをそのまま扱うものです。他人がどう整形したかを嘆いても、たいてい何の役にも立ちません。

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、日曜は0)
%u 曜日の番号(1〜7、月曜は1)
%b 月の略称(Jan、Febなど)
%B 月の完全名(January、Februaryなど)
%y 2桁の年(例: 89)
%Y 4桁の年(例: 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

leap_year()やdst()(夏時間が実施されているか?)など、TRUE/FALSEの値を返すユーティリティ関数もいくつかあります。

月によって日数は異なるので、days_in_month()がその整数値を返します。

時間の幅

dateとdatetimeの値はどちらも、時間軸上の一瞬を表します。

「昨日の今ごろ」や「今から1週間後」といった概念は、どう扱えばよいのでしょうか?

科学技術計算向けの他の言語に慣れた学習者なら、Pythonのdatetime.timedeltaやJuliaのPeriod型をご存じかもしれません。

対照的に、Rには時間の幅を表す方法が3つもあり、それぞれ長所と限界が異なります。 いつもどおり、自分の状況に最も合ったものを選びましょう。

さまざまな時間の幅は、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時間進め、時計の時刻が物理的な時間からずれる原因となるDSTのような不規則さを考慮します。

デュレーションはすべて、対応する期間の名前に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

タイムゾーンに関わる計算は、期間よりもデュレーションを使うほうが信頼できます。

まとめ

日付と日時の扱いは、Base Rよりもlubridateを使うほうがたいていずっと簡単です。

このコンセプトでは概要を伝えようとしましたが、どうしても多くの詳細は省かれています。

繰り返しになりますが、ドキュメントは頼りになる味方です。ぜひ活用してください。

説明

この演習では、2012年9月15日にオープンしたニューヨークの美容サロン向けの予約スケジューラーに取り組みます。

5つのタスクがあり、いずれも予約の日付を扱います。 時刻は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を学んでマスターできます。すべて無料です。