2014年3月4日火曜日

秒から時分秒への変換 及び年月日時分秒への変換

秒から時分秒や年月日時分秒への変換について
後者はいわゆるUNIX時間を実際の年月日に変換する方法について

時分秒への変換は非常に楽です
void Sec2Time(uint32_t Second, Time_t *time) {
    time->Hour = Second / 3600; Second %= 3600;
    time->Min  = Second / 60;
    time->Sec  = Second % 60;
}
関数名とブロックを除けば3行になります
非常に簡単な処理なので、説明の必要も無いでしょう


次に年月日への変換ですが、これは簡単に実装することができません
数学的にちゃんと考えれば可能かもしれませんが 僕は思いつきませんでした。。(考えるのをやめたとも言う)

なぜ計算で簡単にできないかと言うと、年をまたぐ時間処理はうるう年を考慮に入れる必要があるからです
うるう年ではない場合の1年は31536000秒ですが、うるう年の場合は31622400秒となり、その頻度も「4年に1度」など簡単な処理では対応できないからです

Cでうるう年を判定するためのコードを示します(Wikipediaより)
#define IS_LEAP(year) ((!((year) % 4) && ((year) % 100)) || !((year) % 400))
うるう年の場合は真 うるう年でない場合は偽として展開されます

また
#define TIMEZONE_OFFSET (32400)
という定義も追加しておきます
これはタイムゾーンの時間で、経度が東の場合は正の値 西の場合は負の値になります
今回は日本で使うことを想定し9*60*60の値を設定しました
これを定義ではなくグローバル変数で扱えば動作中に任意のタイムゾーンを設定することもできます
タイムゾーンの設定を0に もしくは処理そのものを削除した場合はUTCとして動作します


関数は
void Sec2DateTime(uint32_t Second, DateTime_t *datetime)
のような感じになっています
第一引数に秒数を与え、第二引数に変換後の値を入れるための構造体のポインタを設定します
この構造体はYear,Mon,Day,Hour,Min,Secの6個のメンバがあります
すべての型は符号なし8bitです
ただし関数の引数を32bitではなく、64bit等で実装する場合はYearを16bitや32bitとして実装する必要があります


さて、実際のコードになるわけですが、まずはローカル変数を1つ準備しておきます
uint8_t IsDec = 0;
これはうるう年の処理に使用する変数です 1bitで十分ですが、最小単位が8bitなのでこの型を使用しました

最初の処理として
    datetime->Year = 0;
    Second += TIMEZONE_OFFSET;
を実行します
これは年の初期化と、タイムゾーンの設定です
年以外の月日時分秒はその都度値を代入しているため、初期化する必要はありません
またタイムゾーンの処理はこの箇所でのみ行っています

次に年の計算ですが
これは上記の通り面倒なのでwhileのループで行っています
    while (1) {
        if (!IS_LEAP(datetime->Year + 1970)) {
            if (Second < 31536000) { break; }
            Second -= 31536000;
        } else {
            if (Second < 31622400) { break; }
            Second -= 31622400;
        }  
        datetime->Year++;
    }
まずうるう年か否かを判定し、その結果にそって秒を判定します
その年1年分にみたない場合はbreakで処理を抜け、十分に時間があるなら1年分の時間を引いてから年をインクリメントします
年は代入ではなくインクリメントで行うため、最初に初期化する必要があるわけです

そして
    uint16_t Days = Second / 86400;
    Second %= 86400;
のコードで秒を日に変換します
秒は1日にみたない時間にします

その後で
    if (Days > (31 + 28) && IS_LEAP(datetime->Year + 1970)) { Days--; IsDec = 1; }
の処理を通します
これによりうるう年でかつ2月29日以降の場合は1日減算されます

次に月の変換です
         if (Days >= 334) { datetime->Mon = 12; Days -= 334; }
    else if (Days >= 304) { datetime->Mon = 11; Days -= 304; }
    else if (Days >= 273) { datetime->Mon = 10; Days -= 273; }
    else if (Days >= 243) { datetime->Mon = 9;  Days -= 243; }
    else if (Days >= 212) { datetime->Mon = 8;  Days -= 212; }
    else if (Days >= 181) { datetime->Mon = 7;  Days -= 181; }
    else if (Days >= 151) { datetime->Mon = 6;  Days -= 151; }
    else if (Days >= 120) { datetime->Mon = 5;  Days -= 120; }
    else if (Days >=  90) { datetime->Mon = 4;  Days -=  90; }
    else if (Days >=  59) { datetime->Mon = 3;  Days -=  59; }
    else if (Days >=  31) { datetime->Mon = 2;  Days -=  31; } 
    else                  { datetime->Mon = 1;               }
月はこのようにif分岐で行います
1月1日から11月30日までの日数以上の時間があるなら12月として
1月1日から10月31日までの日数以上の時間があるなら11月として のように判定します
先ほどうるう年の場合は1日減算したのはこの判定をするためです
そして日は適切に減算しておきます

その後
    datetime->Day  = Days + IsDec + 1;
で日を代入します
うるう年の場合はIsDecが1 それ以外は0なので うるう年でかつ2月29日以降の場合は1が加算されます
また日は0からではなく1からなので、常に1を加算します

残りは最初の関数と同じことをするだけなので簡単です
    datetime->Hour = Second / 3600; Second %= 3600;
    datetime->Min  = Second / 60;
    datetime->Sec  = Second % 60;

これですべての計算は終わりです
なお実際に使用する場合には年に1970を加算して表示してやります
構造体のYearを16bitにする場合はYearの初期化を0ではなく1970で行い、IS_LEAPで1970を加算せずに行うのもいいでしょう

他にも、このコードはまだ最適化の余地が残っているので、そのあたりをいじってみるのもいいと思います

ついでに書くと、この関数は入力が符号なしということからも分かる通り、1970年1月1日より前の時間については計算していません


なお、このコードでいくつかの時間を変換し、正常に値が出力されることを確認していますが、確実に動作するとは限らないことをご了承ください


最後に、細切れではない関数を書いておきます
void Sec2DateTime(uint32_t Second, DateTime_t *datetime) {
    uint8_t IsDec = 0;

    datetime->Year = 0;
    Second += TIMEZONE_OFFSET;

    while (1) {
        if (!IS_LEAP(datetime->Year + 1970)) {
            if (Second < 31536000) { break; }
            Second -= 31536000;
        } else {
            if (Second < 31622400) { break; }
            Second -= 31622400;
        }   
        datetime->Year++;
    }   

    uint16_t Days = Second / 86400;
    Second %= 86400;

    if (Days > (31 + 28) && IS_LEAP(datetime->Year + 1970)) { Days--; IsDec = 1; }

         if (Days >= 334) { datetime->Mon = 12; Days -= 334; }
    else if (Days >= 304) { datetime->Mon = 11; Days -= 304; }
    else if (Days >= 273) { datetime->Mon = 10; Days -= 273; }
    else if (Days >= 243) { datetime->Mon = 9;  Days -= 243; }
    else if (Days >= 212) { datetime->Mon = 8;  Days -= 212; }
    else if (Days >= 181) { datetime->Mon = 7;  Days -= 181; }
    else if (Days >= 151) { datetime->Mon = 6;  Days -= 151; }
    else if (Days >= 120) { datetime->Mon = 5;  Days -= 120; }
    else if (Days >=  90) { datetime->Mon = 4;  Days -=  90; }
    else if (Days >=  59) { datetime->Mon = 3;  Days -=  59; }
    else if (Days >=  31) { datetime->Mon = 2;  Days -=  31; } 
    else                  { datetime->Mon = 1;               }   

    datetime->Day  = Days + IsDec + 1;
    datetime->Hour = Second / 3600; Second %= 3600;
    datetime->Min  = Second / 60;
    datetime->Sec  = Second % 60;
}



0 件のコメント:

コメントを投稿