UNIX時間という、1970年1月1日からの経過秒数を使って日付時刻を表示する手法があります。ただ、この大きな数字だけを見てもこれが一体何年何月何日なのかを知ることはできません。困りましたね。
世の中のモダンなフレームワークや処理系には、このUNIX時間とカレンダー形式を変換する機能を備えています。例えば.NET FrameworkではDateTimeOffset.FromUnixTimeSecondsメソッドで簡単に変換できます。C言語には仕様上そういうものはないですが、そもそもUNIX時間とはUNIX用のC言語処理系でtime.hで定義されているtime_t型がそのように時間を表現していたことに由来します。ですので、UNIX含め、他の処理系でもtime_t型の中身がUNIX時間として使える処理系も結構あったりします。
一方で、そのtime_tをUNIX時間として扱っている処理系の中には2038年問題を抱えているものもあります。 time_tを符号付32bit整数として定義してしまっているがために、2038年にオーバーフローしてしまうというものです。私が子供のころにはまだまだ先だなと思っていたものですが、気づいたらもう13年を切ってしまいましたね。時の流れは早いものです。
この2038年問題の回避をするために手っ取り早い方法はtime_tを符号付64bit整数型を使うことです。ただ、処理系として対応していない場合はオレオレ実装をしなければならず、結構厄介です。特に、1970年1月1日からの日数をカレンダーに変換しようとしたときにはうるう年という強敵が立ちはだかります。面倒ですね。
というわけで、C#でオレオレ実装を作ってみました。実際はマイコンで使いたかったので、型名や参照ポインタなどの簡単な置換でC言語でも動かせることを意識して実装しています。
カレンダー構造体の実装
まずは、カレンダー構造体ことstruct tmのオレオレ実装をしておきます。64bitを想定してtm64としました。
public struct tm64
{
public byte tm_sec; // 秒
public byte tm_min; // 分
public byte tm_hour; // 時
public byte tm_mday; // 日
public byte tm_mon; // 月
public ushort tm_year; // 年
public byte tm_wday; // 曜日 [0:日 1:月 ... 6:土]
}
tm_yday(年始からの経過日数)はめんどいので実装を省いています。また、tm_yearはC言語では1900年からの経過年数となっていますが、今回はわかりやすく西暦そのままで表現することとします。tm_monは、C言語では0~11で表現されます。なぜ現実世界の暦が0月0日からスタートではないのかという話は置いておいて、これもバグの温床であることは歴史が物語っているので、今回は1~12での表現としておきます。
ちなみにtm_yearをushort型とした場合は西暦約6万5千年までしか表現できないから64bitを生かしきれないのでは?と思うかもしれません。実際、time_tを64bitにすることで原理的には西暦3000億年くらいまで正常に時間をカウントできるようになります。それより小さい型を使うなんてナンセンスだ!と思うかもしれませんが、そもそも西暦6万5千年まで僕の作ったソフトを使う人なんていないので考えないことにしておきます(そうやって2038年問題が再生産されるのであった…)。
UNIX時間→カレンダーへの変換
さて、まずはUNIX時間からカレンダーへの変換を実装していきます。
public static void Clock_ToTm(ulong time, ref tm64 tm)
{
Clock_ToTm_Date((uint)(time / 86400), ref tm);
Clock_ToTm_Time((uint)(time % 86400), ref tm);
}
private static void Clock_ToTm_Date(uint dayfrom1970, ref tm64 tm)
{
tm.tm_wday = (byte)((dayfrom1970 + 4) % 7); // 1970/1/1は木曜日
uint dayfrom16000301 = dayfrom1970 + 135080; // 1600/3/1基準で考える
ushort year_div400 = (ushort)(dayfrom16000301 / 146097); // 400年単位が何回あったか
uint day400y = dayfrom16000301 % 146097; // 400年単位で3/1からの日数
if(day400y == 146096) { // 最後の日のうるう日
tm.tm_year = (ushort)(1600 + (year_div400 + 1) * 400);
tm.tm_mon = 2; //2月
tm.tm_mday = 29; //29日
} else {
byte year_div100 = (byte)(day400y / 36524); // 100年単位が何回あったか
ushort day100y = (ushort)(day400y % 36524); // 100年単位で3/1からの日数
byte year_div4 = (byte)(day100y / 1461); // 4年単位が何回あったか
ushort day4y = (ushort)(day100y % 1461); // 4年単位で3/1からの日数
if(day4y == 1460) { // 最後のうるう日
tm.tm_year = (ushort)(1600 + year_div400 * 400 + year_div100 * 100 + (year_div4 + 1) * 4);
tm.tm_mon = 2; //2月
tm.tm_mday = 29; //29日
} else {
byte year_div1 = (byte)(day4y / 365); // 1年単位が何回あったか
ushort day1y = (ushort)(day4y % 365); // 1年単位で3/1からの日数
tm.tm_year = (ushort)(1600 + year_div400 * 400 + year_div100 * 100 + year_div4 * 4 + year_div1);
if(day1y < 184) {
if(day1y < 92) {
if(day1y < 31) {
tm.tm_mon = 3; // 3月
tm.tm_mday = (byte)(day1y + 1);
} else if(day1y < 61) {
tm.tm_mon = 4; // 4月
tm.tm_mday = (byte)(day1y - 30);
} else {
tm.tm_mon = 5; // 5月
tm.tm_mday = (byte)(day1y - 60);
}
} else {
if(day1y < 122) {
tm.tm_mon = 6; // 6月
tm.tm_mday = (byte)(day1y - 91);
} else if(day1y < 153) {
tm.tm_mon = 7; // 7月
tm.tm_mday = (byte)(day1y - 121);
} else {
tm.tm_mon = 8; // 8月
tm.tm_mday = (byte)(day1y - 152);
}
}
} else {
if(day1y < 275) {
if(day1y < 214) {
tm.tm_mon = 9; // 9月
tm.tm_mday = (byte)(day1y - 183);
} else if(day1y < 245) {
tm.tm_mon = 10; // 10月
tm.tm_mday = (byte)(day1y - 213);
} else {
tm.tm_mon = 11; // 11月
tm.tm_mday = (byte)(day1y - 244);
}
} else {
if(day1y < 306) {
tm.tm_mon = 12; // 12月
tm.tm_mday = (byte)(day1y - 274);
} else if(day1y < 337) {
tm.tm_year++;
tm.tm_mon = 1; // 1月
tm.tm_mday = (byte)(day1y - 305);
} else {
tm.tm_year++;
tm.tm_mon = 2; // 2月
tm.tm_mday = (byte)(day1y - 336);
}
}
}
}
}
}
private static void Clock_ToTm_Time(uint daysec, ref tm64 tm)
{
ushort daymin = (ushort)(daysec / 60);
tm.tm_sec = (byte)(daysec % 60);
tm.tm_min = (byte)(daymin % 60);
tm.tm_hour = (byte)(daymin / 60);
}
UNIX時間を86400で割った余り、すなわち時分秒部分は超シンプルなので説明は割愛します。残りの1970年1月1日からの日数をカレンダーに変換する方法についてしっかり説明していきます。
まず、うるう年というのは4年に1回(西暦が4で割り切れる年)にやってきますが、西暦が100で割り切れる年はうるう年になりません。ただし、西暦が400で割り切れる年はうるう年となります。なぜこんなに面倒くさいルールなのかと言えば、地球が太陽の周りを公転するのにかかる時間が365.24219日だからです。うるう年を400年で97回にすることで、この端数に近似しようとしているんですね。
うるう日は2月29日に挿入されるので、400年ごとということも踏まえて、1600年3月1日からの日数を数えることにしましょう。400年は146,097日ですので、その日数を146,097で割って余りが146,096の場合は400年に一度の2月29日です。146,096未満の場合はそうではないので、今度はそれを100年の日数36,524で割ってみましょう。その余りが100年未満部分の日数なので、それを4年の日数1,461日で割ると、余りが1,460の場合は4年に一度の2月29日です。そうでなければうるう日ではありません。
このように、うるう日になりうる日を最終日に置くことで、剰余算でうるう日かどうかの判定を簡単にできるようになります。最後は365日で割った余りを得ることによって、3月1日起算で1年のうちの何日目かを求めることができます。これならば、あとは力業で何月かを振り分ければ終わりです。
振り分ける際は順に比較しても良いですが、こういうのは二分探索していったほうが早いので、まずは184日未満か(=3~8月か)を判断して、その中でさらに92日未満か(=3~5月か)を判断してという形で絞り込んでいっています。
こうすることで、複雑なうるう年判定をできるだけ構造化して実装することができました。
カレンダー→UNIX時間への変換
続いてカレンダーからUNIX時間への変換を実装していきます。
public static ulong Clock_FromTm(ref tm64 tm)
{
if(tm.tm_year < 1970)
return 0;
uint days = Clock_FromTm_Date(ref tm);
uint time = Clock_FromTm_Time(ref tm);
return (ulong)days * 86400 + time;
}
private static uint Clock_FromTm_Date(ref tm64 tm)
{
ushort year = tm.tm_year;
byte month = tm.tm_mon;
while(month < 1) {
month += 12;
year--;
}
while(month > 12) {
month -= 12;
year++;
}
ushort yearfrom16000301 = (ushort)(year - ((month >= 3) ? 1600 : 1601));
byte year_div400 = (byte)(yearfrom16000301 / 400);
ushort year400y = (ushort)(yearfrom16000301 % 400);
byte year_div100 = (byte)(year400y / 100);
byte year100y = (byte)(year400y % 100);
byte year_div4 = (byte)(year100y / 4);
byte year4y = (byte)(year100y % 4);
uint day = (uint)((uint)year_div400 * 146097 + year_div100 * 36524 + year_div4 * 1461 + year4y * 365); // 1600年3月1日からの日数を計算
if(month <= 6) {
if(month <= 3) {
if(month == 1)
day += (uint)tm.tm_mday + 305;
else if(month == 2)
day += (uint)tm.tm_mday + 336;
else
day += (uint)tm.tm_mday - 1;
} else {
if(month == 4)
day += (uint)tm.tm_mday + 30;
else if(month == 5)
day += (uint)tm.tm_mday + 60;
else
day += (uint)tm.tm_mday + 91;
}
} else {
if(month <= 9) {
if(month == 7)
day += (uint)tm.tm_mday + 121;
else if(month == 8)
day += (uint)tm.tm_mday + 152;
else
day += (uint)tm.tm_mday + 183;
} else {
if(month == 10)
day += (uint)tm.tm_mday + 213;
else if(month == 11)
day += (uint)tm.tm_mday + 244;
else
day += (uint)tm.tm_mday + 274;
}
}
return day - 135080; // 1970年1月1日からの日数になるように差し引く
}
private static uint Clock_FromTm_Time(ref tm64 tm)
{
return ((uint)tm.tm_hour * 60 + tm.tm_min) * 60 + tm.tm_sec;
}
冒頭でwhileループを2つ回しているのは、月が1~12の範囲外だった時に適切に年にオーバーフローできるようにするためです。
以降の考え方は同じで、1600年3月1日からの経過年数をまず求め、うるう年の条件に合わせて振り分けてその年数に対応する日数を計算していきます。あとは、現在が何月何日かという情報から、3月1日から経過している日数を足すことで1600年3月1日からの経過日数を正確に求めていきます。そして最後に135,080日を引くことで1970年1月1日からの日数に換算しています。
まとめ
前述の通り、本当にこれで64bitをフルに使えるようになっているのかと言えば「いいえ」ですが、少なくとも当面は困らない実装ができました。
今回はわかりやすさ(=実装のしやすさ)を優先してシンプルな実装にしましたが、 例えばここ数十年の間では2000年~2099年の日付が流れてくるだろうと想定して、この範囲だったらこの範囲で最も早く計算できるような仕掛けを作るなどしても良かったかもしれません。4の割り算をするだけなら、組み込みみたいなパワーの小さいプロセッサでもシフト演算で簡単に実装できますしね。
0 件のコメント:
コメントを投稿