简体   繁体   中英

Convert double to zoned_time using Howard Hinnant's date library

I have a double representing the time in days since midnight (local time zone) 1 Jan 1970 and a string representing the time zone. I would like to convert these to a date::zoned_time using Howard Hinnant's date and time zone library.

The background is I need to convert date-times to and from doubles to use in an analytics library. I will also receive date-times as doubles from excel in a local or user-specified time zone.

Here is one attempt I made

using namespace date;
using namespace std::chrono;
typedef date::zoned_time<std::chrono::seconds> datetime;
const double UNIX_MINUS_EXCEL_EPOCH = 25569.0;
const double SECONDS_PER_DAY = 24.0 * 60.0 * 60.0;
datetime timePointFromDouble(double x)
{
    double seconds = (x - UNIX_MINUS_EXCEL_EPOCH) * SECONDS_PER_DAY;
    system_clock::duration d = duration_cast<system_clock::duration>(duration<double>(seconds));
    system_clock::time_point t = system_clock::time_point(d);
    auto xx = make_zoned("America/Chicago", t);

    return xx;
}

It doesn't compile because the result of make_zoned has the wrong type. Also, I am not convinced it correctly maps the input time in days to the output date-time because of leap seconds and days where daylight saving changes.

Specification:

  • x is a measure of days since 1899-12-30 00:00:00 in America/Chicago.

Solution:

using datetime = date::zoned_seconds;

datetime
timePointFromDouble(double x)
{
    using namespace date;
    using namespace std::chrono;
    using ddays = duration<double, days::period>;
    constexpr auto excel_epoch = local_days{1_d/January/1970} -
                                 local_days{30_d/December/1899};
    return datetime{"America/Chicago",
             local_seconds{round<seconds>(ddays{x} - excel_epoch)}};
}

Explanation:

The reason your version doesn't compile is because of the conversion to system_clock::time_point , which in practice has a precision of microseconds or finer. But your result type has a precision of seconds, so the library is refusing to implicitly truncate your high-precision t to your lower-precision xx .

The easiest way to fix this is to time_point_cast<seconds>(t) . But there's more fun to be had...

<chrono> lives and dies by handling the conversions for you. Any time you're doing the conversions yourself, you should opt for removing those conversions in favor of letting <chrono> do them. This will usually simplify your code, and it may just catch a conversion error.

<chrono> knows about how to convert among various durations, but not about the Excel epoch, so that's one conversion we can't avoid. But we can express that epoch in a higher level language than the mysterious constant 25569.0.

So, from the top:

  • date::zoned_seconds is a simpler way to write date::zoned_time<std::chrono::seconds> . It is just a convenience typedef.

  • ddays is a custom duration unit which represents 1 day with a double . This is convenient for converting the scalar input x directly into a <chrono> duration. It is best to get into the <chrono> type system as soon as possible.

  • The epoch difference is the amount of time between 1970-01-01 and 1899-12-30. The units will be days as I've coded it, but that is an unimportant detail. <chrono> takes care of the units for you.

  • I'm using local_days as opposed to sys_days to compute the epoch difference. This is a largely symbolic gesture to communicate that the epoch is in a local time, not UTC. It doesn't make a difference in the actual value of the constant that is computed.

  • Because of the way you worded the question, I assumed you would prefer day-month-year ordering in the code. This is a purely stylistic choice.

  • If you are writing this in C++11, excel_epoch will have to be made const instead of constexpr . The difference is that C++11 has to compute this constant at run-time and C++14 and later can compute it at compile-time.

  • When converting from double-based units to integral-based units, I like to use round instead of duration_cast . The difference is that round chooses the nearest representable value, and duration_cast truncates towards zero to the nearest representable value. The round strategy is more likely to result in stable round-trip conversions between the double and integral representations, whereas truncation is more likely to expose one-off differences due to round-off error in the double representation.

  • The last line has to explicitly get us from double-based to integral-based units, and has to specify seconds to match with the return type, but does not have to worry about converting days into seconds.

  • The last line uses local_seconds to convert the duration into a time_point because this duration represents a measure in the local time of America/Chicago, as opposed to a measure in UTC. This fixes the epoch to 1899-12-30 00:00:00 in America/Chicago as opposed to 1899-12-30 00:00:00 UTC.

  • The result does not take leap seconds into account. This is the correct thing to do, because neither does Excel nor system_clock . Just about every computer-based time-keeping protocol out there doesn't count leap seconds. Here is a good description of Unix Time . If you want to convert to a system that counts leap seconds, this library can do that too. It is called utc_clock / utc_time .

  • The result does take daylight savings time into account for Chicago, including the changes to the daylight savings rules over the years, as best as the IANA database can do (which is exact as far as I know).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM