c++timestampc++14c++-chronoformatdatetime

Convert timestamp to the formatted date time using C++


I can only use C++ standard library (C++14) to convert timestamp to the given format date-time. I new to C++ and I know that C++ doesn't support us a lot via a library like Java. At a given date and time of 2011-03-10 11:23:56 in the Central European Time zone (CET), the following standard-format output will be produced: "2011-03-10T11:23:56.123+0100".

std::string format = "yyyy-MM-dd'T'HH:mm:ss'.'SSSZ"; //default format
auto duration = std::chrono::system_clock::now().time_since_epoch();
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();

My syntax for the format string would be

G : Era 
yy,yyyy : year (two digits/four digits) 
M,MM : month in number (without zero/ with zero) - (e.g.,1/01) 
MMM,MMMM : month in text (shortname/fullname)- (e.g.,jan/january) 
d,dd : day in month (without zero/with zero)- (e.g.,1/01) 
D : day in year
F : day of week of month
E, EEEE : day of week 
h,hh : hours(1-12) (without zero/with zero)- (e.g.,1/01) 
H,HH : hours(0-23) (without zero/with zero)- (e.g.,1/01) 
m,mm : minutes (without zero/with zero)- (e.g.,1/01) 
s,ss : seconds (without zero/with zero)- (e.g.,1/01) 
S,SS,SSS : milliseconds 
w,W : Week in year (without zero/with zero)- (e.g.,1/01) 
a : AM/PM 
z,zzzz : timezone name 

Solution

  • This is a bit of tricky question because:

    1. It isn't explicitly stated what the input is. But from the example code I'm going to assume std::chrono::system_clock::time_point.

    2. It is important to recognize that Central European Time zone (CET) is defined as a time zone with a fixed UTC offset of 1h. Some geographical regions follow this time zone rule year round, some do not. And none have always followed it. In any event, this part of the problem allows us to hard-code the UTC offset involved: 1h. There is no daylight saving adjustment to make.

    In C++14 there are two ways to do this without involving copyrighted (even open-source) 3rd party software:

    1. Use the C API.

    2. Roll your own.

    The problem with 1 is that it is error prone. It doesn't directly handle millisecond precision. It doesn't directly handle specific time zones such as CET. The C API only knows about UTC, and the computer's locally set time zone. But these problems are surmountable.

    The problem with 2 is that it involves non-intuitive arithmetic to extract the year, month and day fields from a std::chrono::system_clock::time_point.

    Despite the problems with 2, that is the solution I prefer and what I will present below. I'll also show how C++20 will make this much easier.

    In all solutions, I'll formalize the input and output by implementing a function of this form:

    std::string format_CET(std::chrono::system_clock::time_point tp);
    

    Roll your own (C++14)

    There are six discrete steps. It will require these headers and no others:

    #include <chrono>
    #include <string>
    #include <iomanip>
    #include <iostream>
    #include <limits>
    #include <sstream>
    

    A. Shift the input by the +1 hour UTC offset.

    // shift time_point to CET
    tp += 1h;
    

    A function-local using directive is convenient to bring the UDL h into scope, and everything else that will be needed from <chrono> within this function:

    using namespace std::chrono;
    

    B. Get two variations of the time_point tp: One with millisecond precision and one with day precision:

    // Get time_points with both millisecond and day precision
    auto tp_ms = time_point_cast<milliseconds>(tp);
    auto tp_d = time_point_cast<days>(tp_ms);
    

    It is important to understand that these two casts round towards zero, and will give incorrect results for negative time points. system_clock gives negative time points prior to its epoch of 1970-01-01 00:00:00 UTC. C++17 introduces floor<millliseconds>(tp) which fixes this problem.

    The day-precision time_point will be used to extract the year, month and day fields, and the millisecond-precision time_point will be used to extract the hour, minute, second and millisecond fields. The duration days used above won't be added until C++20, but you can do it with:

    using days = std::chrono::duration<int, std::ratio<86400>>;
    

    C. To get the year, month and day fields from tp_d it is convenient to use one of the public domain algorithms for calendrical operations. This is not a 3rd party library. It is algorithms for writing your own calendrical libraries (which is what I'm in the middle of explaining). I have customized the civil_from_days algorithm to exactly fix the needs of format_CET:

    // Get {y, m, d} from tp_d
    auto z = tp_d.time_since_epoch().count();
    static_assert(std::numeric_limits<unsigned>::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits<int>::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    z += 719468;
    const int era = (z >= 0 ? z : z - 146096) / 146097;
    const unsigned doe = static_cast<unsigned>(z - era * 146097);          // [0, 146096]
    const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;  // [0, 399]
    int y = static_cast<int>(yoe) + era * 400;
    const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);                // [0, 365]
    const unsigned mp = (5*doy + 2)/153;                                   // [0, 11]
    const unsigned d = doy - (153*mp+2)/5 + 1;                             // [1, 31]
    const unsigned m = mp + (mp < 10 ? 3 : -9);                            // [1, 12]
    y += (m <= 2);
    

    There is an exhaustingly detailed derivation of this algorithm at the site linked above for those who want to know how it works.

    At this point, the integral variables {y, m, d} contain the year, month, day triple.

    D. Get the time duration since the local midnight. This will be used to extract the local time of day:

    // Get milliseconds since the local midnight
    auto ms = tp_ms - tp_d;
    

    E. Get the hour, minute, second and millisecond fields:

    // Get {h, M, s, ms} from milliseconds since midnight
    auto h = duration_cast<hours>(ms);
    ms -= h;
    auto M = duration_cast<minutes>(ms);
    ms -= M;
    auto s = duration_cast<seconds>(ms);
    ms -= s;
    

    At this point, the chrono::duration variables {h, M, s, ms} hold the desired values.

    F. Now we're ready to format:

    // Format {y, m, d, h, M, s, ms} as yyyy-MM-dd'T'HH:mm:ss'.'SSS+0100
    std::ostringstream os;
    os.fill('0');
    os << std::setw(4) << y << '-' << std::setw(2) << m << '-' << std::setw(2)
       << d << 'T' << std::setw(2) << h.count() << ':'
       << std::setw(2) << M.count() << ':' << std::setw(2) << s.count()
       << '.' << std::setw(3) << ms.count() << "+0100";
    return os.str();
    

    Using a combination of the manipulator setw to set the width of each field, with a fill character of 0, one gets the desired leading zeroes.

    The C++20 solution

    This is much easier in the C++20 spec:

    std::string
    format_CET(std::chrono::system_clock::time_point tp)
    {
        using namespace std::chrono;
        static auto const CET = locate_zone("Etc/GMT-1");
        return std::format("{:%FT%T%z}", zoned_time{CET, floor<milliseconds>(tp)});
    }
    

    "Etc/GMT-1" is the IANA equivalent of Central European Time zone (CET). This time_zone const* is located and stored in the variable CET. The time_point tp is truncated to millisecond-precision, and paired with the time_zone using a zoned_time. This zoned_time is then formatted (to millisecond precision) using the format string shown.

    There exists an open-source (MIT license) preview of the C++20 spec with very minor syntax differences here.

    #include "date/tz.h"
    
    std::string
    format_CET(std::chrono::system_clock::time_point tp)
    {
        using namespace date;
        using namespace std::chrono;
        static auto const CET = locate_zone("Etc/GMT-1");
        return format("%FT%T%z", zoned_time<milliseconds>{CET, floor<milliseconds>(tp)});
    }
    

    Some installation is required for Windows.

    This preview does work with C++14. In C++17 and later zoned_time<milliseconds> can be simplified to just zoned_time.

    Custom time zone support

    There is also a way to use the preview library such that no installation is required. It becomes a header-only library. This is done by creating a custom time zone that models just CET, and then install that in the zoned_time. Here is what the custom time zone could look like:

    #include "date/tz.h"
    
    class CET
    {
    public:
    
        template <class Duration>
            auto
            to_local(date::sys_time<Duration> tp) const
            {
                using namespace date;
                using namespace std::chrono;
                return local_time<Duration>{(tp + 1h).time_since_epoch()};
            }
    
        template <class Duration>
            auto
            to_sys(date::local_time<Duration> tp) const
            {
                using namespace date;
                using namespace std::chrono;
                return sys_time<Duration>{(tp - 1h).time_since_epoch()};
            }
    
        template <class Duration>
            date::sys_info
            get_info(date::sys_time<Duration>) const
            {
                using namespace date;
                using namespace std::chrono;
                return {ceil<seconds>(sys_time<milliseconds>::min()),
                        floor<seconds>(sys_time<milliseconds>::max()),
                        1h, 0min, "CET"};
            }
    
        const CET* operator->() const {return this;}
    };
    

    CET now meets enough of the time zone requirements that it can be used within zoned_time and formatted as before. In C++14, the syntax is complicated by having to explicitly specify the zoned_time template arguments:

    std::string
    format_CET(std::chrono::system_clock::time_point tp)
    {
        using namespace date;
        using namespace std::chrono;
        using ZT = zoned_time<milliseconds, CET>;
        return format("%FT%T%z", ZT{CET{}, floor<milliseconds>(tp)});
    }
    

    This option is also in the C++20 spec, and its advantage is that the time zone abbreviation (which is unused in your problem) will correctly report "CET" instead of "+01".

    More documentation on custom time zones is found here.

    With any of these solutions, the function can now be exercised like this:

    #include <iostream>
    
    int
    main()
    {
        std::cout << format_CET(std::chrono::system_clock::now()) << '\n';
    }
    

    And a typical output looks like this:

    2019-10-29T16:37:51.217+0100