Howard E. Hinnant
2021-03-22

Time Zone Database Parser

Contents

Introduction

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:

  1. 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).

  2. Rule: A specification for a single daylight-saving rule. This helps implement and consolidate the specifications of Zones.

  3. link: This is an alternative name for a Zone.

  4. 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.

Description

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.

What is the current local time?

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:

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

What time is it somewhere else in the world?

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.

How do I convert a 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_time is 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

Summary

We now have 5 concepts and their associated types:

  1. 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