unixtimeについて

コンピュータでは日時、時刻をグリニッジ標準時で、1970年1月1日午前0時0分0秒からの 秒数で表現している。 コンピュータに限らず、各種デバイスでも多用されている。

これを一般にUNIXTIME と言うのだが、これを通常の年月日時分秒に変換したり、逆に年月日時分秒をUNIXTIMEに変換したり・・・は良くやる処理であり、 ちょっと検索すればサンプルプログラムには突き当たる。

でもこれ理解しようとすると案外すぐには理解できない代物(整数ってむずかしい)。

説明を試みてみようと思います。

といいながら先にサンプルを出します。説明しやすい順序とプログラムの順序が合わないからです。

【(1)UNIXTIME->標準時(東京)】

#define ARRAYSIZE(_arr) (sizeof(_arr) / sizeof(_arr[0]))
#define GMT_TOKYO       (9*60*60)
#define SECONDS_IN_A_DAY    (24*60*60)
#define EPOCH_DAY       (1969*365L + 1969/4 - 1969/100 + 1969/400 + 306)    //  days from 0000/03/01 to 1970/01/01
#define UNIX_EPOCH_DAY  EPOCH_DAY
#define YEAR_ONE        365
#define YEAR_FOUR       (YEAR_ONE * 4 + 1)  //  it is YEAR_ONE*4+1 so the maximum reminder of day / YEAR_FOUR is YEAR_ONE * 4 and it occurs only on 2/29
#define YEAR_100        (YEAR_FOUR * 25 - 1)
#define YEAR_400        (YEAR_100*4 + 1)    //  it is YEAR_100*4+1 so the maximum reminder of day / YEAR_400 is YEAR_100 * 4 and it occurs only on 2/29
void ConvertUnixTimeToTokyoTime(uint64_t unixtime, uint32_t *pyear, uint8_t *pmonth, uint8_t *pday, uint8_t *phour, uint8_t *pminute, uint8_t *psecond) {
    uint32_t unixday;
    uint16_t year = 0;
    uint8_t  leap = 0;
    uint32_t n;
    uint8_t month, day, weekday;
    uint8_t hour, minute, second;
    static const uint16_t monthday[] = { 0,31,61,92,122,153,184,214,245,275,306,337 };

    unixtime += GMT_TOKYO;
    second = unixtime % 60;
    minute = (unixtime / 60) % 60;
    hour = (unixtime / 3600) % 24;

    unixday = (uint32_t)(unixtime / SECONDS_IN_A_DAY);
    weekday = (uint8_t)((unixday + 3) % 7); //  because the unix epoch day is thursday
    unixday += UNIX_EPOCH_DAY;  //  days from 0000/03/01 to 1970/01/01

    year += 400 * (unixday / YEAR_400);
    unixday %= YEAR_400;

    n = unixday / YEAR_100;
    year += n * 100;
    unixday %= YEAR_100;

    if (n == 4){
        leap = 1;
    } else {
        year += 4 * (unixday / YEAR_FOUR);
        unixday %= YEAR_FOUR;
        
        n = unixday / YEAR_ONE;
        year += n;
        unixday %= YEAR_ONE;
        if (n == 4) {
            leap = 1;
        }
    }
    if (leap != 0) {
        month = 2;
        day = 29;
    }
    else {
        month = (unixday * 5 + 2) / 153;
        day = unixday - monthday[month] + 1;    //  
        month += 3;
        if (month > 12) {
            ++year;
            month -= 12;
        }
    }
    *psecond = second;
    *pminute = minute;
    *phour = hour;
    *pyear = year;
    *pmonth = month;
    *pday = day;
}

 

 

理解せぬコピペはバグが出るとお手上げです。以下解説を試みます。

【時差の吸収】

    unixtime += GMT_TOKYO;
グリニッジ標準時と日本標準時(東京)・・・明石じゃないのってのは置いておいて・・・は9時間なので、 9×60×60秒を足します。

 

 

【時分秒の算出】

    second = unixtime % 60;
    minute = (unixtime/60) % 60;
    hour = (unixtime/3600) % 24;

秒は59秒までなので、まず60で割ったあまりを出します。
1分は60秒なので、60で割ったものを使いますが、 時刻の分は59分までなので、分を60で割ったあまりを使用します。
1時間は、60×60秒なので、秒を3600で割ったものを時とします。
しかし時刻の時は23時までなので(23:59の一分後は0:00)、24で割ったあまり使用します。

 

 

【1970年1月1日からの経過日数】

    unixday = (uint32_t)(unixtime / SECONDS_IN_A_DAY);

UNIXTIME を一日の秒数で割ることで1970年1月1日からの経過日数に変換します。
UNIXTIME から作った日数をここではUNIXDAYと呼ぶことにします。

【曜日の算出】

    weekday = (uint8_t)((unixday + 3) % 7);

曜日を算出します。0:月曜 6:日曜です。1970年1月1日は木曜日です。
日数を7で割ったあまりを出すと

0:木曜、1:金曜、2:土曜、3:日曜、4:月曜、5:火曜、6:水曜

となります。+3で3日ずらすことで、0:月曜~6:日曜の形に変換します。

ここまでは比較的簡単です。ポイントはここからです。


【日付の変換(西暦0年3月1日からの日数に変換)】

    unixday += UNIX_EPOCH_DAY;

UNIXDAYを、0年3月1日からの日数に変換します。3月1日という日付がポイントです。
なぜかというと、3月1日から一年を開始するとその年の最終日は2月28日になります。 うるう年は2月29日が最終です。うるう年とそうでない年との違いは、
最後の一日のある/ないの違いになります。これはコンピュータにとって都合がいい。

 

 

【うるう年の考え方】

4年という単位で日数を考える。すると 365×4+1日、具体的には、1461日となる
365日×4+1
365日365日365日365日

経過日数を4年(1461日)で割ると、余りの最大値は、1460である。
それを1年(365日)で割ると、1460÷365=4 なので、商が4。

で割ったあまりを1で割ると商が4!

これは、うるう年の2月29日だけで発生する状況です。
※この割り算、2月29日ではかならず割り切れます(その一日しかないからです)。

なお、前日の1459日を365で割ると商は3。




西暦(グレゴリオ暦)にはまだ以下のルールがあります。

これも先ほどのルールと同様で解決できます。

【400年に一度のうるう年】

100年という単位で日数を考える。すると先ほどの4年×25-1日となる。100年に一回うるう年なしなので、この計算となる。具体的には、36,524日
次に400年。これは 100年×4+1日、具体的には、146,097日となる

100年×4+1
100年100年100年100年

経過日数を400年(146,097日)で割ると、余りの最大値は、146,096である。
それを100年(36,524日)で割ると、146,096÷36,524=4 なので、商が4。

( {(100年×4+1)-1} ÷ 100年 なのであたりまえ・・・)

400で割ったあまりを100で割ると商が4!

これも、うるう年の2月29日だけで発生する計算です。

※この割り算、2月29日ではかならず割り切れます(その一日しかないからです)。 なお、前日の146,095日を36,524で割ると商は3。



以上をまとめると、年月日の年を算出するプログラムが完成します。

400年がいくつある、100年がいくつある、4年がいくつある、1年がいくつある。
これを合算すればよいのです。
そして、100年と1年の計算で4が出てきたら割り切れるので、そこで計算を打ち切って大丈夫です。



【西暦年の算出】

    year += 400 * (unixday / YEAR_400);
    unixday %= YEAR_400;

    n = unixday / YEAR_100;
    year += n * 100;
    unixday %= YEAR_100;

    if (n == 4){
        leap = 1;
    } else {
        year += 4 * (unixday / YEAR_FOUR);
        unixday %= YEAR_FOUR;
        
        n = unixday / YEAR_ONE;
        year += n;
        unixday %= YEAR_ONE;
        if (n == 4) {
            leap = 1;
        }
    }
プログラムに書くとこうなります。

後は月日


まずうるう年の2月29日はこれまでの計算で出ています。
このときの日付は、2月29日で確定です。

そうでないときは算出が必要です。
年の算出が終わったあと、unixdate には、その年の何日目かという情報が入っています。

テーブル探索で力業という手もあります。 (割り算が苦手なCPU たとえば、Z-80 とかだとその方が早い、標準関数なんか使えない・・・というのは、むしろそういう環境ではないかと思います。)

【月の算出】

整数的に正く算出するなら以下のとおりとなります。
    month = (unixday / 153) * 5;
    unixday %= 153;
    month += (unixday / 61) * 2;
    month += ((unixday % 61) / 31);

割り算(剰余算含む)が5回・・・・・
(商と余が同時に出力される整数除算命令があること、最適化されることを前提としても割り算3回です)
なんとかならないのと考えてみます。

除算の小数点以下は切り捨てられるので、何が入ろうが気にしなくていいってところがポイントとなります

    month = (unixday * 5 + 2) / 153;

誰が考えたか、この通りで月が出ます・・・・では解説になりませんので、説明を試みます。

日数累積
33131
43061
53192
630122
731153
831184
930214
1031245
1130275
1231306
131337
228365

左の表を見てください。ポイントは最初の五カ月と次の五カ月の月の日数が 同じパターンで並んでいることです。

最後の2カ月もはじめの月はパターンに乗っています。これはいけそうです。

そのため日数を5カ月の日数153日で割ります。
これを5倍して、5カ月単位で考えるのが、一つ目の式の最初の行となります。
ただ、そのままだと8月で初めて月が一つ上がってしまいます。5カ月なので 割る前に5を掛けておきます(整数除算なので割ってからかけてもダメ)。 これだとそれっぽい値が出てきます。
小数点以下を考えると、これだと一カ月が30.6日

実際の月の境目と合いません。

以下実際に試算してみます。

月初(月初日*5)/153月末日(月末日*5)/153
300.0300.980392157
4311.013071895601.960784314
5611.993464052912.973856209
6923.0065359481213.954248366
71223.9869281051524.967320261


小数点以下を、月初四捨五入、月末切り捨てで行けちゃいそうですが、結局 二つ式が必要になるので、あまり軽減になりません。

足し算でなんとかなれば良い感じです・・・。

こんな感じで
    month = ((unixday + ?) * 5) / 153;

?はなんだろうと考えます。

とりあえず月末はすべて切り捨てで正しくでています。

問題は月初です。全て若干足りない・・・という感じです。
日数の式の分母に足しておかしくならない数値を考えます。まず、正しい末尾がおかしくならない 限界を探ります。
月末日30.6日の月末マージン
33030.60.6
46061.21.2
59191.80.8
6121122.41.4
7152153.01.0

0.6未満なら足して大丈夫(結果が変わらない)です。

次に月初を考えます。

月初日30.6日の月初不足日数
3000.0
43130.60
56161.20.2
69291.80
7122122.40.4

最大の不足分は0.4、そして月末がおかしくならないマージンが0.6これなら大丈夫。



とりあえず以下の式

    month = ((unixday + 0.4) * 5) / 153;

しかし、これだと整数で演算できないので0.4 に5をかけてカッコから出します。

    month = ((unixday * 5) + 2) / 153;

これで元の式になりました。

実際に計算すると

月初(月初日*5+2)/153月末日(月末日*5+2)/153
300300
4311601
5612912
69231213
712241524
815351835
918462136
1021472447
1124582748
1227593059
13061033610
23371135511


いけました。これで良しです。

次に日数を調整します。理屈はありません。テーブルを使います。

【日数の計算】

日を計算します。 既に月は判っていますので後はテーブルを使った引き算です。
その月までの日数を引いて、その月の何日目かを出します。

まず、3月1日からその月の月初までの日数を配列でデータ化しておきます。

    static const uint16_t monthday[] = { 0,31,61,92,122,153,184,214,245,275,306,337 };


上のような感じです。

そして月は出ているので、その月までの日数をunixday から引くとその月の何日かが出てきます。

また、日は、0日からでなく1日からなので、1を足して調整してます。

    if (leap != 0) {
        month = 2;
        day = 29;
    }
    else {
        month = (unixday * 5 + 2) / 153;
        day = unixday - monthday[month] + 1;    //  日は0日からじゃないので+1
        month += 3;
        if (month > 12) {
            ++year;
            month -= 12;
        }
    }


最初の月は0月じゃなくて3月なので、3を足します。
すると、13月14月が計算上出てきますので、それは最後に、翌年の1・2月に変換しています。
これで計算は完了しました。

次のページ