HowardHinnant / date

A date and time library based on the C++11/14/17 <chrono> header
Other
3.14k stars 677 forks source link

Why "choose" has no effect when local_time is nonexistent? #711

Closed guoang closed 2 years ago

guoang commented 2 years ago

The doc says:

choose: For some conversions from local_time to a sys_time, choose::earliest or choose::latest can be used to convert a non-existent or ambiguous local_time into a sys_time, instead of throwing an exception.

However, the following code outputs two same lines:

std::cout << make_zoned(
                 "America/New_York",
                 local_days{Sunday[2] / March / 2016} + 2h + 30min,
                 choose::earliest)
          << std::endl;                 // 2016-03-13 03:00:00 EDT
std::cout << make_zoned(
                 "America/New_York",
                 local_days{Sunday[2] / March / 2016} + 2h + 30min,
                 choose::latest)
          << std::endl;                 // 2016-03-13 03:00:00 EDT

And when I try to debug, I found:

template <class Duration>
sys_time<typename std::common_type<Duration, std::chrono::seconds>::type>
time_zone::to_sys_impl(local_time<Duration> tp, choose z, std::false_type) const
{
    auto i = get_info(tp);
    if (i.result == local_info::nonexistent)
    {
        return i.first.end;
    }
    else if (i.result == local_info::ambiguous)
    {
        if (z == choose::latest)
            return sys_time<Duration>{tp.time_since_epoch()} - i.second.offset;
    }
    return sys_time<Duration>{tp.time_since_epoch()} - i.first.offset;
}

Is this a bug or a feature? If it's a feature, why?

HowardHinnant commented 2 years ago

This is a feature.

This local time point does not exist. On this date the local time jumps from 2:00 to 3:00. Without a choose argument, an exception of type nonexistent_local_time would be thrown with the following message:

2016-03-13 02:30:00 is in a gap between 2016-03-13 02:00:00 EST and 2016-03-13 03:00:00 EDT which are both equivalent to 2016-03-13 07:00:00 UTC

The choose argument is used to force a mapping between local time and UTC even though there is none. In this case, both choose values will map all non-existent local times in the range [2:00, 3:00) to 7:00 UTC, which is the closest mapping to this local time "gap". I.e. 1:59 local maps to 6:59 UTC and 3:00 local maps to 7:00 UTC.

Had the local time point been local_days{Sunday[1] / November / 2016} + 1h + 30min then another type of mapping error occurs: ambiguity. On this date the local times in the range [1:00, 2:00) occur twice. If a choose argument is not supplied an exception of type ambiguous_local_time is thrown with the message:

2016-11-06 01:30:00 is ambiguous. It could be 2016-11-06 01:30:00 EDT == 2016-11-06 05:30:00 UTC or 2016-11-06 01:30:00 EST == 2016-11-06 06:30:00 UTC

With the choose arguments as you show, either the first (earliest) or second (latest) mapping can be chosen:

2016-11-06 01:30:00 EDT
2016-11-06 01:30:00 EST

(note the different abbreviations). These local times correspond to 2016-11-06 05:30:00 UTC and 2016-11-06 06:30:00 UTC respectively.

So in summary, the feature is that you can use choose to pick an "approximate" mapping from local to UTC when no unique mapping exists. And the value of choose allows you to customize that mapping when it is ambiguous. If the mapping is unique, the choose argument has no effect. If the mapping is non-existent, the choose argument keeps an exception from being thrown, but the value of the choose argument has no impact as there is really only one reasonable (yet imaginary) mapping to choose from.

guoang commented 2 years ago

Got it, thank you :)