日付と時刻

2038年問題、タイムゾーン、サマータイム
ウーラニアー世界時計
ウーラニアー世界時計
データ属性としての日付・時刻は単純な数値ではなく、2038 年問題という制約や、タイムゾーン、サマータイムを意識する必要がある。

西暦年月日時分秒、時差、サマータイムの全てを表す標準書式として、ISO 8601がある。Python や PHP には、ISO 8601を扱う仕組みが用意されているが、2038 年問題の制約を受けることから、±100 万年を扱える PHP用のクラスを作成する。

また、PHP による西暦と元号の相互変換クラスは「PHP でテキスト中の和暦・西暦年号を統一する(その 2)」を、旧暦計算クラスは「PHP で 3 ヶ月カレンダーを作る」を参考にしてほしい。

サンプル・プログラムのダウンロード

圧縮ファイルの内容
pahooDataStructure.phpデータ構造に関わるPHPのクラス。include_pathが通ったディレクトリに配置すること。
dt02-21-01.pyPython:ISO 8601日付・時刻の計算
dt02-21-02.phpPHP:ISO 8601日付・時刻の計算
dt02-21-03.phpPHP:生存日数、死亡予定日を計算
dt02-21-04.phpPHP:現地到着日時を計算
dt02-21-05.phpPHP:現地到着日時を計算(サマータイムを含む)

日付を表す文字列

「2018 年 6 月 29 日」「2018/06/29」「平成 30 年 6 月 29 日」は、すべて同じ日付を表す。日付の表記方式は数値のように統一されていないので、identity としての日付を格納したいだけであれば、データ属性を文字列としておけばいいだろう。

しかし、日付には「2018 年 6 月 29 日から 10 日は何月何日?」というように「quantitation として扱う場合がある。この場合は、計算可能なデータ属性にしておかなければならない。時刻のデータ属性を含めて後述する。

時刻を表す文字列

「午後 10 時 35 分」「24 時 25 分」「24:25」が、すべて同じ時刻を表す。
時刻も日付と同様、表記方式が統一されておらず、identity としての時刻を格納したいだけであれば、データ属性を文字列としておけばいいだろう。
しかし、日付にも「24 時 25 分から 5 時間後は何時何分」というように「quantitation として扱う場合がある。この場合は、計算可能なデータ属性にしておかなければならない。

さらに時刻の計算を行う場合、わが国にいる限りは意識しなくて済むが、時差」やサマータイムも意識しなくてはいけない。

quantitation としての日付・時刻

日付や時刻を計算可能なデータ属性にするには、まず、表記方式を統一する必要がある。
日付・時刻を表記する国際規格として ISO 8601 がある。これを使った表記例を下表に示す。最近は、インターネットでよく見かける表記方式で、ここでも ISO 8601 を利用することにする。

時刻の後ろに Z を添えることで UTC(協定世界時)を、また +/- で UTC からの時差を表現することで各地のローカル時刻を表現することができる。
コンピュータ内部では文字列として保持すれば十分だが、データ属性が日付時刻であることを意識する必要がある。
ISO 8601 表記
ISO 8601 表記 日本語表記
2018 西暦2018年
2018-07 西暦2018年7月
2018-07-12 西暦2006年7月12日
2018-07-12T23:20Z 西暦2006年7月12日23時20分UTC
2018-07-21T23:20:52Z 西暦2006年7月21日23時20分52秒UTC
2018-07-21T23:20:50.32Z 西暦2006年7月21日23時20分50.32秒UTC
2018-07-19T16:10:40.22-08:00 西暦2006年7月19日16時10分40.22秒
(米太平洋標準時間;UTCから-8時間の時差)
2018-07-19T16:10:40.22+09:00 西暦2006年7月19日16時10分40.22秒
(日本標準時間;UTCから+9時間の時差)
2006-01-01T08:59:60+09:00 西暦2006年1月1日8時59分60秒
(日本標準時間;閏秒を示す)

Pythonによる日付・時刻計算

Python には ISO8601 パッケージが用意されている。
導入は、コマンドラインから

pip install iso8601


とすればいい。

0009: from datetime import *
0010: import sysiso8601
0011: 
0012: sys.stdout.write("\n")
0013: 
0014: # 2018年6月29日の7日後
0015: str = '2018-06-29'
0016: dd  = 7
0017: if (dd > 0):
0018:     sg = ''
0019: else:
0020:     sg = ''
0021: dt = iso8601.parse_date(str)
0022: dt = dt + timedelta(days=1) * dd
0023: sys.stdout.write("{} ".format(str))
0024: sys.stdout.write(sg)
0025: sys.stdout.write(" {} days = ".format(abs(dd), '%d'))
0026: sys.stdout.write("{}\n".format(dt.strftime('%Y/%m/%d')))
0027: 
0028: # 2018年6月29日8:50の9時間前
0029: str = '2018-06-29T08:50'
0030: dd  = -9
0031: if (dd > 0):
0032:     sg = ''
0033: else:
0034:     sg = ''
0035: dt = iso8601.parse_date(str)
0036: dt = dt + timedelta(hours=1) * dd
0037: sys.stdout.write("{} ".format(str))
0038: sys.stdout.write(sg)
0039: sys.stdout.write(" {} hours = ".format(abs(dd), '%d'))
0040: sys.stdout.write("{}\n".format(dt.strftime('%Y-%m-%d %H:%M')))
0041: 
0042: sys.stdout.write("\n")

PythonでISO 8601日付・時刻の計算
サンプル・プログラム "dt02-21-01.py" の実行結果は上図の通りである。
ISO8601 パッケージに用意されているメソッド iso8601.parse_date により、ISO 8601 形式の日付時刻文字列を datetimeオブジェクトに変換する。

日付計算の制約

Python の datetimeオブジェクトが通用するのは、1970 年から 2038 年のまでの範囲である。他の多くの処理系でも、日付計算できる範囲は同様の制約を受ける。
これは、日付を内部的に秒数で保持しており、32 ビット整数で表現できる年数が±68 年という制約による。
また、UNIX 処理系では 1970 年 1 月 1 日 0 時 0 分 0秒からの累積秒数(UNIX epoch)を保持しており、今日でも多くの処理系がこれに倣っている。
これらの制約を2038 年問題と呼ぶ

PHPによる日時クラスの作成

PHP の組み込み関数  strtotime  は ISO 8601 を解釈し、UNIX epoch に変換できる。しかし、Phthon と同様、1970 年から 2038 年のまでの範囲の制約を受ける。

そこで、少なくとも±100 万年の日時計算ができるユーザー・クラス pahooDateTime を作成した。次のメソッドを備えている。
メソッド機能
datetime()年月日時分秒をセットする。
delta()日時の差分を計算する。
sub()$dtを減算した結果を返す。
getTimezone()タイムゾーン識別子によりタイムゾーンのオフセット値を取得する。
tzone()タイムゾーンを変更する。
parse_date()ISO 8601 日時フォーマットの解釈。
date()日時フォーマットで文字列に変換する。
isleap()閏年かどうか判定する。
getDaysInMonth()指定した月の日数を返す。
verify()年月日時分秒の各値が正しいかどうか検査する。

 

先ほどの Python プログラムを PHP に移植すると、"dt02-21-02.php" のようになる。

0011: //データ構造クラス
0012: require_once('pahooDataStructure.php');
0013: $dt = new pahooDateTime();                   //pahooDateTimeオブジェクト用意
0014: 
0015: printf("\n");
0016: 
0017: // 2018年6月29日の7日後
0018: $str = '2019-06-29';
0019: $dd  = 7;
0020: $sg = '';
0021: $dt->parse_date($str);
0022: printf("%s", $dt->date('Y-m-d'));
0023: printf(" %s %d days = ", $sg$dd);
0024: $dt->delta(($sg == '' ? (+1) : (-1)), 0, 0, $dd);
0025: printf("%s\n", $dt->date('Y-m-d'));
0026: 
0027: // 2018年6月29日8:50の9時間前
0028: $str = '2019-06-29T08:50+09:00';
0029: $dd  = 9;
0030: $sg = '';
0031: $dt->parse_date($str);
0032: printf("%s", $dt->date('Y-m-d g:i:s'));
0033: printf(" %s %d hours = ", $sg$dd);
0034: $dt->delta(($sg == '' ? (+1) : (-1)), 0, 0, 0, $dd);
0035: printf("%s\n", $dt->date('Y-m-d g:i:s'));

カレンダー計算

0011: //データ構造クラス
0012: require_once('pahooDataStructure.php');
0013: 
0014: //pahooDateTimeオブジェクト用意
0015: $dt0 = new pahooDateTime();
0016: $dt1 = new pahooDateTime();
0017: 
0018: $birth = '1954-09-21';            //誕生日
0019: $dt0->parse_date($birth);
0020: $today = date('Y-m-d');      //現在日付
0021: $dt1->parse_date($today);
0022: $birth = '1954-09-21';            //誕生日
0023: $life = 80.98;                    //平均余命
0024: 
0025: //生存日数を計算
0026: list($dd$ss) = $dt1->sub($dt0);
0027: printf("\n%sから %sまで %d日\n", $birth$today$dd);
0028: 
0029: //死亡予定日を計算
0030: $dt0->delta(+1, $life);
0031: printf("%s 死亡見込み\n", $dt0->date('Y-m-d'));

PHPで生存日数、死亡予定日を計算
サンプル・プログラム "dt02-21-03.php" の実行結果は左図の通りである。
2038 年問題の制約を受けないことから、誕生日から現在までの生存日数や、誕生日に平均余命を加算することを死亡予定日を計算することもできる。あの超人ロックの年数計算だってできますよ😀

pahooDateTime クラス は、年月日と時分秒を別々に計算することで2038 年問題回避している。また、年・月・日・時・分・秒・タイムゾーン(オフセット秒数)を別々のクラス内変数に保持するようにした。100 万年=1000000 年×365.25 日=3 億 6525 万日である。一方、32 ビットで表せる整数は±21 億 4748 万 3647である。

現地到着日時を計算

0011: //データ構造クラス
0012: require_once('pahooDataStructure.php');
0013: 
0014: //pahooDateTimeオブジェクト用意
0015: $dt = new pahooDateTime();
0016: 
0017: $start = '2018-07-01T21:45+09:00';            //成田空港出発日時
0018: $tz = '-10:00';                           //ハワイのタイムゾーン
0019: $hour = 7;   $min = 30;                       //飛行時間
0020: 
0021: $dt->parse_date($start);
0022: printf("\n出発:%s\n", $dt->date('c'));
0023: 
0024: $dt->delta(+1, 0, 0, 0, $hour$min);      //飛行時間を加算
0025: $dt->tzone($tz);                          //タイムゾーン変更
0026: printf("到着:%s\n", $dt->date('c'));

PHPで現地到着日時を計算
サンプル・プログラム "dt02-21-04.php" の実行結果は左図の通りである。
pahooDateTime クラス はタイムゾーンも保持しており、成田空港からハワイへ飛行機で飛び、到着時の現地時刻を計算するのも実質2行で実装できる。

24時制と12時制

0698:         $sec -= 60;
0699:         $min++;
0700:     }
0701:     while ($sec < 0) {
0702:         $sec += 60;
0703:         $min--;
0704:     }
0705:     while ($min >= 60) {
0706:         $min -= 60;
0707:         $hour++;
0708:     }

コンピュータでは、12 時制の場合、1~12 の自然数しか使わない。したがって、深夜 00:00 は AM12:00、正午 12:00 は PM12:00、13:00 は PM1:00 となる。
なぜゼロを使わないかというと、アナログ時計を見れば分かる。ヨーロッパで 12 時制の時計が作られ始めていた当時、インドからゼロの概念が入っていなかったためだ。

サマータイム

daylight saving time = サマータイム
2020 年開催の東京オリンピック・パラリンピックを目指し、わが国でもサマータイム(夏時間;summer time)の導入が検討されている。アメリカでは DST(daylight saving time)と呼ばれる。

現在、サマータイムを導入している国は、ヨーロッパを中心に約 70 カ国ある。
たとえばイギリスでは、毎年 3 月の最終日曜日 1 時(UTC)から 10 月の最終日曜日 1 時(UTC)までの期間、UTC より 1 時間だけ早めるイギリス夏時間(BST;British Summer Time)を実施している。
たとえば 2018 年 3 月 25 日(日)の 1 時になった瞬間、時計は 2 時を指す。そして、10 月 28 日(日)の 2 時になった瞬間、時計は 1 時に戻る。
一方、アラスカでは、毎年 4 月の第一日曜日 2 時から 10 月の最終日曜日 2 時までをアラスカ夏時間(AKDT;Alaska Daylight Time)としている。
このように、国や地域によってサマータイムの期間が異なる

さらに、イギリスでは、第二次大戦中の 1940 年 2 月 15 日(木)2 時(UTC)から 1945 年 10 月 7 日(日)2 時(UTC)までの間、2 時間のサマータイムを行っていた。
このように、サマータイムは1 時間とは限らないこと、そして年代によっては期間や時間が異なる。ある時期にサマータイムを廃止した国もあるし、新規に導入した国もある。

わが国も、GHQ の指導により、1948 年 4 月 28 日に夏時刻法を公布し、サマータイムを導入した時期がある。実施期間は、1948 年 5 月 2 日(日)0 時~9 月 12 日(日)1 時、1949 年 4 月 3 日(日)0 時~1949 年 9 月 11 日(日)1 時、1950 年 5 月 7 日(日)0 時~1950 年 9 月 10 日(日)1 時、1951 年 5 月 6 日(日)0 時~1951 年 9 月 8 日(土)1 時という変則的なものだった。

このようにローカルな条件が多いため、ISO 8601 ではサマータイムを定義しておらず、時差で表現することを推奨している。
サマータイム表記
ISO 8601 表記 日本語表記
2018-03-25T01:30Z 西暦2018年3月25日01時30分UTC
2018-03-25T02:30+01:00 西暦2018年3月25日02時30分
(イギリス夏時間BST;UTCから+1時間の時差)
コンピュータ・システムへのサマータイムの実装は、千差万別である。
Windows 7 以降の Windows や macOS、Linux では、時刻の内部管理に UTC を用いており、OS 側でサマータイムを自動的に補正している。古い OS や、FAT のような古いファイル管理システムでは、タイムスタンプをローカル時刻で記録しているため、サマータイムとのズレが発生してしまう。

サマータイムを実施すると、期間初日は 23 時間しかなく、最終日は 25 時間あることになる。ローカルタイムで日時計算を行うと、とくに最終日は 1 時間ないし 2 時間時刻が重なることになり、計算結果がおかしなことになる。そこで、かならず UTC に換算して計算する必要がある。
よって、日時のデータ構造は、UTC で行うか、ISO 8601 のように UTC との時差を明記した形で行うべきである。

現地到着日時を計算(サマータイム編)

サンプル・プログラム "dt02-21-04.php" では時差を含めた現地到着日時を計算したが、メソッド tzone に渡すタイムゾーンを変更することで、サマータイムにおける現地到着日時を計算することができる。
ただ、前述の通り、サマータイムの開始・終了日や時差は地域によって異なることから、これをいちいち指定するのは面倒だ。そこで、'Asia/Tokyo' などシステムでよく使われるタイムゾーン識別子を与えることで、時差とサマータイムの有無を織り込んで現地到着日時を計算するサンプル・プログラム "dt02-21-05.php" を作成する。

0562: /**
0563:  * タイムゾーンを取得する
0564:  * @param string $timezone タイムゾーン識別子('Asia/Tokyo' など)
0565:  * @return string タイムゾーン(+|-hh:mm形式)/FALSE 定義域外
0566: */
0567: function getTimezone($timezone) {
0568:     $dtz = new DateTimeZone($timezone);
0569:     if ($dtz == FALSE) {
0570:         $this->error = TRUE;
0571:         $this->errmsg = 'TZONE: illegal';
0572:         return FALSE;
0573:     }
0574:     $dt  = new DateTime();
0575:     $dt->setTimezone($dtz);
0576:     if ($dt->setDate($this->year$this->month$this->day) == FALSE) {
0577:         $this->error = TRUE;
0578:         $this->errmsg = 'TZONE: illegal';
0579:         return FALSE;
0580:     }
0581:     if ($dt->setTime($this->hour$this->minuite$this->second) == FALSE) {
0582:         $this->error = TRUE;
0583:         $this->errmsg = 'TZONE: illegal';
0584:         return FALSE;
0585:     }
0586: 
0587:     $res = $dt->format('P');
0588:     if ($dt->format('Z') > 0)    $res = '+' . $res;
0589: 
0590:     $dtz = NULL;
0591:     $dt  = NULL;
0592: 
0593:     return $res;
0594: }

メソッド getTimezone は、先ほど使ったメソッド tzone の引数を求めるものだが、pahooDateTime インスタンスに記録されている年月日を見て、サマータイムかどうかを自動的に判断する。
タイムゾーン識別子から時差を求める処理は、PHP の DateTimeZone クラスと DateTime クラスを呼び出して実行している。

0014: //pahooDateTimeオブジェクト用意
0015: $dt = new pahooDateTime();
0016: 
0017: //タイムゾーン識別子
0018: $tokyo  = 'Asia/Tokyo';
0019: $london = 'Europe/London';
0020: 
0021: //飛行時間:羽田→ヒースロー
0022: $hour = 12;  $min = 30;
0023: 
0024: //通常時間
0025: $dt->datetime(2018, 1, 6, 11, 50, 0, $tokyo); //羽田空港出発日時
0026: printf("\n出発:%s\n", $dt->date('c'));
0027: $dt->delta(+1, 0, 0, 0, $hour$min);          //飛行時間を加算
0028: $tz = $dt->getTimezone($london);              //イギリス標準時
0029: $dt->tzone($tz);                              //タイムゾーン変更
0030: printf("到着:%s\n", $dt->date('c'));
0031: 
0032: //夏時間
0033: $dt->datetime(2018, 7, 1, 11, 50, 0, $tokyo); //羽田空港出発日時
0034: printf("\n出発:%s\n", $dt->date('c'));
0035: $dt->delta(+1, 0, 0, 0, $hour$min);          //飛行時間を加算
0036: $tz = $dt->getTimezone($london);              //イギリス標準時
0037: $dt->tzone($tz);                              //タイムゾーン変更
0038: printf("到着:%s\n", $dt->date('c'));

PHPで現地到着日時を計算(サマータイム編)
サマータイムがあるイギリス・ロンドンへの到着時刻を求めるサンプル・プログラム "dt02-21-05.php" の実行例は左図の通りだ。1 月 6 日出発と 7 月 1 日出発とでは、日本にはサマータイムがないが、イギリスにはあるため、現地到着時刻が 1 時間変化する。

参考サイト

参考書籍

表紙 時計の科学 人と時間の5000年の歴史
著者 織田 一朗
出版社 講談社
サイズ 新書
発売日 2017年12月13日
価格 1,058円(税込)
rakuten
ISBN 9784065020418
人類が「時間」の存在に気づいたのは、いまから5000年以上も前のことです。太陽の動きを利用した「日時計」から始まり、周期を人工的につくりだす「機械式時計」の誕生、精度に革命を起こした「クオーツ時計」、そして、時間の概念を変えた「原子時計」まで。時代の最先端技術がつぎ込まれた時計の歴史を余すところなく解説します。
 
(この項おわり)
header