PHP で祝日の情報を扱うライブラリ Yasumi
2丁目から同じ町の1丁目に引っ越す kagata です。
さて、今回は表題のとおり、PHP で祝日を取り扱うのにたいへん便利なライブラリ Yasumi をご紹介します。日本国内ではまだあまり使われていないのか、「PHP 祝日」みたいなキーワードでぐぐってみてもなかなか使用例が出てきません。すごく使い勝手がよいので、ぜひ広まってほしいとの願いを込めてお送りします。
Yasumi のいいところ
オフライン対応
祝日の情報を取り扱う方法のひとつに、Google Calendar API をはじめとする外部のリソースからカレンダーを取得する、というものがあります。
これはこれでお手軽なのですが、インターネットに接続できない隔離された環境ではこの手が使えません。また、実装によっては外部 API サービスが落ちているせいで使えなくなる、なんてことも起こるかもしれません。
Yasumi は祝日それぞれの定義(日本の成人の日は1月の第2月曜日、など)にもとづいて該当の日付を計算する方式をとるので、外部にデータを取りに行く必要がありません。
過去や未来の日付にも広く対応
同じような方法として、祝日のリストを設定ファイルに書くなどしてまるまる保持してしまう、という実装も見かけます。カレンダーを自前で持ってしまえば、外部にデータを取りに行けない環境でも問題なく動作できます。
この方法をとる場合、過去や未来のカレンダーをどこまで持っておくか、考える必要が出てきます。できるだけ長期間のカレンダーを持つのが安心ではありますが、それだけ作成に手間がかかります。
前述のとおり、Yasumi は祝日のリストを持つのでなく自分で日付を計算するため、長い期間にも安定して対応できます。日本の祝日についての実装を見ると、古くは現在の『国民の祝日に関する法律』が施行された1948年から、未来は2150年までサポート(これ以降は春分の日・秋分の日が null
になる)してくれているのがわかります。
充実の国際化対応
どうやらこの点にいちばん力が入っているようで、Yasumi の説明によれば日本を含む29か国と65の地域の祝日をサポートしているとのことです。圧巻の対応ぶりですね。
また、祝日名については現地表記(日本の祝日なら日本語)と英語表記の2種類をサポートしています。
もちろん Composer 対応
イマドキのライブラリなので、当然 Composer でインストールできます。PEAR::Date_Holidays_Japan という PEAR ライブラリもありますが、ふつうのプロジェクトなら Composer でインストールできる Yasumi のほうが導入しやすいでしょう。
ただ、Yasumi のサポートする PHP バージョンが5.6以上となっているので、それ以前の古い環境では PEAR::Date_Holidays_Japan が活躍する場面もあるかもしれません。山の日に対応するなど、メンテナンスは継続されているようです。
使い方
インストールします。
$ composer require azuyalabs/yasumi
PsySH を使って、コマンドラインから遊んでみましょう。
$ composer require psy/psysh
$ ./vendor/bin/psysh
まずは日本の(第1引数)2017年の(第2引数)カレンダーを日本語で(第3引数)呼び出します。
>>> $holidays = \Yasumi\Yasumi::create('Japan', 2017, 'ja_JP');
=> Yasumi\Provider\Japan {#192}
祝日は何日あるでしょうか。
>>> echo $holidays->count();
17⏎
17日でした。
次は祝日の日付と名前を列挙してみましょう。
>>> foreach ($holidays as $holiday) echo $holiday . ' ' . $holiday->getName() . "\n";
2017-01-01 元日
2017-01-02 振替休日 (元日)
2017-01-09 成人の日
2017-02-11 建国記念の日
2017-03-20 春分の日
2017-04-29 昭和の日
2017-05-03 憲法記念日
2017-05-04 緑の日
2017-05-05 子供の日
2017-07-17 海の日
2017-08-11 山の日
2017-09-18 敬老の日
2017-09-23 秋分の日
2017-10-09 体育の日
2017-11-03 文化の日
2017-11-23 勤労感謝の日
2017-12-23 天皇誕生日
今年の春分の日っていつでしたっけ。そんなときは祝日の英語名で調べられます(その英語名はコードを追ってみないとわからなかったりするのですが)。
>>> echo $holidays->whenIs('vernalEquinoxDay');
2017-03-20⏎
ところで今日って祝日じゃないんですかね?
>>> var_dump($holidays->isHoliday(new DateTime()));
bool(false)
=> null
違いました。気のせいだったようです。
あ、次の祝日は9月18日か。敬老の日。
>>> var_dump($holidays->isHoliday(new DateTime('2017-09-18')));
bool(true)
=> null
ほら祝日でした。楽しみです。
ほかには?
上の使用例でいうカレンダー的なオブジェクト $holidays
はクラス \Yasumi\Provider\Japan
のインスタンスです。 $holidays
にどんなメソッドが用意されているかについては、これが継承する抽象クラス \Yasumi\Provider\AbstractProvider
を見るとだいたいわかります。また、上の使用例で $holidays
をしれっと foreach
していますが、これができるのは IteratorAggregate
のおかげであることもわかります。
祝日オブジェクト $holiday
はクラス \Yasumi\Holiday
のインスタンスです。ここを見れば、どんなことができるかがだいたいわかります。また、上の使用例で文字列でもない $holiday
をしれっと echo
できたのは、 マジックメソッド __toString()
のおかげです。いい仕事してますね。
ちょっとコードを読んでみる
ところで、具体的なカレンダーデータを持たずに祝日を取り扱うとは、どういう実装になっているのでしょうか。気になったので、祝日の定義にかかわる部分のコードを読んでみました。
上の使用例でいうカレンダー的なオブジェクト $holidays
はクラス \Yasumi\Provider\Japan
のインスタンスです。このクラスの中に、日本の祝日の定義がひととおり入っています。
たとえば、建国記念の日は initialize
メソッドに次のような形で実装されています。
if ($this->year >= 1966) {
$holiday = new Holiday(
'nationalFoundationDay',
['en_US' => 'National Foundation Day', 'ja_JP' => '建国記念の日'],
new DateTime("$this->year-2-11", new DateTimeZone($this->timezone)),
$this->locale
);
$this->addHoliday($holiday);
}
建国記念の日は1966年以降の2月11日、というわけです。基本的に、毎年日付が固定の祝日はすべて initialize
メソッドにまとめて実装されています。
一方、日付が固定でなく曜日で決まるような祝日は独立したメソッドになっています。たとえば1999年までは1月15日、2000年からは1月の第2月曜日になった成人の日はこう。
private function calculateComingOfAgeDay()
{
$date = null;
if ($this->year >= 2000) {
$date = new DateTime("second monday of january $this->year", new DateTimeZone($this->timezone));
} elseif ($this->year >= 1948) {
$date = new DateTime("$this->year-1-15", new DateTimeZone($this->timezone));
}
if (null !== $date) {
$this->addHoliday(new Holiday(
'comingOfAgeDay',
['en_US' => 'Coming of Age Day', 'ja_JP' => '成人の日'],
$date,
$this->locale
));
}
}
DateTime
のコンストラクタに渡している日付の相対表記がいい味を出しています。
実装を見るまでいちばん気になっていたのが、「天体の動きで微妙に変わる春分の日・秋分の日はどうなっているのか?」という点でした。ちょっと長いですが、春分の日の計算に必要なところを抜き出してみます。
const EQUINOX_GRADIENT = 0.242194;
const VERNAL_EQUINOX_PARAM_1979 = 20.8357;
const VERNAL_EQUINOX_PARAM_2099 = 20.8431;
const VERNAL_EQUINOX_PARAM_2150 = 21.8510;
private function calculateVernalEquinoxDay()
{
$day = null;
if ($this->year < 1948) {
$day = null;
} elseif ($this->year >= 1948 && $this->year <= 1979) {
$day = floor(self::VERNAL_EQUINOX_PARAM_1979 + self::EQUINOX_GRADIENT * ($this->year - 1980) - floor(($this->year - 1983) / 4));
} elseif ($this->year <= 2099) {
$day = floor(self::VERNAL_EQUINOX_PARAM_2099 + self::EQUINOX_GRADIENT * ($this->year - 1980) - floor(($this->year - 1980) / 4));
} elseif ($this->year <= 2150) {
$day = floor(self::VERNAL_EQUINOX_PARAM_2150 + self::EQUINOX_GRADIENT * ($this->year - 1980) - floor(($this->year - 1980) / 4));
} elseif ($this->year > 2150) {
$day = null;
}
if (null !== $day) {
$this->addHoliday(new Holiday(
'vernalEquinoxDay',
['en_US' => 'Vernal Equinox Day', 'ja_JP' => '春分の日'],
new DateTime("$this->year-3-$day", new DateTimeZone($this->timezone)),
$this->locale
));
}
}
最初に定義している定数 const EQUINOX_GRADIENT = 0.242194
は、1年が正確には365日より0.242194日長いという事実を指しています。
また、3つ定義されている定数 VERNAL_EQUINOX_PARAM_n
は、それぞれ西暦 n
年の厳密な春分の日(春分点通過日)を示しています。たとえば、1979年の春分の日は3月20.8357日だった(20日20時3分24秒ごろに太陽が春分点を通過した)、というような感じです。
そして4つの定数で示される事実から、メソッド calculateVernalEquinoxDay
が各年の春分の日を計算する、というしくみになっています。
上のコードで使われている2099年と2150年の春分の日は、現在までの天体観測の結果から予測されている値のようです(出典がわからないのですが、理科年表とかでしょうか)。これが予測値とずれれば、未来の春分の日もここでの計算結果とずれることになります。また、2150年より先の春分の日を Yasumiが扱えないのは、持っている予測値が2150年までだから、というわけです。
なお、春分の日・秋分の日の計算方法について詳しくは下記記事が参考になります。
まとめ
PHP で祝日を扱うのに便利なライブラリ Yasumi について、簡単な使い方と実装の解説をしました。同種のライブラリと比べてもかなり使い勝手がよいと感じています。ぜひ使ってみてください。