目的
- C++ で ISO8601 形式の時刻文字列を出力する例を提示したい。
- 1秒未満の値も扱いたい(ミリ秒、マイクロ秒など)
- 当然クロスプラットフォームとして linux, osx, windows(mingw) などで同じ出力を得たい。
期待される出力例
ミリ秒まで出力させた場合には次のような出力を得たい。
2016-01-25T15:30:15.123+09:00
実装例
- usagi/chrono/to_string_iso8601.hxx
/// @file
/// @brief time_point to iso8601 string converter
#pragma once
#ifdef _WIN32
#include "time_zone_difference.hxx"
#endif
#include <chrono>
#include <string>
#include <sstream>
#include <iomanip>
#include <cmath>
#include <iostream>
namespace usagi
{
namespace chrono
{
// 実際の usagi ライブラリーでは少し細工をしているが、
// この記事では std::chrono 互換の clock で default_clock を与える事を示すに留める
using default_clock = std::chrono::system_clock;
#ifdef _WIN32
// TDM-GCC-5.1.0 is not support %F and %T
// note: mingw is supported. but we cannot predicate TDM or not.
constexpr auto format_date_time = "%Y-%m-%dT%H:%M:%S";
#else
constexpr auto format_date_time = "%FT%T";
#endif
constexpr auto format_time_zone = "%z";
namespace detail
{
template
< typename T = usagi::chrono::default_clock::time_point
, typename U = std::chrono::seconds
>
auto get_sub_seccond_string( const T& time_point )
-> std::string
{
constexpr auto ratio_in_real = static_cast< long double >( U::period::num ) / U::period::den;
constexpr auto sub_second_digits = -std::log10( ratio_in_real );
if ( sub_second_digits < 1 )
return "";
const auto full = std::chrono::duration_cast< std::chrono::duration< long double > >( time_point.time_since_epoch() );
const auto sec = std::chrono::duration_cast< std::chrono::seconds >( time_point.time_since_epoch() );
const auto sub_second_in_real = ( full - sec ).count();
std::stringstream s;
s << std::fixed << std::setprecision( sub_second_digits ) << sub_second_in_real;
constexpr auto in_dot = 1;
return s.str().substr( in_dot );
}
}
/// @brief time_point to iso8601 string
/// @tparam T clock type
/// @param t time_point
/// @return iso8601 string
template
< typename TIME_POINT = default_clock::time_point
, typename SECOND_UNIT = std::chrono::seconds
>
auto to_string_iso8601_gmt
( const TIME_POINT& t = TIME_POINT::clock::now()
)
-> std::string
{
using namespace std::chrono;
const auto& ct = TIME_POINT::clock::to_time_t ( t );
const auto gt = std::gmtime( &ct );
std::stringstream r;
r << std::put_time( gt, format_date_time )
<< detail::get_sub_seccond_string< TIME_POINT, SECOND_UNIT >( t )
<< "Z"
;
return r.str();
}
template
< typename TIME_POINT = default_clock::time_point
, typename SECOND_UNIT = std::chrono::seconds
>
auto to_string_iso8601_jst
( const TIME_POINT& t = TIME_POINT::clock::now()
)
{
using namespace std::chrono;
const auto& ct = TIME_POINT::clock::to_time_t ( t );
auto lt = *std::localtime( &ct );
#ifdef _WIN32
// MSVC++ ( and mingw; MSVCRT ) put an invalid `%z` time zone string, then
// write convertible code and it requred a time zone difference.
const auto z = time_zone_difference();
#endif
std::stringstream r;
r << std::put_time( <, format_date_time )
<< detail::get_sub_seccond_string< TIME_POINT, SECOND_UNIT >( t )
#ifdef _WIN32
<< ( std::signbit( z.count() ) ? "-" : "+" )
<< std::setw( 2 ) << std::setfill( '0' )
<< std::to_string( std::abs( duration_cast< hours >( z ).count() ) )
<< ":"
<< std::setw( 2 ) << std::setfill( '0' )
<< std::to_string( std::abs( duration_cast< minutes >( z ).count() ) % minutes::period::den )
#else
<< std::put_time( <, format_time_zone )
#endif
;
return r.str();
}
}
}
- usagi/chrono/time_zone_difference.hxx
/// @file
/// @brief time zone difference
#pragma once
#include <chrono>
#include <ctime>
namespace usagi
{
namespace chrono
{
template < typename T = std::chrono::minutes >
static auto time_zone_difference()
{
const auto current_time = time ( 0 );
auto local_tm = *gmtime ( ¤t_time );
const auto utc_time = mktime ( &local_tm );
return std::chrono::duration_cast< T >
( std::chrono::duration< double > ( difftime ( current_time, utc_time ) )
);
}
}
}
- to_time_t.hxx
/// @file
/// @brief time_point to time_t converter
#pragma once
#include <chrono>
namespace usagi
{
namespace chrono
{
template < typename T >
inline static auto to_time_t ( const T& t )
{
using std::chrono::system_clock;
using std::chrono::duration_cast;
const auto& dt = t - T::clock::now();
const auto& et = system_clock::now() + duration_cast< system_clock::duration > ( dt );
return std::time_t ( system_clock::to_time_t ( et ) );
}
}
}
使い方
#include <usagi/chrono/to_to_string_iso8601.hxx>
#include <iostream>
auto main() -> int
{
using namespace usagi::chrono;
using namespace std::chrono;
// 現在時刻を GMT で表示
std::cout << to_string_iso8601_gmt() << "\n";
// 現在時刻を JST で表示
std::cout << to_string_iso8601_jst() << "\n";
// 123456789 秒後の time_point を明示的に渡す例
std::cout << to_string_iso8601_jst( steady_clock::now() + seconds( 123456789 ) ) << "\n";
// ミリ秒まで表示(正確かどうかは別の問題)
std::cout << to_string_iso8601_gmt< high_resolution_clock::time_point, milliseconds >() << "\n";
// マイクロ秒まで表示(正確かどうかは別の問題)
std::cout << to_string_iso8601_jst< high_resolution_clock::time_point, microseconds >() << "\n";
// ナノ秒まで表示(正確かどうかは別の問題)
std::cout << to_string_iso8601_jst< high_resolution_clock::time_point, nanoseconds >() << "\n";
// 秒未満の桁を1/100単位(2桁)まで表示(正確かどうかは別の問題)
std::cout << to_string_iso8601_jst< high_resolution_clock::time_point, duration< double, std::ratio< 1, 100 > > >() << "\n";
}
result
2016-01-25T07:14:22Z
2016-01-25T16:14:22+0900
2019-12-24T13:47:31+0900
2016-01-25T07:14:22.561Z
2016-01-25T16:14:22.561232+0900
2016-01-25T16:14:22.561250137+0900
2016-01-25T16:14:22.56+0900
wandbox
Note
- タイムゾーンの時刻差分を表示してくれるはずの
%z
がいわゆる windows 環境では%Z
な挙動を示すのでクロスプラットフォーム対象に windows なやつがいると地味にコストがかかる。 - ついで、いわゆる windows 環境では
%F
%T
も利かないので地味にこまごまと書く手間がかかる。 - ↑↑こうしたいわゆる windows 環境の問題は MSVCRT を使う場合(mingw経由も含め)に発症するようだ。