C++ & ISO8601 : 時刻文字列 "2016-01-25T15:30:15.123+09:00" を出力する例

# 目的

- C++ で ISO8601 形式の時刻文字列を出力する例を提示したい。
    - 1秒未満の値も扱いたい（ミリ秒、マイクロ秒など）
- 当然クロスプラットフォームとして linux, osx, windows(mingw) などで同じ出力を得たい。

## 期待される出力例

ミリ秒まで出力させた場合には次のような出力を得たい。

```
2016-01-25T15:30:15.123+09:00
```

# 実装例

- usagi/chrono/to_string_iso8601.hxx

```cpp
/// @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( &lt, 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( &lt, format_time_zone )
#endif
        ;
      
      return r.str();
    }
  }
}
```

- usagi/chrono/time_zone_difference.hxx

```cpp
/// @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 ( &current_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

```cpp
/// @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 ) );
    }
  }
}
```

## 使い方

```cpp
#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

- http://melpon.org/wandbox/permlink/t3U5JhPvOrWkiSCc

# Note

- タイムゾーンの時刻差分を表示してくれるはずの `%z` がいわゆる windows 環境では `%Z` な挙動を示すのでクロスプラットフォーム対象に windows なやつがいると地味にコストがかかる。
- ついで、いわゆる windows 環境では `%F` `%T` も利かないので地味にこまごまと書く手間がかかる。
- ↑↑こうしたいわゆる windows 環境の問題は MSVCRT を使う場合（mingw経由も含め）に発症するようだ。