はじめに
「スター☆トゥインクルプリキュア」第2話(2019年2月10日放送)で、こんなやりとりがありました。
ひかる「ララちゃん1ってすごいよねぇ。ロケット操縦して、修理までしちゃうんだもん」
ララ「いい大人ルン。できて当然ルン」
ひかる「えっ?大人って?いくつなの?」
ララ「地球の年齢だと…」
AI「13歳と294日です」
ひかる「わたしと同い年じゃん!」
さて、今ララの年齢は、地球基準で何歳と何日なのでしょうか?
それをプログラムで算出したいと思って調べたのですが、出てくるのは「何年何ヶ月と何日」というのを求めるものばかり…。
そこで、本記事では「任意の2つの日付間の経過年数と日数」をPHPで求める方法を考えたいと思います。
なおPHP独自のクラスや構文が出てきますが、アルゴリズムは他の言語でも活用できると思います。
前提条件
「任意の2つの日付」を、「基準日(1)」と「基準日(2)」と表すこととします。
なお基準日(1)は、基準日(2)より前であるものとします。
まず、基準日(1)ですが、同年7月7日の第22話でララの誕生日であることに触れられていること、キャラクター設定上も7月7日が誕生日とされていること、そしてひかると同じ「中学2年生」などなどから、第22話の放送の14年前に当たる「2005年7月7日」とします。
日付計算の際には、基準日(1)当日を含めないものとします。
PHPのバージョンは7.2.30を前提としていますが、7系であれば多分大丈夫だと思います。
考え方
基準日(2)によって、次の3パターンに分けます。
基準日(2)を、以下では$y_2年m_2月d_2日$とします。
そして先ほどの「2005年7月7日」は次のように設定します。
y_1 = 2005, m_1 = 7, d_1 = 7
基準日(1)と基準日(2)の月日が同じ場合
数式で表すと、$m_1 = m_2 かつ d_1 = d_2$の場合、となります。
この場合は単純に、$(y_2 - y_1)歳と0日$を返せば良いことになります。
例)基準日(2)が「2019年7月7日」の場合
年数:$2019 - 2005 = 14$
結果:14歳と0日
基準日(1)と基準日(2)の月日が違う場合(パターン1)
ここでは$y_2年m_1月d_1日$が基準日(2)より前の場合を取り上げます。
それ以外の場合は、パターン2を参照してください。
この場合は、年の計算は先ほどと同様に$(y_2 - y_1)$で計算します。
あとは日付を、$y_2年y_2月y_2日 - y_2年m_1月d_1日$(初日不算入)で計算します。
例)基準日(2)が「2019年7月26日」の場合:
年数:$2019 - 2005 = 14$
日数:$2019年7月26日 - 2019年7月7日 = 19$
結果:14歳と19日
例)基準日(2)が「2020年7月26日」の場合:
☆2020年は閏年
年数:$2020 - 2005 = 15$
日数:$2020年7月26日 - 2020年7月7日 = 19$
結果:15歳と19日
基準日(1)と基準日(2)の月日が違う場合(パターン2)
ここでは$y_2年m_1月d_1日$が基準日(2)より後の場合を取り上げます。
それ以外の場合は、パターン1を参照してください。
この場合、年の計算は$(y_2 - y_1 - 1)$になります。
日付ですが、$y_2年y_2月y_2日 - (y_2 - 1)年m_1月d_1日$(初日不算入)で計算します。
例)基準日(2)が「2019年4月27日」の場合
年数:$2019 - 2005 - 1 = 13$
日数:$2019年4月27日 - (2019-1)年7月7日 = 2019年4月27日 - 2018年7月7日 = 294$
結果:13歳と294日
例)基準日(2)が「2020年4月27日」の場合
☆2020年は閏年
年数:$2020 - 2005 - 1 = 14$
日数:$2020年4月27日 - (2020-1)年7月7日 = 2020年4月27日 - 2019年7月7日 = 295$
結果:14歳と295日
例)基準日(2)が「2019年2月27日」の場合
年数:$2019 - 2005 - 1 = 13$
日数:$2019年2月27日 - (2019-1)年7月7日 = 2019年2月27日 - 2018年7月7日 = 235$
結果:13歳と235日
例)基準日(2)が「2020年2月27日」の場合
☆2020年は閏年
年数:$2020 - 2005 - 1 = 14$
日数:$2020年2月27日 - (2020-1)年7月7日 = 2020年2月27日 - 2019年7月7日 = 235$
結果:14歳と235日
実装していきます
長いので折りたたむルン
<?php
/**
* dateDiffの戻り値クラス
*/
class DateDiffResult {
/** 年数 */
public $years;
/** 日数 */
public $days;
function __construct(int $years, int $days) {
$this->years = $years;
$this->days = $days;
}
}
/**
* 指定年月日の0時0分0秒のDateTimeを返す
*
* @param int $year 年
* @param int $month 月
* @param int $day 日
* @return DateTime
*/
function getDateTime(int $year, int $month, int $day) : DateTime {
$date = new DateTime();
$date->setDate($year, $month, $day);
$date->setTime(0, 0, 0);
return $date;
}
/**
* 基準日(1)から基準日(2)まで、何年と何日経過したかを求める(初日不算入)
*
* @param DateTime $inputDate1 基準日(1)
* @param DateTime $inputDate2 基準日(2)
* @return DateDiffResult
*/
function dateDiff(DateTime $inputDate1, DateTime $inputDate2) : DateDiffResult {
// 各変数をセットする
$y1 = intval($inputDate1->format("Y"));
$m1 = intval($inputDate1->format("n"));
$d1 = intval($inputDate1->format("j"));
$y2 = intval($inputDate2->format("Y"));
$m2 = intval($inputDate2->format("n"));
$d2 = intval($inputDate2->format("j"));
$date1 = getDateTime($y1, $m1, $d1);
$date2 = getDateTime($y2, $m2, $d2);
// 基準日(1)と基準日(2)の月日が同じ場合
if ($m1 == $m2 && $d1 == $d2) {
return new DateDiffResult($y2 - $y1, 0);
}
// y_2年m_1月d_1日
$date3 = getDateTime($y2, $m1, $d1);
// y_2年y_2月y_2日 - y_2年m_1月d_1日
$diff1 = $date3->diff($date2);
if ($diff1->format("%R") == "+") {
// y_2年m_1月d_1日が基準日(2)より前の場合
return new DateDiffResult($y2 - $y1, intval($diff1->format("%a")));
} else {
// y_2年m_1月d_1日が基準日(2)より後の場合
$date4 = getDateTime($y2 - 1, $m1, $d1);
$diff2 = $date4->diff($date2);
return new DateDiffResult($y2 - $y1 - 1, intval($diff2->format("%a")));
}
}
テストします
長いので折りたたむルン
<?php
require_once("datediff.php");
use PHPUnit\Framework\TestCase;
class DateDiffTest extends TestCase {
// 基準日(1)と基準日(2)の月日が同じ場合
public function testDiff0()
{
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2019, 7, 7);
$actual = dateDiff($date1, $date2);
$this->assertEquals(14, $actual->years);
$this->assertEquals(0, $actual->days);
}
// 基準日(1)と基準日(2)の月日が違う場合(パターン1)その1
public function testDiff1_1() {
/**
* 基準日(2)が「2019年7月26日」の場合:
* 年数:$2019 - 2005 = 14$
* 日数:$2019年7月26日 - 2019年7月7日 = 19$
* 結果:14歳と19日
*/
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2019, 7, 26);
$actual = dateDiff($date1, $date2);
$this->assertEquals(14, $actual->years);
$this->assertEquals(19, $actual->days);
}
// 基準日(1)と基準日(2)の月日が違う場合(パターン1)その2
public function testDiff1_2() {
/**
* 基準日(2)が「2020年7月26日」の場合:
* ☆2020年は閏年
* 年数:$2020 - 2005 = 15$
* 日数:$2020年7月26日 - 2020年7月7日 = 19$
* 結果:15歳と19日
*/
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2020, 7, 26);
$actual = dateDiff($date1, $date2);
$this->assertEquals(15, $actual->years);
$this->assertEquals(19, $actual->days);
}
// 基準日(1)と基準日(2)の月日が違う場合(パターン2)その1
public function testDiff2_1() {
/**
* 基準日(2)が「2019年4月27日」の場合
* 年数:$2019 - 2005 - 1 = 13$
* 日数:$2019年4月27日 - (2019-1)年7月7日 = 2019年4月27日 - 2018年7月7日 = 294$
* 結果:13歳と294日
*/
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2019, 4, 27);
$actual = dateDiff($date1, $date2);
$this->assertEquals(13, $actual->years);
$this->assertEquals(294, $actual->days);
}
// 基準日(1)と基準日(2)の月日が違う場合(パターン2)その2
public function testDiff2_2() {
/**
* 基準日(2)が「2020年4月27日」の場合
* ☆2020年は閏年
* 年数:$2020 - 2005 - 1 = 14$
* 日数:$2020年4月27日 - (2020-1)年7月7日 = 2020年4月27日 - 2019年7月7日 = 295$
* 結果:14歳と295日
*/
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2020, 4, 27);
$actual = dateDiff($date1, $date2);
$this->assertEquals(14, $actual->years);
$this->assertEquals(295, $actual->days);
}
// 基準日(1)と基準日(2)の月日が違う場合(パターン2)その3
public function testDiff2_3() {
/**
* 基準日(2)が「2019年2月27日」の場合
* 年数:$2019 - 2005 - 1 = 13$
* 日数:$2019年2月27日 - (2019-1)年7月7日 = 2019年2月27日 - 2018年7月7日 = 235$
* 結果:13歳と235日
*/
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2019, 2, 27);
$actual = dateDiff($date1, $date2);
$this->assertEquals(13, $actual->years);
$this->assertEquals(235, $actual->days);
}
// 基準日(1)と基準日(2)の月日が違う場合(パターン2)その4
public function testDiff2_4() {
/**
* 基準日(2)が「2020年2月27日」の場合
* ☆2020年は閏年
* 年数:$2020 - 2005 - 1 = 14$
* 日数:$2020年2月27日 - (2020-1)年7月7日 = 2020年2月27日 - 2019年7月7日 = 235$
* 結果:14歳と235日
*/
$date1 = getDateTime(2005, 7, 7);
$date2 = getDateTime(2020, 2, 27);
$actual = dateDiff($date1, $date2);
$this->assertEquals(14, $actual->years);
$this->assertEquals(235, $actual->days);
}
// 閏年チェック(1)
public function testDiff_uruu_1() {
$date1 = getDateTime(2020, 2, 28);
$date2 = getDateTime(2020, 3, 1);
$actual = dateDiff($date1, $date2);
$this->assertEquals(0, $actual->years);
$this->assertEquals(2, $actual->days);
}
// 閏年チェック(2)
public function testDiff_uruu_2() {
$date1 = getDateTime(2020, 2, 28);
$date2 = getDateTime(2021, 2, 1);
$actual = dateDiff($date1, $date2);
$this->assertEquals(0, $actual->years);
$this->assertEquals(339, $actual->days);
}
}
作りました
ここまでのものを使って、hokkaidosm.net プリキュアデータ内にララちゃんっていくつなの?を追加しました。
Mastodonでは毎日午前0時に配信予定です。
2020/08/01追記分
7月27日(リリース翌日)の午前0時過ぎ…
「15歳と19日です」(2020年7月26日現在)
ララ「オヨ!?何で昨日の日付ルン!?」
その日の昼…
「15歳と20日です」(2020年7月27日現在)
ララ「オヨ!?昼になったら今日の日付になってるルン!」
ララ「でも、Mastodonはちゃんと0時に今日のデータになってるルン」
原因を調べてみる
Mastodonを投稿するPHPスクリプト2は、次のcron文で呼び出しています。
0 0 * * * /usr/bin/php /var/www/html_precure/star_twincle/lala_age/toot.php >/dev/null 2>&1
一方で、Apacheの設定は次のようになっています(重要な部分だけ抜き出しています)。
<VirtualHost *:443>
ServerName precure.hokkaidosm.net
DocumentRoot /var/www/html_precure
DirectoryIndex index.php index.html
<Directory "/var/www/html_precure">
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:9072"
</FilesMatch>
</Directory>
</VirtualHost>
そしてこの 127.0.0.1:9072
の正体はこちらです。
listen = 127.0.0.1:9072
ということで、それぞれのデフォルトタイムゾーンを確認してみます。
[hokkaidosm@ik1-324-22250 ~]$ /usr/bin/php -i | grep "Default timezone"
Default timezone => Asia/Tokyo
[hokkaidosm@ik1-324-22250 ~]$ /opt/remi/php72/root/usr/bin/php -i | grep "Default timezone"
Default timezone => UTC
つまり、
- cronで実行する場合:タイムゾーンは日本時間「Asia/Tokyo」
- サイトへのアクセス:タイムゾーンはUTC(日本時間より9時間遅い)
となっていたわけです。
変えてみた
そこで、 getDateTime
を変更しました。
/**
* 指定年月日の0時0分0秒のDateTimeを返す
*
* @param int $year 年
* @param int $month 月
* @param int $day 日
* @return DateTime
*/
function getDateTime(int $year, int $month, int $day) : DateTime {
// $date = new DateTime(); // これを…
$date = new DateTime("now", new DateTimeZone('Asia/Tokyo')); // こう変えた
$date->setDate($year, $month, $day);
$date->setTime(0, 0, 0);
return $date;
}
これで上手くいきました。
ひかる「タイムゾーンには注意しないとね」
ララ「ルン!」
※ちなみに、同じ環境で正常に動いているシステムがあることから、そちらへの影響を考えた結果、デフォルトタイムゾーンを変える、ということはしないこととしました。
-
ひかるがララを呼ぶときに「ララ」と呼ぶのは、第3話の最後のほうからです。 ↩
-
https://precure.hokkaidosm.net/star_twincle/lala_age/toot.php にはアクセスできないように設定しています。 ↩