C++で時刻(ミリ秒単位)を文字列に変換する

Linuxと書いていますが、POSIX準拠であれば大体同じはずです。
サンプルコードでは返り値チェック等はあえて記載していません。

時刻を扱う4つの構造体

Linux上で時刻を扱う際に最初に混乱するのは、時刻を扱う構造体が主に4種類存在することです。
これは歴史的な経緯もあり、そういうもんだと思っておきます。

time_t

3つの内で最も古い。The Epoch(1970年1月1日0時0分0秒UTC)からの経過秒数を表す。
したがってミリ秒以下の情報は持たない。

具体的な定義は実装依存。Ubuntu16(64bit)ではtime_t = long intです。

1
2
3
4
5
6
7
8
9
10
11
12
/* types.h */
#define __SQUAD_TYPE long int

/* typesizes.h */
#define __SYSCALL_SLONG_TYPE __SQUAD_TYPE
#define __TIME_T_TYPE __SYSCALL_SLONG_TYPE

/* types.h (again) */
typedef __TIME_T_TYPE __time_t

/* time.h */
typedef __time_t time_t

timeval

マイクロ秒まで保有することを想定した構造体。
以下のlongの部分は、実際の環境では別名でtypedefされていますが、実質longです。

1
2
3
4
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};

timespec

ナノ秒まで保有することを想定。一番新しい。

1
2
3
4
struct timespec {
time_t tv_sec; /* Seconds. */
long tv_nsec; /* Nanoseconds. */
};

tm

これは上記3つとは少し性質が異なり、エポック秒を年/月/日/…へ分解した情報を保有する。
精度的には秒までのため、time_tと組み合わせて使うことが多いです。

1
2
3
4
5
6
7
8
9
10
11
struct tm {
int tm_sec; /* Seconds. [0-60] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year - 1900. */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /* DST. [-1/0/1]*/
};

時刻を取得する関数

上記のtime_t,timeval,timespecそれぞれに対して、時刻を取得する関数が異なります。
順番に見ていきます。

time関数

返り値としてtime_tを返します。

man

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <ctime>

int main(void) {
time_t t;

// Sucess : Returns epoch time
// Faliure: Returns -1
t = time(NULL);

std::cout << "Return: " << t << std::endl;
// Return: 1559983942

return 0;
}

gettimeofday関数

timeval構造体のポインタを第1引数に与えます。
第2引数は本来はタイムゾーンの情報ですが、非推奨となっておりNULLを与えます。

man

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <sys/time.h>

int main(void) {

struct timeval tv;

// Success: Returns 0
// Failure: Returns -1
// Second arg should be always NULL (see man page)
int ret = gettimeofday(&tv, NULL);

std::cout << "Seconds : " << tv.tv_sec << std::endl
<< "Micro seconds : " << tv.tv_usec << std::endl;
// Seconds : 1559984525
// Micro seconds : 593563

return 0;
}

clock_gettime関数

使い方はgettimeofday関数と大差ありません。
第1引数の種類で時計の性質を選択できます。詳細はmanを見てください。

man

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <ctime>

int main(void) {

struct timespec ts;

// Success: Returns 0
// Failure: Returns -1
// First arg: CLOCK_REALTIME, CLOCL_MONOTONIC, etc.
int ret = clock_gettime(CLOCK_REALTIME, &ts);

std::cout << "Seconds : " << ts.tv_sec << std::endl
<< "Nano seconds : " << ts.tv_nsec << std::endl;
// Seconds : 1559985934
// Nano seconds : 113947957

return 0;
}

エポック時刻を現地時刻の文字列に変換する

前置きが長くなりましたが、エポック時刻を現地時刻の文字列(2019/6/8 13:33:25.345みたいな感じ)に変換する処理を考えます。

基本的な考え方は以下です。

  • 年月日時分秒までは、time_t型をstrftime関数で文字列にする変換する処理が使える
  • (ナノ秒まで保持するtimespec構造体の1つめの変数tv_secは、単なるtime_t型です)
  • ミリ秒以下はタイムゾーン等に一切影響されない要素なので、上記で求めた結果に追加表示するだけ

以下、時刻取得はtimespec型で取得した前提で記載します(あえて古いものを使う理由は特にないので)。

現地時刻の年月日時分秒を作る

localtime_r関数でエポック秒を、冒頭で紹介したtm構造体に変換します。
似た名前のlocaltime関数はスレッドセーフではないため、localtime_rを使います。

man

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <ctime>

int main(void) {

struct timespec ts;
struct tm t;

int ret = clock_gettime(CLOCK_REALTIME, &ts);

// No error return
localtime_r(&ts.tv_sec, &t);

std::cout << "Year : " << t.tm_year + 1900 << std::endl // See definition of tm
<< "Month : " << t.tm_mon + 1 << std::endl // See definition of tm
<< "Day : " << t.tm_mday << std::endl
<< "Hour : " << t.tm_hour << std::endl
<< "Minute : " << t.tm_min << std::endl
<< "Second : " << t.tm_sec << std::endl;

return 0;
}

年と月の値に注意します。基本的にはこれで現地時刻へ変換は達成できていますが、文字列への変換をより手軽にやってくれるstrftime関数も使ってみます。

フォーマット指定詳細はmanを見てください。

1
2
3
4
5
6
7
8
char buf[32];

// Success: Returns written length
// Failure: Returns 0
int r = strftime(buf, 32, "%Y/%m/%d %H:%M:%S", &t);

std::cout << std::string(buf) << std::endl;
// 2019/06/08 19:22:06

あとはミリ秒を追加するだけ

最後にミリ秒やマイクロ秒の情報を追加してあげればよいだけです。
方法はどうにでも出来ますが、以下サンプルです。
(最後だけ、返り値チェックも真面目にやっておきます)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <cstdio> // for perror
#include <ctime>

int main(void) {

struct timespec ts;
struct tm t;
int ret;

// Get epoch time
ret = clock_gettime(CLOCK_REALTIME, &ts);
if (ret < 0) {
perror("clock_gettime fail");
}

// Convert into local and parsed time
localtime_r(&ts.tv_sec, &t);

// Create string with strftime
char buf[32];
ret = strftime(buf, 32, "%Y/%m/%d %H:%M:%S", &t);
if (ret == 0) {
perror("strftime fail");
}

// Add milli-seconds with snprintf
char output[32];
const int msec = ts.tv_nsec / 1000000;
ret = snprintf(output, 32, "%s.%03d", buf, msec);
if (ret == 0) {
perror("snprintf fail");
}

// Result
std::cout << std::string(output) << std::endl;

return 0;
}

これで2019/06/08 19:41:13.712のような結果が得られます。
今回のコードはGitHubに置いてあります。