I had just completed writing date, which is a
library for extending <chrono> into the realm of calendars, and I was
looking around for the most challenging date time problem I could find with which I could
demonstrate the power of this new library. "I know," I said to myself, "I'll handle all
of the world's time zones, and maybe even leap seconds!" Thus began my journey into a
rabbit hole which I knew existed, but had never truly appreciated the intricacies of.
This library adds timezone and leap second support to this date
library. This is a separate library from date
because many clients of date do not need timezone
nor leap second support, and this support does not come for free (though the cost is quite
reasonable).
This library is a complete parser of the IANA Time Zone Database. This database contains timezone information that represents the history of local time for many representative locations around the globe. It is updated every few months to reflect changes made by political bodies to time zone boundaries, UTC offsets, and daylight-saving rules. The database also maintains a list of leap seconds from 1972 through the present.
The IANA Time Zone Database contains four specific types of data:
Zone: A geographic location with a human-readable name (e.g. "America/New_York") which specifies the offset from UTC and an abbreviation for the zone. This data includes daylight saving rules, if applicable, for the zone. This data is not only the rules currently in effect for the region, but also includes specifications dating back to at least 1970, and in most cases dating back to the mid 1800's (when uniform time was first introduced across regions larger than individual towns and cities).
Rule: A specification for a single daylight-saving rule. This helps implement and consolidate the specifications of Zones.
link: This is an alternative name for a Zone.
leap: The date of the insertion of a leap second.
The library documented herein provides access to all of this data, and offers
efficient and convenient ways to compute with it. And this is all done based on the date library, which in turn is based on the C++11/14
<chrono> library. So once you've learned those fundamental libraries,
the learning curve for this library is greatly eased.
Here is an overview of all the types we are going to talk about at some point. They are all fully covered in the reference section. This link is just there to give you a view of everything on one quick page so that you don't get lost or overwhelmed. Many of these types will never need to be explicitly named in typical use cases.
tz_types.jpeg
Everything documented below is in namespace date. Explicit references to
this namespace in example code below is intentionally omitted in the hopes of reducing
verbosity.
One of the first things people want to do is find out what the current local time it is. Here is a complete program to print out the local time in human readable format:
#include "date/tz.h"
#include <iostream>
int
main()
{
using namespace date;
using namespace std::chrono;
auto t = make_zoned(current_zone(), system_clock::now());
std::cout << t << '\n';
}
This just output for me:
2016-05-14 18:33:24.205124 EDT
There are some noteworthy points about this program:
This is a <chrono>-based system. The current time is
found with std::chrono::system_clock::now().
The computer's current local time zone is not assumed. If anything is assumed that
would be UTC, since this is the time zone that system_clock tracks
(unspecified but de facto standard).
Specifying you want to convert system_clock::time_points to the
current local time zone is as easy as calling date::current_zone()
and pairing that with a system_clock::time_point using
date::make_zoned. This creates a zoned_time.
This zoned_time maintains whatever precision it was given. On my
platform system_clock::now() has microseconds precision, so in this
example, t has microseconds precision as well.
Then t is simply streamed out. By default the output
represents all of the precision it is given.
Everything about the above program can be customized: the precision, the formatting, and the time zone. But by default, things just work, and don't throw away information.
For example let's say we wanted to limit the precision to milliseconds. This can
be done by inserting floor<milliseconds> in one place. This
makes t have just a precision of milliseconds
and that is reflected in the streaming operator with no further effort:
auto t = make_zoned(current_zone(), floor<milliseconds>(system_clock::now())); std::cout << t << '\n'; // 2016-05-14 18:33:24.205 EDT
Seconds precision is just as easy:
auto t = make_zoned(current_zone(), floor<seconds>(system_clock::now())); std::cout << t << '\n'; // 2016-05-14 18:33:24 EDT
The entire time_get / time_put formatting capability is
also at your fingertips (and at any precision):
auto t = make_zoned(current_zone(), system_clock::now());
std::cout << format("%a, %b %d, %Y at %I:%M %p %Z", t) << '\n';
// Sat, May 14, 2016 at 06:33 PM EDT
Using any std::locale your OS supports:
auto t = make_zoned(current_zone(), floor<seconds>(system_clock::now()));
std::cout << format(locale("de_DE"), "%a, %b %d, %Y at %T %Z", t) << '\n';
// Sa, Mai 14, 2016 at 18:33:24 EDT
From the previous section:
Hmm... German locale in an American time zone.
We can fix that easily too:
auto zone = locate_zone("Europe/Berlin");
auto t = make_zoned(zone, floor<seconds>(system_clock::now()));
std::cout << format(locale("de_DE"), "%a, %b %d, %Y at %T %Z", t) << '\n';
// So, Mai 15, 2016 at 00:33:24 CEST
The date::locate_zone() function looks up the IANA time zone with the name
"Europe/Berlin" and returns a const time_zone* which has no ownership
issues and can be freely and cheaply copied around. It is not possible for
locate_zone() to return nullptr, though it might throw
an exception if pushed far enough (e.g. locate_zone("Disney/Mickey_Mouse")).
You can also call make_zoned with the time zone name right in the call:
auto t = make_zoned("Europe/Berlin", floor<seconds>(system_clock::now()));
The first way is very slightly more efficient if you plan on using zone
multiple times since it then only has to be looked up once.
time_zone from one time zone to another?
So far we've only looked at converting from system_clock::now() to
a local, or specific time zone. We've used make_zoned with the
first argument being either current_zone() or a specification for
some other time zone, and the second argument being a
system_clock::time_point. So far so good.
But now I have a video-conference meeting on the first Monday of May, 2016 at 9am New York time. I need to communicate that meeting with partners in London and Sydney. And the computation is taking place on a computer in New Zealand (or some other unrelated time zone). What does that look like?
#include "date/tz.h"
#include <iostream>
int
main()
{
using namespace date::literals;
using namespace std::chrono_literals;
auto meet_nyc = make_zoned("America/New_York", date::local_days{Monday[1]/May/2016} + 9h);
auto meet_lon = make_zoned("Europe/London", meet_nyc);
auto meet_syd = make_zoned("Australia/Sydney", meet_nyc);
std::cout << "The New York meeting is " << meet_nyc << '\n';
std::cout << "The London meeting is " << meet_lon << '\n';
std::cout << "The Sydney meeting is " << meet_syd << '\n';
}
The output is the following. But before you forward it, send a generous bonus to the guys in Australia.
The New York meeting is 2016-05-02 09:00:00 EDT The London meeting is 2016-05-02 14:00:00 BST The Sydney meeting is 2016-05-02 23:00:00 AEST
The first time, meet_nyc is a pairing of a time zone ("America/New_York")
with a local time (Monday[1]/May/2016 at 09:00). Note that this
input is exactly reflected in the output:
The New York meeting is 2016-05-02 09:00:00 EDT
The next line creates meet_lon with the zoned_time
meet_nyc and a new time zone: "Europe/London". The effect of this pairing
is to create a time_point with the exact same UTC time point, but
associated with a different time_zone for localization purposes. That is,
after this "converting construction", an invariant is that
meet_lon.get_sys_time() == meet_nyc.get_sys_time(), even though these
two objects refer to different time zones.
The same recipe is followed for creating meet_syd. The default formatting
for these zoned_times is to output the local date and time followed
by the current time zone abbreviation.
Summary: zoned_time is a pairing of local or UTC time with a time_zone.
The result is a well-specified point in time. And it carries with it the ability to
serve as a translator to any other time_point which carries time zone
information (to any precision).
local_time vs sys_time
Let's say I want to refer to the New Years Day party at 2017-01-01 00:00:00. I don't
want to refer to a specific party at some geographical location. I want to refer to
the fact that this moment is celebrated in different parts of the world according to
local times. This is called a local_time.
auto new_years = local_time<days>{2017_y/January/1} + 0h + 0m + 0s;
A local_time<D> can be created with any duration D and
is a std::chrono::time_point except that
local_time<D>::clock has no now() function. There is
no time zone associated with local_time.
local_timeis not the time associated with the current local time the computer is set to.
local_time is a time associated with an as yet
unspecified time zone. Only when you pair a local_time with a
time_zone do you get a concrete point in time that can be converted
to UTC and other time zones: a zoned_time.
There also exist convenience type aliases:
using local_seconds = local_time<std::chrono::seconds>; using local_days = local_time<days>;
In summary: When is 1min after New Years 2017?
auto t = local_days{January/1/2017} + 1min;
cout << t << '\n'; // 2017-01-01 00:01
When is 1min after New Years 2017 UTC?
auto t = sys_days{January/1/2017} + 1min;
cout << t << '\n'; // 2017-01-01 00:01
This effectively means that year_month_day is also ambiguous as to
whether it refers to a local (timezone-less) time or to UTC. You have to
specify which when you use it. But that is the nature of how people use dates
(points in time with days precision). "There will be a celebration on New Years."
In many contexts the time zone is intentionally left unspecified.
When is 1min after New Years 2017 in New York?
zoned_seconds t{"America/New_York", local_days{January/1/2017} + 1min};
cout << t << '\n'; // 2017-01-01 00:01:00 EST
What time will it be in New York when it is 1min after New Years 2017 UTC?
zoned_seconds t{"America/New_York", sys_days{January/1/2017} + 1min};
cout << t << '\n'; // 2016-12-31 19:01:00 EST
We now have 5 concepts and their associated types:
Calendars: These are day-precision time points that are typically field structures (multiple fields that create a unique "name" for a day).
Example calendars include year_month_day and
year_month_weekday. Other examples could include the ISO
week-based calendar, the Julian calendar, the Islamic calendar, the Hebrew
calendar, the Chinese calendar, the Mayan calendar, etc.
Calendars can convert to and from both sys_days and
local_days. These two conversions involve identical arithmetic, but
have semantic differences.
Once these conversions are implemented, the calendars are not only interoperable
with zoned_time, but are also interoperable with each other. That
is dates in the Chinese calendar can easily be converted to or from dates in the
Mayan calendar even though these two calendar