How to convert a local_time to zoned_time (or sys_time) without throwing ambiguous_local_time?
Given that we know a local_time and know the "isdst" i.e. is the daylight saving rule in effect, how to convert a local_time to zoned_time (or sys_time) without risking exception throwings? How to use choose::earliest and choose::latest?
In other words, what is the equivalent of the job of mktime. mktime received the tm_isdst and a local timestamp, and provided a unix time (similar to sys_time).
There is a way to do this, but it is not obvious, easy or convenient. And the reason is that the known inputs to this problem are not usually "I know the this local time is in daylight saving". There is no consistency between time zones in how they label which season is "daylight saving".
For example currently Europe/Dublin refers to standard time between March and October and daylight saving between October and March.
Generally speaking mktime maps a local field type ({y, m, d, h, M, s}) to a UTC serial type ({count of seconds}). And generally this library does that functionality with:
sys_seconds t = zoned_time{current_zone(), local_days{y/m/d} + h + M + s}.get_sys_time();
Now the problem comes in when either:
- There is no mapping from
{y, m, d, h, M, s}to UTC, or: - There are two mappings from
{y, m, d, h, M, s}to UTC.
If we're in case 1, there is no right answer in general because {y, m, d, h, M, s} never actually existed in local time. E.g. We skipped from 2am to 3am and you're requesting a mapping from 2:30am. However applications may still choose to map 2:30am to the UTC equivalent of 3am. And in this case, choosing either earliest or latest will give that answer.
If we're in case 2, then there are 2 right answers. E.g. at 2am the clock was set back to 1am, and we're asking for 1:30am, which occurs twice on this date. If we're in New York, this means we're coming off of daylight saving, and so the first 1:30am is what you want: choose::earliest. But if we're in Dublin then we're going on to daylight savings and so the second mapping is correct: choose::latest.
So knowing only that tm_isdst > 0 isn't sufficient to reliably get the right answer. However, I said there's a way to do it anyway (turns out still not be 100% reliable) and here it is:
template <class Duration>
auto
map_prefer_daylight_saving(date::time_zone const* tz,
date::local_time<Duration> t)
{
using namespace std::chrono;
using namespace date;
auto info = tz->get_info(t);
if (info.result == local_info::ambiguous)
{
if (info.first.save == 0min && info.second.save != 0min)
return tz->to_sys(t, choose::latest);
else if (info.first.save != 0min && info.second.save == 0min)
return tz->to_sys(t, choose::earliest);
throw std::runtime_error("Problem is above my paygrade");
}
return tz->to_sys(t);
}
One has to drop down to a very low-level API in the library. Given a local_time and a time_zone, one can get a local_info which returns everything there is to know about a time zone's mapping at that local point in time: https://howardhinnant.github.io/date/tz.html#local_info
A local_info has a result member which is one of these 3 enum values:
-
unique -
nonexistent -
ambiguous
This means that the mapping to UTC from this local time is either unique (exactly one mapping), does not exist (0 mappings), or there are two mappings.
If there is 1 mapping, we just use it: return tz->to_sys(t).
If there are 0 mappings, we let return tz->to_sys(t) throw an exception.
If there are 2 mappings, then we need to dig into the info to find the right answer. A local_info (with an ambiguous value) is a pair of sys_info (https://howardhinnant.github.io/date/tz.html#sys_info), sorted by chronological order. A sys_info has (among other things) a member called save with type minutes which is essentially equivalent to tm_isdst. save != 0min is the same as tm_isdst > 0. So if the first info has a save of 0min and the second doesn't, we choose the second. If the second info has a save of 0min and the first doesn't, we choose the first. Otherwise we throw an exception.
The latter can happen. Sometimes the UTC offset for a time zone is set backward without declaring that one side of the shift is daylight saving and the other side is not.
For example on 2019-01-01 01:00:00 UTC, Africa/Sao_Tome changed its UTC offset from 1h to 0h (creating two local 00:30:00 local times on 2019-01-01) and both offsets are considered "standard time."
And as another example on 2008-01-21 02:00:00 UTC, America/Argentina/San_Luis changed its UTC offset from -2h to -3h and both offsets were considered daylight saving.
<Bonus Points> These facts were gleaned from the IANA database with a program using this library which was able to efficiently search the entire space-time domain of the IANA database for these conditions.
You probably weren't aware of how complicated and ugly time zones could get (regardless of the library). And that's why governments like to change the UTC offset at the least interesting local time possible. :-) This library makes the common cases easy. But if you really want to explore the ugliness, there's low-level API to help you do that too.