ララ「13歳と294日って、どうやって求めるルン？」 on PHP

# はじめに

「スター☆トゥインクルプリキュア」第2話（2019年2月10日放送）で、こんなやりとりがありました。

> ひかる「ララちゃん[^chan]ってすごいよねぇ。ロケット操縦して、修理までしちゃうんだもん」
ララ「いい大人ルン。できて当然ルン」
ひかる「えっ？大人って？いくつなの？」
ララ「地球の年齢だと…」
AI「13歳と294日です」
ひかる「わたしと同い年じゃん！」

[^chan]: ひかるがララを呼ぶときに「ララ」と呼ぶのは、第3話の最後のほうからです。

さて、今ララの年齢は、地球基準で何歳と何日なのでしょうか？

それをプログラムで算出したいと思って調べたのですが、出てくるのは「何年何ヶ月と何日」というのを求めるものばかり…。
そこで、本記事では「任意の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日」は次のように設定します。

```math
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](#基準日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](#基準日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日

# 実装していきます

<details><summary>長いので折りたたむルン</summary><div>

```php:datediff.php
<?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")));
    }
}
```
</div></details>

# テストします

<details><summary>長いので折りたたむルン</summary><div>

```php:datedifftest.php
<?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);
    }
}
```
</div></details>

# 作りました

ここまでのものを使って、[hokkaidosm.net プリキュアデータ](https://precure.hokkaidosm.net/)内に[ララちゃんっていくつなの？](https://precure.hokkaidosm.net/star_twincle/lala_age/)を追加しました。
[Mastodon](https://mstdn.hokkaidosm.net/@lala_age)では毎日午前0時に配信予定です。

# 2020/08/01追記分

7月**27日**（リリース翌日）の午前0時過ぎ…

「15歳と19日です」（2020年7月**26日**現在）

ララ「オヨ！？何で**昨日の日付**ルン！？」

その日の昼…

「15歳と20日です」（2020年7月**27日**現在）

ララ「オヨ！？昼になったら**今日の日付**になってるルン！」
ララ「でも、[Mastodon](https://mstdn.hokkaidosm.net/@lala_age/104580749867362209)はちゃんと0時に今日のデータになってるルン」

## 原因を調べてみる

Mastodonを投稿するPHPスクリプト[^2]は、次のcron文で呼び出しています。

```
0 0 * * * /usr/bin/php /var/www/html_precure/star_twincle/lala_age/toot.php >/dev/null 2>&1
```

[^2]: https://precure.hokkaidosm.net/star_twincle/lala_age/toot.php にはアクセスできないように設定しています。

一方で、Apacheの設定は次のようになっています（重要な部分だけ抜き出しています）。

```conf:/etc/httpd/conf.d/vhost.conf
<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` の正体はこちらです。

```conf:/etc/opt/remi/php72/php-fpm.d/www.conf
listen = 127.0.0.1:9072
```

ということで、それぞれのデフォルトタイムゾーンを確認してみます。

```sh
[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` を変更しました。

```php:datediff.php
/**
 * 指定年月日の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;
}
```

これで上手くいきました。

ひかる「タイムゾーンには注意しないとね」
ララ「ルン！」

※ちなみに、同じ環境で正常に動いているシステムがあることから、そちらへの影響を考えた結果、デフォルトタイムゾーンを変える、ということはしないこととしました。