C で時間データを扱うには、以下の 2 つが標準的。
- tm 時間構造体
- 年月日時分秒などで表現する
- time_t 型
- UNIX 時間を表す
tm 時間構造体はタイムゾーンと共に時間を扱う。
time_t 型は Thu Jan 1 00:00:00 1970 GMT からの通し秒で時間を扱い、タイムゾーンの概念は存在しない。
プログラム内部で時間を扱うには time_t 型を使うのが便利だが、カレンダー表記に対応させるために、tm 時間構造体で入出力する必要がある。
それぞれ変換は以下の通り。
time_t 型→時間構造体
struct tm *newtime_struct;
newtime_struct = localtime(&new_time);
newtime_struct = gmtime(&new_time);
localtime でタイムゾーンに沿ったローカルタイムの入った時間構造体を得る。
gmtime でGMTの入った時間構造体を得る。
時間構造体→time_t 型
struct tm time_struct={}; // 明示的に初期化しないと異常動作の原因となる
time_t new_time;
// 2020/11/02 18:01:02 GMT データを構造体として作る
time_struct.tm_year = 120; // year
time_struct.tm_mon = 11-1; // month
time_struct.tm_mday = 2; //day
time_struct.tm_hour = 18; //hour
time_struct.tm_min = 1; //minute
time_struct.tm_sec = 2; //second
new_time = mktime(&time_struct);
mktime で UNIX 時間が得られるが、時間構造体に入ったデータはローカルタイムとしてみなされる。
IoTの憂鬱
タイムゾーンが正しく得られれば、上記のような処理を恙無く行えば問題ない。しかしながら IoT では以下のような場合を考慮しないといけない。
- タイムゾーンやカレンダーのリソースのない小フットプリントのシステム
- 時間情報を外部から得られない
- タイムゾーン情報を外部から得られない
- 通信の無い状態でタイムゾーンをまたがって移動する
何らかの方法で RTC または通算起動時間を得られる場合は、外部の時間から切り離して内部だけの時間で動作を管理することができる。
タイムゾーンの管理については、外部から情報を得られなくても、各国の電波法に基づいてIoT製品を出荷する際に国に紐付いたタイムゾーンをある程度推測してプリセットするという方法がある。
しかしながらその方法も常に有効ではないので、タイムゾーンは設定操作などで設定するなどの考慮が必要になる。
ある程度限定された利用シーンでは、IoT機器の内部での時間データを全てUTCとして考えることができ、その場合扱いが簡単になる。
しかしながら上記の例では 時間構造体→time_t 型 の変換で使用している mktime はローカルタイムとして変換処理されるため、UTCとして変換するには工夫が必要になる。
方法
time_t 型は unix 時間として、もともと UTC ベースの情報である。これのみを扱う場合は、特にタイムゾーンを意識する必要はない。
unix 時間を tm 時間構造体に相互変換する際、UTC として扱うように工夫する。
time_t 型→時間構造体
変換に localtime() ではなく gmtime() を使用する。
時間構造体→time_t 型
mktime() は引数の時間構造体をローカル時間として処理するので、timezone 分の時間を加減して time_t を得る。
origin_time = mktime(newtime_struct); // これだとローカルタイムになる
origin_time = mktime(newtime_struct)-timezone; // これだと UTC が得られる
なお、暦の切り替えなどをまたぐ場合は誤差が出る場合があることに注意。サマータイムなど。
実例
- TEST1 unix時間←0 で 1970/01/01 00:00:00 GMT データを作る
- TEST2 時間構造体に変換
- TEST3 時間構造体から年月日時分秒タイムゾーンメンバの取り出し
- TEST4 システムに設定されたタイムゾーン値の確認
- TEST5 時間構造体をunix時間に再変換
- TEST6 変換したunix時間をUTCとして表示
- TEST7 任意の年月日時分秒を指定して時間構造体を作成
- TEST8 unix時間に変換してUTCとして表示
asctime
マジメに時間を表示するには、TEST3 のように時間構造体からメンバを取り出して表示する必要があります。
// 構造体から値を取り出す
year = newtime_struct->tm_year;
month = newtime_struct->tm_mon + 1;
day = newtime_struct->tm_mday;
hour = newtime_struct->tm_hour;
minute = newtime_struct->tm_min;
second = newtime_struct->tm_sec;
printf("test3 %d/%d/%d %d:%d:%d %s\n",
year,
month,
day,
hour,
minute,
second,
newtime_struct->tm_zone);
めんどくさいですね! そこで asctime を使うと安直に時間構造体を "Mon Nov 2 18:01:02 2020" 形式で表示することができます。
time_t 形式を表示するには、以下のようにすればOkです。
sctime(gmtime(&new_time)));
TEST1,TEST2,TEST6,TEST7,TEST8 では asctime()を使用しています。
しかしながら 引数の構造体に問題があると表示がおかしくなります。TEST7では曜日の表示がおかしくなっています。TEST8はTEST7をサニタイズして asctime で表示しても問題ないようにしています。
サンプルコード
#include <stdio.h>
#include <time.h>
void main() {
struct tm *time_struct_now;
struct tm time_struct={}; // 明示的に初期化しないと異常動作の原因となる
struct tm *newtime_struct;
static time_t origin_time, uncalib_time, new_time,work_time;
time_t now;
char tzstr[30]="GMT";
// char tzstr[30]="JST";
int year,month,day,hour,minute,second;
tzset();
// 1970/01/01 00:00:00 GMT データを作る
new_time = 0;
printf("test1 %s",asctime(gmtime(&new_time)));
// 出力:"test1 Thu Jan 1 00:00:00 1970"
// 構造体に変換する
newtime_struct = gmtime(&new_time);
printf("test2 %s",asctime(newtime_struct));
// 出力: "test2 Thu Jan 1 00:00:00 1970"
// 構造体から値を取り出す
year = newtime_struct->tm_year;
month = newtime_struct->tm_mon + 1;
day = newtime_struct->tm_mday;
hour = newtime_struct->tm_hour;
minute = newtime_struct->tm_min;
second = newtime_struct->tm_sec;
printf("test3 %d/%d/%d %d:%d:%d %s\n",
year,
month,
day,
hour,
minute,
second,
newtime_struct->tm_zone);
// 出力: "test3 70/1/1 0:0:0 GMT"
// timezone値を確認する
printf("test4 timezone=%d\n",(int)timezone);
// 出力: "test4 timezone=-32400"
// 構造体から UNIX 時間を求める
// mktime の引数は tm_zone を無視してローカル時間とみなされるので
// 出力から timezone 分を引いて UTC にしたうえで unix 時間を得る
origin_time = mktime(newtime_struct)-timezone;
// 0 になるはず
printf("test5 %d \n",(int)origin_time);
// 出力: "test5 0 "
// UTC表示する
printf("test6 %s",asctime(gmtime(&origin_time)));
// 出力: "test6 Thu Jan 1 00:00:00 1970"
// 2020/11/02 18:01:02 GMT データを構造体として作る
time_struct.tm_year = 120; // year
time_struct.tm_mon = 11-1; // month
time_struct.tm_mday = 2; //day
time_struct.tm_hour = 18; //hour
time_struct.tm_min = 1; //minute
time_struct.tm_sec = 2; //second
// time_struct.tm_wday= 1; // 曜日を入れないとtest6が異常になる
time_struct.tm_zone = "GMT";
printf("test7 %s",asctime(&time_struct)); // 曜日表示が異常
// 出力: "test7 Fri Nov 2 18:01:02 2020"
// 一度 UNIX 時間に変換する
// ここでも上記と同じように timezone 分を引いて UTC として扱う
new_time = mktime(&time_struct)-timezone;
printf("test8 %d \n",(int)new_time);
// 出力: "test8 Mon Nov 2 18:01:02 2020"
// UNIX 時間から再度構造体に戻すと正常になる
printf("test8 %s",asctime(gmtime(&new_time))); // 正常
// 出力: "test9 1604340062 "
}