Search code examples
c++datetime

error while converting broken time into correct date time with timezone and dst using HowardHinnant's date library


error while running program with Howard Hinnat library My program is as following

#include "date/tz.h"
#include <iostream>
#include <sstream>

int main() {
    // Example broken-down time components
    int year = 2023;
    int month = 3; // March
    int day = 12;
    int hour = 2;
    int minute = 0;

    // Timezone
    std::string timezone = "America/New_York";
    //std::string timezone = "US/Central";

    // Construct a local_time type from the broken-down time
    auto localTime = date::local_days{date::year{year}/month/day} + std::chrono::hours{hour} + std::chrono::minutes{minute};

    // Convert local time to zoned time with DST adjustments
    auto zonedTime = date::make_zoned(timezone, localTime);

    // Output the original and converted date-time
    std::cout << "Original broken-down time: " << year << "-" << month << "-" << day << " " << hour << ":" << minute << std::endl;
    std::cout << "Converted time with DST: " << zonedTime << std::endl;

    return 0;
}

I am getting following error

terminate called after throwing an instance of 'date::nonexistent_local_time' what(): 2023-03-12 02:00:00 is in a gap between 2023-03-12 02:00:00 EST and 2023-03-12 03:00:00 EDT which are both equivalent to 2023-03-12 07:00:00 UTC

I have copied tzdata to Downloads folder and put windowsZones.xml in it.

Can any one help ? I am completely new to this.

I am using x86_64-w64-mingw32-g++.exe compiler with version gcc version 13.2.0 (Rev4, Built by MSYS2 project) with thread model posix in cygwin environment


Solution

  • I just ran your program on macOS with the latest IANA timezone database and got identical results:

    libc++abi: terminating due to uncaught exception of type date::nonexistent_local_time: 2023-03-12 02:00:00 is in a gap between
    2023-03-12 02:00:00 EST and
    2023-03-12 03:00:00 EDT which are both equivalent to
    2023-03-12 07:00:00 UTC
    

    This exception is propagating out of the zonedTime constructor which has type date::zoned_time.

    It turns out that the timezone "America/New_York" changes from "standard time" to "daylight saving time" on 2023-03-12 02:00:00 EST by increasing its UTC offset from -5h to -4h. As it does this the local time skips from 2am to 3am.


    2023-03-12 02:00:00 local time does not exist in this timezone.


    You've done nothing wrong, and your program is correct. It is simply that you've hit an exceptional condition.

    How you want to handle this exceptional condition is up to you, and you've got options:

    1. You could reject the input local time 2023-03-12 02:00:00 since it doesn't exist, and move on, say to the next input local time.
    2. You could create an artificial mapping of local time to UTC such that the entire hour [2am, 3am) maps to 2023-03-12 07:00:00 UTC / 2023-03-12 03:00:00 EDT.

    To choose the latter change the zonedTime construction to:

    auto zonedTime = date::make_zoned(timezone, localTime, date::choose::earliest);
    

    Now the exception is suppressed, and the output is:

    Original broken-down time: 2023-3-12 2:0
    Converted time with DST: 2023-03-12 03:00:00 EDT
    

    With this option, the converted time will be same for all values of the input time from 2:00:00 to 2:59:59.999... .

    You could also use date::choose::latest with the same effects. The choice makes no difference in this example. But when going the other way: from daylight saving to standard, there will be a difference between earliest and latest. In this case there will be two mappings from local time to UTC, instead of 0 mappings (the local time happens twice). You can choose to map either to the earliest local time, or the latest local time (ordered by their UTC equivalent).

    Bonus Question 1:

    Are there any api/function which will tell us if given date time is in dst or not. Like 2023-03-12 04:00:00 is in dst but Nov 07, 2021 03:00 is not in dst?

    Yes. Given a sys_time or a local_time, paired with a time_zone, there is a sys_info object containing everything there is to know about that instant in time in that timezone.

    sys_info documentation

    struct sys_info
    {
        sys_seconds          begin;
        sys_seconds          end;
        std::chrono::seconds offset;
        std::chrono::minutes save;
        std::string          abbrev;
    };
    

    If the save member is 0min then the associated time_point/time_zone is not in daylight saving, else it is.

    Example code to extract this information:

    #include "date/tz.h"
    #include <chrono>
    #include <iostream>
    
    template <class Duration>
    bool
    is_dst(date::zoned_time<Duration> const& zt)
    {
        using namespace std::literals;
    
        auto info = zt.get_info();
        return info.save != 0min;
    }
    
    int
    main()
    {
        using namespace date;
        using namespace std;
        using namespace chrono;
    
        auto tz = locate_zone("America/New_York");
        cout << is_dst(zoned_time{tz, local_days{2023_y/3/12} + 4h}) << '\n';
        cout << is_dst(zoned_time{tz, local_days{November/7/2021} + 3h}) << '\n';
    }
    

    Output:

    1
    0
    

    Bonus Question 2:

    Is there any api which will take input as 2023-03-12 04:00:00 EST but will return 2023-03-12 05:00:00 EDT?

    Sort of. You're not going to like it. For the specific example of EST to EDT, yes. But in general, no.

    The reason it works for EST to EDT is that there exists an IANA timezone that goes by the name of EST that is always offset by -5h. But the same is not true of most other timezone abbreviations including EDT.

    I can make this specific example work by creating a zoned_time in the EST time_zone, and then finding the equivalent time in America/New_York.

    #include "date/tz.h"
    #include <chrono>
    #include <iostream>
    
    int
    main()
    {
        using namespace date;
        using namespace std;
        using namespace chrono;
    
        zoned_time zt1{"EST", local_days{2023_y/03/12} + 4h};
        zoned_time zt2{"America/New_York", zt1};
        cout  << zt1 << '\n';
        cout  << zt2 << '\n';
    }
    

    Output:

    2023-03-12 04:00:00 EST
    2023-03-12 05:00:00 EDT
    

    However, in general there is no easy way to convert a "daylight saving" time_point to a "non daylight saving" time_point, or vice-versa.

    I say "easy" because there are always low-level hacks that one can do with this library. But there lies the rabbit hole. You'll get into situations that work only sometimes. Maybe even most of the time if you're really unlucky. There's no sure-fire technique.

    Bonus Question 3:

    How to convert zt2 into broken time (year, month, days, hour, min and sec)?

    // break zt2 into local field values
    
    // get local time_point which is a "datetime"
    auto tpl = zt2.get_local_time();
    // get date as count of days
    auto tpd = floor<days>(tpl);
    // get time of day: datetime - date
    auto tod = tpl - tpd;
    // convert date from {count} data structure to {year, month, day}
    year_month_day ymd{tpd};
    // convert duration since midnight into {hours, minutes, seconds}
    hh_mm_ss hms{tod};
    
    // ymd and hms together have all of the fields.
    // There is a getter for each field to get a type safe field
    
    year y = ymd.year();
    month m = ymd.month();
    day d = ymd.day();
    
    hours h = hms.hours();
    minutes M = hms.minutes();
    seconds s = hms.seconds();
    
    // Each field has a way to convert to integral types
    // This step is considered unsafe.  It is better to 
    // stay within the chrono type system!
    
    int yi{y};
    unsigned mi{m};
    unsigned di{d};
    
    int64_t hi = h.count();
    int64_t Mi = M.count();
    int64_t si = s.count();
    

    To break down the sys_time from zt2 do the same thing, except start with zt2.get_sys_time() instead of zt2.get_local_time().

    To go the other way, from type safe fields into a local time_point:

    tpl = local_days{y/m/d} + h + M + s;
    

    Use sys_days in place of local_days if you are reversing a sys_time breakdown.

    If you are starting with integral types, each integral type will first have to be explicitly converted into a type safe field, for example:

    year{yi} // etc.