時間情報の取得方法と扱い方

作成:

ここではC言語での時間情報の取得方法について説明していく、 時間情報には様々な種類があり、また環境により利用できる関数も変わってくる。 C言語の環境であればどこでも利用できる標準関数から始まり、POSIX環境、Windows環境で利用できるAPIも紹介する。

C言語標準

まずはC言語の標準関数で、C言語が使える環境であればどこでも使用できる方法を紹介する。

time()

時刻情報を取得するtime()関数。 プロトタイプ宣言は以下のようになっている。

#include <time.h>
time_t time(time_t *t);

値の取得方法としては、以下のように引数に格納先へのポインタを渡す方法。

time_t t;
time(&t);

または、以下の用に戻り値を利用する方法。

time_t t = time(NULL);

引数がNULLでない場合は、その値に代入した後、同じ値が戻される。 どちらを使っても違いはない。 通常は戻り値を利用するほうが使いやすいだろう。

以下のようなコードを実行したとすると

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv) {
  time_t t = time(NULL);
  printf("%ld\n", t);
  printf("%zd\n", sizeof(time_t));
  return 0;
}

以下の様な結果が得られる。

1446551245
8

time()の戻り値は、1970年1月1日0時0分0秒(UTC)からの経過秒数である。 この値は、「UNIX時間」とか、「エポック秒」、「POSIX時間」などとよばれており、 ほとんどのコンピュータシステムがこの値で時間を管理している。

次に、 time_tsizeof の値を表示しているが、 見ての通り、本校執筆時点では、多くのシステムで 8Byte つまり 64bit 符号付き整数で扱われている。 しかし、 64bit 化が行われるようになったのもここ数年の話なので、 まだ 32bit になっているシステムもあるかもしれない。

時刻を扱う上で、プログラマーであれば必ず知っておくべき事柄なので、2038年問題について書いておく。 64bit システムが出てくるまでは、多くのシステムで time_t は符号付き 32bit 整数だった。 符号付き 32bit 整数の最大の値は 2,147,483,647 であり、これをUNIX時間として解釈すると2038年1月19日3時14分7秒となる。 この時刻を超えると、符号付き 32bit 整数で扱っているシステムが正常に動作しなくなる。これが 2038年問題 である。

この制限は何も最終的な出力としてだけでなく、計算の途中にも発生する。 期日の中間地点を計算する場合、安直には二つの時間を足して2で割る計算をするだろう。 その場合、計算の中間地点で約2倍の値を保持する必要があるが、 この処理を行っていたATMが 2004年1月11日 に誤動作したという事件も発生している。

time_t型が 64bit になったように、 既存のライブラリ等を素直に利用していれば、問題が起こらないようになってきてはいる。 しかし、time_tではなく 32bit 変数に代入するなどしている箇所が 一箇所でもあると将来問題が発生するので注意しよう。

時刻を文字列表現に変換

さて、時刻情報の取り方はこれだけなのだが、 何かの経過時間を取るということであれば、はじめと終わりでこの時間を取得し、差を取れば良い。 ただし、時刻として扱う場合において、日付や時間表示したりするには、 やや面倒な手続きが必要であるため、幾つかの関数が用意されている。

例えば以下の関数を使うと、現在時刻を表現する文字列に変換してくれる。

#include <time.h>
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);

ctimeは内部メモリへのポインタを返すためスレッドセーフではない、 ctime_rはPOSIXで追加されている(つまりC言語標準ではない) スレッドセーフ版で出力バッファを引数で指定する。

以下のように使用する。

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv) {
  time_t t = time(NULL);
  printf("%s", ctime(&t));
  return 0;
}

実行すると、

Tue Nov  3 20:47:25 2015

と言った出力が得られる。記述量が小さく簡単に使えるが、 柔軟性がなく、日本時間ではあるが日本ではあまり馴染みのない出力になる。 また、POSIX的には非推奨となっており、後に紹介するstrftimeの使用が推奨されている。

年月日時分秒、各パラメータへの変換

以下の関数を使うと、日付時刻の各パラメータに分離された構造体に変換できる。 gmtime()は GMT、localtime()は地域情報に基づき時差を含めた時刻に変換する。

#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);

struct tm構造体は以下の様な構造となっている。

struct tm {
    int tm_sec;        /* 秒 (0-60) */
    int tm_min;        /* 分 (0-59) */
    int tm_hour;       /* 時間 (0-23) */
    int tm_mday;       /* 月内の日付 (1-31) */
    int tm_mon;        /* 月 (0-11) */
    int tm_year;       /* 年 - 1900 */
    int tm_wday;       /* 曜日 (0-6, 日曜 = 0) */
    int tm_yday;       /* 年内通算日 (0-365, 1 月 1 日 = 0) */
    int tm_isdst;      /* 夏時間 */
};

これを使用し、文字列に変換するとすれば以下のようになるだろう。

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv) {
  struct tm tm;
  char *dayofweek[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
  time_t t = time(NULL);
  localtime_r(&t, &tm);
  printf("%04d/%02d/%02d %s %02d:%02d:%02d\n",
         tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
         dayofweek[tm.tm_wday], tm.tm_hour, tm.tm_min, tm.tm_sec);
  return 0;
}

年が西暦から1900を引いた値なので、1900を足す。 また、月は配列の添字に使えるように0~11で表現されているため、 数字として出力する場合は1を足す必要がある。

出力は以下のようになる。

2015/11/03 Tue 20:47:25

ただ、単純に文字列に変換したいだけの場合は、上記の記述方法は多少冗長で面倒である。 そこで、時刻情報を文字列に変換する関数が用意されている。

フォーマットを指定しての変換

#include <stdio.h>
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

printfに似たフォーマットで、どのような形式の文字列にするかを指定すれば、 引数のstruct tmを元に文字列を作成することができる。 フォーマットの詳細についてはMANなどを参照。 使用例は以下のようになる。

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv) {
  char date[64];
  time_t t = time(NULL);
  strftime(date, sizeof(date), "%Y/%m/%d %a %H:%M:%S", localtime(&t));
  printf("%s\n", date);
  return 0;
}

出力は以下のようになり、先ほどのprintfを使用したものと同じ出力を、より簡単に記述することができる。

2015/11/03 Tue 20:47:25

このstrftimeのフォーマットについては、この関数についてだけでなく、 日付表記方法を表現するための手段の一つとして使用されることもある。

自前での計算

これまで、用意された関数を利用してUNIX時間から日時情報への変換を紹介した。 しかし、日付についてはうるう年の計算など多少面倒な部分があるが、 時間情報については単純な計算で変換できる。

例えば、日本の時差は UTC+9 であるので、以下の計算で時、分、秒を求めることができる。

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv) {
  char date[64];
  time_t t = time(NULL);
  strftime(date, sizeof(date), "%Y/%m/%d %a %H:%M:%S", localtime(&t));
  printf("%s\n", date);
  printf("%02ld時\n", (t / 3600 + 9) % 24);
  printf("%02ld分\n", t / 60 % 60);
  printf("%02ld秒\n", t % 60);
  return 0;
}

実行結果は以下のようになる。

2015/11/03 Tue 20:47:25
20時
47分
25秒

通常は時刻の値を変換するというのはそれほど高頻度に実行される計算ではないので、 分かりやすさや確実性から変換関数を利用するのがよいだろう。 しかし、特定の単位の値だけを求めればよく、かつ、非常に多数の計算が必要というシーンでは、 より単純で小さなコストの計算で求められることを覚えておくと良いだろう。

最後に、注意点として時刻情報ということはシステムの時計に依存する。 この関数を使って経過時間を計測する場合、 計測している間に NTP や管理者によってシステムの時計が変更された場合、 意図しない値が取れてしまう場合があるので気をつけよう。