
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
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 periodically 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.
Like date, this library revolves around
std::chrono::system_clock. Thus it is important to understand the properties
of this foundation. std::chrono::system_clock has nested types including
time_point and duration, and a static function called
now which returns a time_point. This time_point
measures time against some unspecified epoch using the units of duration.
namespace std { namespace chrono {
class system_clock
{
public:
using duration = microseconds;
using time_point = chrono::time_point<system_clock>;
// other types ...
static time_point now() noexcept;
// other member functions ...
};
}} // namespace std::chrono
The use of microseconds above for the duration type is just
an example. This is what is used for libc++ on
OS X. Other implementations may use
other units such as nanoseconds. But one unspecified property that all
implementations of std::chrono::system_clock have in common is that they
all model Unix time.
Unix time counts the number of seconds since 1970-01-01 00:00:00 UTC, except that leap seconds are ignored in the count. An interesting and useful property of Unix time is that it treats leap seconds as nothing more than a typical clock correction. That is, no clock keeps perfect time, not even the one in your computer. And several times a day your computer will ask another computer what time it is, and typically correct itself by a small fraction of a second.
When a leap second occurs, instead of counting an extra second, Unix time reacts by saying, oh, I'm off by a second, I'll slow down or speed up to correct. Companies such as Google will "smear" the application of a leap second over a period of hours, letting their Unix time counters adjust by milliseconds, or even microseconds at a time, so that the insertion of a leap second is virtually undetectable to applications running on the computer.
It is this de-facto standard based on
Unix time that allows
date to portably convert a count of microseconds
(for example) into field-based structures containing year/month/day and
hours::minutes::seconds.microseconds. And when you need to further convert
the system_clock::time_point into a local time, and/or take leap seconds into
account, the library documented herein handles it by accessing your local copy of the
IANA Time Zone Database.
This synopsis provides nothing but a quick overview of the documented API of this library. If you don't see it in the synopsis below, it is not a documented part of this library.
namespace date
{
using second_point = std::chrono::time_point<std::chrono::system_clock,
std::chrono::seconds>;
struct Info
{
second_point begin;
second_point end;
std::chrono::seconds offset;
std::chrono::minutes save;
std::string abbrev;
};
std::ostream&
operator<<(std::ostream& os, const Info& r);
enum class tz {utc, local};
enum class choose {earliest, latest};
class nonexistent_local_time
: public std::runtime_error
{
public:
};
class ambiguous_local_time
: public std::runtime_error
{
public:
};
class Zone
{
public:
const std::string& name() const;
template <class Rep, class Period>
std::pair
<
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>,
std::string
>
to_local(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const;
template <class Rep, class Period>
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>
to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const;
template <class Rep, class Period>
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>
to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp,
choose z) const;
template <class Rep, class Period>
Info
get_info(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp,
tz timezone) const;
};
const Zone* locate_zone(const std::string& tz_name);
const Zone* current_zone();
bool operator==(const Zone& x, const Zone& y);
bool operator!=(const Zone& x, const Zone& y);
bool operator< (const Zone& x, const Zone& y);
bool operator> (const Zone& x, const Zone& y);
bool operator<=(const Zone& x, const Zone& y);
bool operator>=(const Zone& x, const Zone& y);
std::ostream& operator<<(std::ostream& os, const Zone& z);
class Link
{
public:
const std::string& name() const;
const std::string& target() const;
};
bool operator==(const Link& x, const Link& y);
bool operator!=(const Link& x, const Link& y);
bool operator< (const Link& x, const Link& y);
bool operator> (const Link& x, const Link& y);
bool operator<=(const Link& x, const Link& y);
bool operator>=(const Link& x, const Link& y);
std::ostream& operator<<(std::ostream& os, const Link& x);
class Leap
{
public:
second_point date() const;
};
bool operator==(const Leap& x, const Leap& y);
bool operator!=(const Leap& x, const Leap& y);
bool operator< (const Leap& x, const Leap& y);
bool operator> (const Leap& x, const Leap& y);
bool operator<=(const Leap& x, const Leap& y);
bool operator>=(const Leap& x, const Leap& y);
std::ostream& operator<<(std::ostream& os, const Leap& x);
template <class Duration>
bool
operator==(const Leap& x,
const std::chrono::time_point<std::chrono::system_clock, Duration>& y);
template <class Duration>
bool
operator==(const std::chrono::time_point<std::chrono::system_clock, Duration>& x,
const Leap& y);
template <class Duration>
bool
operator!=(const Leap& x,
const std::chrono::time_point<std::chrono::system_clock, Duration>& y);
template <class Duration>
bool
operator!=(const std::chrono::time_point<std::chrono::system_clock, Duration>& x,
const Leap& y);
template <class Duration>
bool
operator< (const Leap& x,
const std::chrono::time_point<std::chrono::system_clock, Duration>& y);
template <class Duration>
bool
operator< (const std::chrono::time_point<std::chrono::system_clock, Duration>& x,
const Leap& y);
template <class Duration>
bool
operator> (const Leap& x,
const std::chrono::time_point<std::chrono::system_clock, Duration>& y);
template <class Duration>
bool
operator> (const std::chrono::time_point<std::chrono::system_clock, Duration>& x,
const Leap& y);
template <class Duration>
bool
operator<=(const Leap& x,
const std::chrono::time_point<std::chrono::system_clock, Duration>& y);
template <class Duration>
bool
operator<=(const std::chrono::time_point<std::chrono::system_clock, Duration>& x,
const Leap& y);
template <class Duration>
bool
operator>=(const Leap& x,
const std::chrono::time_point<std::chrono::system_clock, Duration>& y);
template <class Duration>
bool
operator>=(const std::chrono::time_point<std::chrono::system_clock, Duration>& x,
const Leap& y);
class Rule;
struct TZ_DB
{
std::string version;
std::vector<Zone> zones;
std::vector<Link> links;
std::vector<Leap> leaps;
std::vector<Rule> rules;
};
std::ostream& operator<<(std::ostream& os, const TZ_DB& db);
const TZ_DB& get_tzdb();
const TZ_DB& reload_tzdb();
#if HAS_REMOTE_API
std::string remote_version();
bool remote_download(const std::string& version);
bool remote_install(const std::string& version);
#endif
class utc_clock
{
public:
using duration = std::chrono::system_clock::duration;
using rep = duration::rep;
using period = duration::period;
using time_point = std::chrono::time_point<utc_clock>;
static constexpr bool is_steady = true;
static time_point now() noexcept;
template <class Duration>
static
std::chrono::time_point<utc_clock,
typename std::common_type<Duration, std::chrono::seconds>::type>
sys_to_utc(std::chrono::time_point<std::chrono::system_clock, Duration> t);
template <class Duration>
static
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<Duration, std::chrono::seconds>::type>
utc_to_sys(std::chrono::time_point<utc_clock, Duration> t);
};
template <class Duration>
std::string
format(const std::locale& loc, std::string format,
std::chrono::time_point<std::chrono::system_clock, Duration> tp,
const Zone* zone = nullptr);
std::string
format(const std::locale& loc, std::string format, day_point tp,
const Zone* zone = nullptr);
template <class Duration>
std::string
format(std::string format,
std::chrono::time_point<std::chrono::system_clock, Duration> tp,
const Zone* zone = nullptr);
std::string
format(std::string format, day_point tp, const Zone* zone = nullptr);
template <class Duration>
void
parse(std::istream& is, const std::string& format,
std::chrono::time_point<std::chrono::system_clock, Duration>& tp);
template <class Duration>
void
parse(std::istream& is, const std::string& format,
std::chrono::time_point<std::chrono::system_clock, Duration>& tp,
std::string& abbrev);
} // namespace date
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.
The database is represented with the type TZ_DB:
struct TZ_DB
{
std::string version;
std::vector<Zone> zones;
std::vector<Link> links;
std::vector<Leap> leaps;
std::vector<Rule> rules;
};
This is a singleton class. You can get a const TZ_DB& to the singleton
using this function:
const TZ_DB& get_tzdb();
The first call to get_tzdb() will initialize the database from your local
copy of the IANA Time Zone Database located
at install (a file-scope variable of type std::string in
tz.cpp).
You will need to catch the return of this function by const& as the
TZ_DB is not constructible from a const TZ_DB. This can be done
with the following example code:
auto& db = get_tzdb();
With a reference to the database in hand, you have read-only access to the entire
database, which is nothing more than sorted vectors for the four types of
data contained in the database. With such a reference you could (for example) print the
names of all the Zones in the database:
for (auto& z : db.zones)
std::cout << z.name() << '\n';
There are currently 377 zones in the database.
Or you could output the 89 Links, including their name() and
target():
for (auto& link : db.links)
std::cout << link << '\n';
If you aren't happy with the format this outputs in, Link has public member
functions name() and target() so that you can achieve whatever
format you desire.
If needed, db.version is a std::string containing the
IANA Time Zone Database version of the
database you are reading. For example the current version when this sentence
was written was "2016a".
You can even print the entire database out in a semi-human-readable format if desired:
std::cout << db << '\n';
If you constrain the geography or history of the database during installation, those constraints will be reflected in these examples.
If you decide you need to reload the database say, because you want to install a new version of the IANA Time Zone Database without stopping your program, you can use this function:
const TZ_DB& reload_tzdb();
This re-initializes the database by reading from the install location you
customized on installation. The use of the reload_tzdb function is not
pain-free, and not for every application (not for most of them I'm guessing). For example
see the Thread Safety section for issues related to the use of these functions.
The remote API is enabled only if HAS_REMOTE_API is set to 1 during
compilation. See Installation for more details.
std::string remote_version();
This function will query the
IANA Time Zone Database website for the
latest version number of the IANA database, and return it as a std::string.
If an internet connection can not be made, an empty string is returned.
This string can be compared against the version of your local copy of the database:
get_tzdb().version.
bool remote_download(const std::string& version);
This function will attempt to download the database with the version version
from the IANA Time Zone Database website.
If successful, true is returned and a file named
version + ".tar.gz" will be stored at the location install.
If not successful, false is returned.
bool remote_install(const std::string& version);
This function will attempt to uncompress the tar file downloaded by
remote_download(version) and replace any existing database with
the result. It will then delete the tar file. If the tar file doesn't exist,
remote_install will do nothing. Returns true on
success, else returns false.
The Zone class is the most important type in this library. It provides the
main access to the functionality provided by this library. Each Zone is
named, represents a geographic area, and provides a mapping between UTC and the local
time, in both directions. This mapping from local time to UTC is in general not one to
one. The mapping, and even the specific rule, depends upon the input
time_point, which can represent either UTC or local time.
The detailed API of the Zone class depends upon a small amount of
infrastructure which is introduced first.
using second_point = std::chrono::time_point<std::chrono::system_clock,
std::chrono::seconds>;
second_point is a std::chrono::time_point based on
system_clock but with the precision of seconds. This library
will interoperate with system_clock::time_points of any precision.
However the data in the database is largely based on second_point, and
some of the data which is presented, such as that in the Info class, uses
this type alias as a convenience, and to reduce verbosity. second_point
will implicitly convert to system_clock::time_point. And coarser
time_points such as the day_point from the
date library will implicitly convert to
second_point.
struct Info
{
second_point begin;
second_point end;
std::chrono::seconds offset;
std::chrono::minutes save;
std::string abbrev;
};
The Info struct is the return type of the get_info member
function of the Zone class. It contains very detailed information about the
Zone at the time_point (UTC or local) input into this member
function. Info contains no pointers or references into the database.
Therefore clients do not need to be concerned about holding on to Infos
during a call to reload_tzdb(). Though a call to reload_tzdb()
could potentially make the data in an outstanding Info obsolete. See
Zone::get_info for more details.
enum class tz {utc, local};
enum class choose {earliest, latest};
These enums are used as input to some of the Zone member
functions. tz::utc indicates that a time_point represents a
time in the UTC time zone. tz::local indicates that a
time_point represents a time in the Zone's local time zone.
The choose enum allows a client to specify how a mapping from local to UTC
should behave when the mapping is not one to one. Alternatively one can not specify
a policy in the mapping, and if the mapping is not unique, an exception will be thrown.
class nonexistent_local_time
: public std::runtime_error
{
public:
const char* what() const override;
};
class ambiguous_local_time
: public std::runtime_error
{
public:
const char* what() const override;
};
These are the exception classes thrown by the local to UTC mapping. In addition to their
type indicating the nature of the exceptional circumstance, they also sport a
what() member function that will contain a very detailed explanation
including specific times for the specific time_points involved in the
attempted mapping.
If in a call to Zone::to_sys the local time_point falls into a
"gap" for which no local time exists, a nonexistent_local_time exception is
thrown.
If in a call to Zone::to_sys the local time_point has an
ambiguous mapping to UTC, a ambiguous_local_time exception is thrown.
Either exceptional situation can be circumvented with the use of
choose::earliest or choose::latest in the call to
to_sys.
class Zone
{
public:
const std::string& name() const;
template <class Rep, class Period>
std::pair
<
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>,
std::string
>
to_local(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const;
template <class Rep, class Period>
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>
to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const;
template <class Rep, class Period>
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>
to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp,
choose z) const;
template <class Rep, class Period>
Info
get_info(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp,
tz timezone) const;
};
const Zone* locate_zone(const std::string& tz_name);
const Zone* current_zone();
bool operator==(const Zone& x, const Zone& y);
bool operator!=(const Zone& x, const Zone& y);
bool operator< (const Zone& x, const Zone& y);
bool operator> (const Zone& x, const Zone& y);
bool operator<=(const Zone& x, const Zone& y);
bool operator>=(const Zone& x, const Zone& y);
std::ostream& operator<<(std::ostream& os, const Zone& z);
The entire public API of the Zone is const. Once the database
is initialized (or reloaded), Zones are set in concrete.
The current time zone associated with your computer can be retrieved with the namespace
scope function current_zone(). For example:
std::cout << current_zone()->name() << '\n';
For me the above currently outputs America/New_York.
const Zone* locate_zone(const std::string& tz_name);
locate_zone returns a pointer to a Zone in the database
associated with tz_name. If it can't find a Zone named
tz_name, the implementation will search for a Link named
tz_name, and then return the Zone associated with the
Link's target(). If tz_name can not be found in the
database, a std::runtime_error is thrown.
Example:
try
{
cout << locate_zone("Europe/London")->name() << '\n'; // A Zone
cout << locate_zone("Europe/Jersey")->name() << '\n'; // A Link to a Zone
cout << locate_zone("Europe/New_Jersey")->name() << '\n'; // Doesn't exist
}
catch (const exception& e)
{
cout << e.what() << '\n';
}
Which outputs:
Europe/London Europe/London Europe/New_Jersey not found in timezone database
Note that locate_zone never returns nullptr. Also note that
the first call to locate_zone may implicitly initialize the database.
template <class Rep, class Period>
std::pair
<
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>,
std::string
>
to_local(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const;
to_local maps a system_clock-associated time_point
from UTC to local time, returning both the mapped time_point and an
abbreviation for the local time zone. This member function accepts any precision
time_point, but returns a time_point with a precision of
seconds or finer. This is done because it is possible that some of the
mappings returned by the database need the precision of a second.
There are only two ways this function can fail:
Out of memory error. Not bloody likely. The only memory that possibly could be
allocated is for the abbreviation stored in a std::string and all known
implementations will fit all known abbreviations into their short string buffer.
If you curtailed history during installation, a runtime_error will be thrown
if tp refers to a time_point outside of the range
min_year/jan/1 00:00:00 to max_year/dec/31 23:59:59. This can
not happen with the default settings of min_year and max_year.
Example:
auto local = current_zone()->to_local(system_clock::now()); cout << local.first << ' ' << local.second << '\n';
Which just output for me:
2015-07-12 16:57:14.430467 EDT
Not quite 5pm in the US Eastern timezone during daylight saving time.
And for a historical example:
auto distant_past = locate_zone("America/New_York")->to_local(day_point(feb/9/1942) + 7h);
cout << distant_past.first << ' ' << distant_past.second << '\n';
Which outputs:
1942-02-09 03:00:00 EWT
The US shifted to "War Time."
If you want to go the other direction (from local time to UTC) use:
template <class Rep, class Period>
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<std::chrono::duration<Rep, Period>,
std::chrono::seconds>::type>
to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const;
For example:
auto distant_past = locate_zone("America/New_York")->to_sys(day_point(feb/9/1942) + 3h);
cout << distant_past << ' ' << " UTC\n";
Which outputs:
1942-02-09 07:00:00 UTC
This function will throw an exception of type nonexistent_local_time if the
local time does not exist. This can happen when the local clock is discontinuously set
forward, such as when moving from standard time to daylight savings time.
For example:
try
{
auto distant_past = locate_zone("America/New_York")->to_sys(day_point(feb/9/1942) + 3h - 1ms);
cout << distant_past << ' ' << " UTC\n";
}
catch (const exception& e)
{
cout << e.what() << '\n';
}
Which outputs:
1942-02-09 02:59:59.999 is in a gap between 1942-02-09 02:00:00 EST and 1942-02-09 03:00:00 EWT which are both equivalent to 1942-02-09 07:00:00 UTC
And sometimes a local time can be ambiguous, mapping to more than one UTC time:
try
{
auto distant_past = locate_zone("America/New_York")->to_sys(day_point(sep/30/1945) + 2h - 1ns);
cout << distant_past << " UTC\n";
}
catch (const exception& e)
{
cout << e.what() << '\n';
}
1945-09-30 01:59:59.999999999 is ambiguous. It could be 1945-09-30 01:59:59.999999999 EPT == 1945-09-30 05:59:59.999999999 UTC or 1945-09-30 01:59:59.999999999 EST == 1945-09-30 06:59:59.999999999 UTC
If you would rather not deal with these rare exceptions, you can choose ahead of time to select the earliest time or latest time when a local time falls into a gap:
auto z = locate_zone("America/New_York");
auto distant_past = z->to_sys(day_point(sep/30/1945) + 2h - 1ns, choose::earliest);
cout << distant_past << " UTC\n";
distant_past = z->to_sys(day_point(sep/30/1945) + 2h - 1ns, choose::latest);
cout << distant_past << " UTC\n";
Which outputs:
1945-09-30 05:59:59.999999999 UTC 1945-09-30 06:59:59.999999999 UTC
When using this form of to_sys and the local time is non-existent, both
choices will map to the single UTC time on either side of the gap:
auto z = locate_zone("America/New_York");
auto distant_past = z->to_sys(day_point(feb/9/1942) + 3h - 1ms, choose::earliest);
cout << distant_past << " UTC\n";
distant_past = z->to_sys(day_point(feb/9/1942) + 3h - 1ms, choose::latest);
cout << distant_past << " UTC\n";
Which outputs:
1942-02-09 07:00:00.000 UTC 1942-02-09 07:00:00.000 UTC
So far I've shown how given a Zone and a system_clock::time_point
of arbitrary precision, you can use to_local to map UTC to local time, and
to_sys to map local time to UTC, with your choice of either detecting any
errors, or choosing how to resolve errors. But what if that is not enough? You may be
thinking: Do I have to call these mapping functions every second? How often does the
offset change?
This library offers a partial solution to this dilemma. If the location you are concerned
about doesn't change, and if the database isn't reloaded, then get_info can
tell you how far into the past, and far into the future a given offset and abbreviation
are guaranteed to stay valid:
template <class Rep, class Period>
Info
get_info(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp,
tz timezone) const;
Input a time_point tp, and indicate whether tp represents a UTC
time_point (tz::utc) or a local time_point
(tz::local), and a struct Info for that time_point
is returned:
auto info = locate_zone("America/New_York")->get_info(system_clock::now(), tz::utc);
Upon return info will contain the following information:
info.offset has type std::chrono::seconds and indicates the
current offset from UTC. A positive offset indicates that local time is ahead of UTC and
a negative offset indicates that local time is behind UTC.
info.abbrev has type std::string and indicates the
current abbreviation for the local time zone.
info.begin has type second_point and indicates the first
instant guaranteed to have this same offset and abbrev. The
time_point info.begin is implicitly in the UTC time zone. Note that it is
possible that the instant prior to begin may or may not have a
different offset or abbrev.
info.end has type second_point and indicates the last instant
before which it is guaranteed to have this same offset and
abbrev. The time_point info.end is implicitly in the UTC time
zone. The offset and abbrev associated with
info.end and beyond may or may not be different.
info.save has type std::chrono::minutes and indicates the
amount of time that daylight savings time has moved the current offset. This can be
used to detect whether or not daylight savings is in effect (no if the value is 0min).
Note that save is already incorporated into the value of offset,
so you don't have to look at this field to get the current offset. This field exists
just in the spirit of more information is better.
The Info also has a streaming operator which is mainly useful for debugging
purposes. Here is sample code and output:
cout << current_zone()->get_info(system_clock::now(), tz::utc); 2015-03-08 07:00:00 2015-11-01 06:00:00 -04:00:00 01:00 EDT
This is considered to be a low-level function, and as such there is no error detection
if you input a local time that either does not exist, or is ambiguous. Enough information
is returned for you to compute those conditions. Indeed, this is exactly how error
detection is computed in to_sys: by calling get_info and
analyzing how the input time relates to begin and end.
Additionally the Zone is equality and less-than comparable (using the
name()). And you can stream the Zone out to a stream, though
the output may not be crystal clear. The streaming output is mainly used as an aid in
debugging this library, not your code.
There's nothing like a real-world example to help demonstrate things. Imagine a plane flying from New York, New York, USA to Tehran, Iran. To make it more realistic, lets say this flight occurred before the hostage crisis, right at the end of 1978. Flight time for a non-stop one way trip is 14 hours and 44 minutes.
Given that the departure is one minute past noon on Dec. 30, 1978, local time, what is the local arrival time?
#include "tz.h"
#include <iostream>
int
main()
{
using namespace std::chrono;
using namespace date;
auto nyc_tz = locate_zone("America/New_York");
auto teh_tz = locate_zone("Asia/Tehran");
auto nyc_departure_sys = nyc_tz->to_sys(day_point(dec/30/1978) + 12h + 1min);
auto nyc_departure = nyc_tz->to_local(nyc_departure_sys);
auto flight_length = 14h + 44min;
auto teh_arrival_sys = nyc_departure_sys + flight_length;
auto teh_arrival = teh_tz->to_local(teh_arrival_sys);
std::cout << "departure NYC time: " << nyc_departure.first << ' '
<< nyc_departure.second << '\n';
std::cout << "flight time is " << make_time(flight_length) << '\n';
std::cout << "arrival Tehran time: " << teh_arrival.first << ' '
<< teh_arrival.second << '\n';
}
There are several points to be made about the above code:
A stylistic guide is to use "sys" for system_clock time_points.
This helps distinguish system times (UTC) from local times.
No time arithmetic is done using local time_points. All time arithmetic is
done in the UTC time zone. Time arithmetic in terms of local time_points is
error prone. Note though that this computation (using
system_clock::time_point) is ignorant of leap seconds. If you must, see how
to take leap seconds into account with utc_clock.
There is no using namespace std because "dec" is ambiguous if
both date and std are brought into scope. In date
"dec" means December. In std "dec" means:
ios_base& dec(ios_base& str);
Thank goodness for namespaces!
The output of the above program is:
departure NYC time: 1978-12-30 12:01:00 EST flight time is 14:44 arrival Tehran time: 1978-12-31 11:45:00 IRST
And this program is exactly correct. But what happens with the same flight on the following day?
auto nyc_departure_sys = nyc_tz->to_sys(day_point(dec/31/1978) + 12h + 1min); departure NYC time: 1978-12-31 12:01:00 EST flight time is 14:44 arrival Tehran time: 1979-01-01 11:15:00 IRST
Now we have the flight arriving 30min earlier. This is because the time zone "Asia/Tehran" undergoes an offset change while the plane is in the air, shifting its UTC offset to 30min earlier. Is this the final word on this example? Almost. If accuracy down to the second is required (it is not for a flight arrival), then additional effort needs to be expended. See Flight Example with leap seconds.
One of the first questions everyone asks when a new date-time library comes out is:
Does it handle leap seconds?
The answer here is yes, this library can handle leap seconds. But be careful what you ask for. Correctly handling leap seconds is error prone. Therefore this library handles leap seconds in a completely different type-safe way, which can't be accidentally mixed with everything else presented so far. The motivation for this separation is born from several issues:
system_clock (Unix
time) sort of handles leaps seconds in that "now" in
Unix
time always corresponds to "now" in UTC (UTC being leap second aware). It is
just that the difference between two
Unix time time_points
may produce a number of std::chrono::seconds which does not reflect the
exact number of physical seconds which has actually transpired.
Unless you are using a very special computer, directly connected to an atomic clock, your computer is likely using Unix time. If you want to correctly interpret things such as time stamps, and you want to correctly handle leap seconds, it is critical to know if those time stamps (the input data) were generated by software that correctly handled leap seconds. Odds are very good that they were generated by software following Unix time, or Network Time Protocol which for our purposes here, treats leaps seconds in essentially the same way Unix time does (as a clock correction).
Handling leap seconds is not free. Don't try to just so you can be "more exact." Do it when your requirements actually demand it, and you have the resources to test that you are indeed correctly handling leap seconds. If the person telling you to correctly handle leap seconds has pointy hair, double down on your testing, and have your atomic clock ready. The extra expense is not so much memory or performance (those penalties exist but are relatively small), but in the problem of believing you've achieved more accuracy when you actually haven't.
utc_clock is a std::chrono-conforming clock with the same
duration as your system_clock, and a now()
function that returns the actual number of physical seconds since 1970-01-01 00:00:00 UTC
(counting leap seconds):
class utc_clock
{
public:
using duration = std::chrono::system_clock::duration;
using rep = duration::rep;
using period = duration::period;
using time_point = std::chrono::time_point<utc_clock>;
static constexpr bool is_steady = true;
static time_point now() noexcept;
template <class Duration>
static
std::chrono::time_point<utc_clock,
typename std::common_type<Duration, std::chrono::seconds>::type>
sys_to_utc(std::chrono::time_point<std::chrono::system_clock, Duration> t);
template <class Duration>
static
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<Duration, std::chrono::seconds>::type>
utc_to_sys(std::chrono::time_point<utc_clock, Duration> t);
};
Additionally utc_clock has static member functions for converting between
utc_clock-based time_points to and from
system_clock-based time_points of any precision. But it is
important to remember that utc_clock isn't connected to a super accurate
atomic clock. All it does is look its time_point up in the database to
see how many leap seconds have passed since 1972, and adds or subtracts that number of
seconds to do the conversion. The utc_clock::now() function simply calls
system_clock::now() and adds the current total of leaps seconds (currently
26) to the result. This is useful behavior but it is important to understand that
utc_clock is not a highly accurate scientific instrument. It is precisely
as accurate as your existing std::chrono::system_clock.
In the preceding section a flight from New York City to Tehran was offered, demonstrating
how local political changes in the rules governing UTC offsets can affect time
computations. As it turns out, while that flight departing on dec/31/1978
was in the air, we also underwent a leap second addition. How does that impact the
computation, and how can this library be used to account for that (should it actually be
important)?
#include "tz.h"
#include <iostream>
int
main()
{
using namespace std::chrono;
using namespace date;
auto nyc_tz = locate_zone("America/New_York");
auto teh_tz = locate_zone("Asia/Tehran");
auto nyc_departure_sys = nyc_tz->to_sys(day_point(dec/31/1978) + 12h + 1min);
auto nyc_departure = nyc_tz->to_local(nyc_departure_sys);
auto nyc_departure_utc = utc_clock::sys_to_utc(nyc_departure_sys);
auto flight_length = 14h + 44min;
auto teh_arrival_utc = nyc_departure_utc + flight_length;
auto teh_arrival_sys = utc_clock::utc_to_sys(teh_arrival_utc);
auto teh_arrival = teh_tz->to_local(teh_arrival_sys);
std::cout << "departure NYC time: " << nyc_departure.first << ' '
<< nyc_departure.second << '\n';
std::cout << "flight time is " << make_time(flight_length) << '\n';
std::cout << "arrival Tehran time: " << teh_arrival.first << ' '
<< teh_arrival.second << '\n';
}
departure NYC time: 1978-12-31 12:01:00 EST
flight time is 14:44
arrival Tehran time: 1979-01-01 11:14:59 IRST
As can be seen, we now report an arrival time 1s before the arrival time we computed
without taking leap seconds into account. The key to working with leap seconds is to make
sure that all your time arithmetic takes place using utc_clock-based
time_points, instead of system_clock-based
time_points. Just convert to system_clock when you're ready to
break the date and time up into field-based structures, or are ready to further convert it
into a local time_point. In this example, the only time arithmetic is:
auto teh_arrival_utc = nyc_departure_utc + flight_length;
The reset of the code is simply about converting from local, to system_clock
to utc_clock and back.
Digression: Doing computations with leap seconds is cool. But perhaps the true power of this library is revealed in the ease with which I created this example. I sat back and said to myself:
I want to find a time and location where a timezone offset changed within 12 hours of a leap second insertion. And then build my flight time example around that event.
Subsequently I wrote the following code to search the entire planet, and the last 45 years, to find these rare chronological events:
const auto& db = get_tzdb(); for (auto const& leap : db.leaps) { for (auto const& zone : db.zones) { auto info = zone.get_info(leap.date(), tz::utc); if (leap.date() - info.begin <= 12h) { auto prev = zone.get_info(info.begin - 1s, tz::utc); if (prev.offset != info.offset) std::cout << zone.name() << " " << info.begin << " : " << leap << ' ' << make_time(info.offset-prev.offset) << '\n'; } if (info.end - leap.date() <= 12h) { auto next = zone.get_info(info.end, tz::utc); if (next.offset != info.offset) std::cout << zone.name() << " " << info.end << " : " << leap << ' ' << make_time(next.offset - info.offset) << '\n'; } } }The flight time example wasn't really about Iran, the US, and politics after all. It was about finding this needle in a haystack of time and space, which turned out to be relatively easy and incredibly efficient.
You too can analyze the IANA Time Zone Database in creative and interesting ways no one else has thought of. There is a lot of history here.
All of the types in this library, as well as in
date.h are streamable when you need quick and
simple output. However in addition to this simplistic streaming there is more
sophisticated formatting built on top of the C++11 time_put<char>
facet. time_put<char> itself is built on C's strftime
function. But time_put<char> is sensitive to C++ locales.
The basic way to use formatting is to call the format function like this:
cout << format("%A %F %T", floor<seconds>(system_clock::now())) << '\n';
Which just output for me:
Sunday 2016-04-03 22:02:19
Note the cast to seconds precision in the call. This is how you control
the precision of the seconds output (if any). The modifiers %S
and %T will output seconds to whatever the precision is of the
time_point. For example:
cout << format("%A %F %T", floor<milliseconds>(system_clock::now())) << '\n';
would instead output:
Sunday 2016-04-03 22:02:19.656
Note that there is an implicit time zone being used here: UTC. The %z and
%Z modifiers can be used to show this:
cout << format("%A %F %T %z %Z", floor<milliseconds>(system_clock::now())) << '\n';
would instead output:
Sunday 2016-04-03 22:02:19.656 +0000 UTC
A Zone can also be passed in and then the %z and
%Z modifiers will reflect that passed-in zone. It is important to
remember however that format never shifts the
time_point for you. Instead you pass in a Zone that you know
to be associated with your time_point. For example:
auto zone = locate_zone("Europe/Berlin");
auto local = zone->to_local(floor<milliseconds>(system_clock::now())).first;
cout << format("%A %F %T %z %Z", local, zone) << '\n';
Monday 2016-04-04 00:02:19.656 +0200 CEST
The only thing format ever does with a Zone is extract
the offset and/or the abbreviation for use with the %z and %Z
modifiers.
You can also pass in a locale to format:
cout << format(locale("de_DE"), "%A %F %T %z %Z", local, zone) << '\n';
Montag 2016-04-04 00:02:19,656 +0200 CEST
The set of named locales that your OS supports is defined by your OS, not this library.
Instead of a time_point you can also pass in anything that is implicitly
convertible to day_point:
cout << format(locale("de_DE"), "%A %B %e, %Y", 2016_y/jul/mon[1]) << '\n';
Montag Juli 4, 2016
In summary, use format by passing in a format string and a
time_point, or something implicitly convertible to a day_point.
You can optionally pass in a locale as the first parameter, and a
Zone as the last parameter. format will never alter the value
of your time_point. The precision of the time_point controls
the precision of seconds with the %S and %T modifiers. If you
pass in a Zone, this will only impact the output of %z and
%Z (which default to +0000 and UTC respectively).
The output of format is a std::string.
Since all parts of all date-types in this library can be constructed with integral types, you can parse any format you wish as integrals, and create dates from any format you wish that way.
However this section introduces a parse function which is built on top
of the C++11 time_get facet which can also be used:
template <class Duration>
void
parse(std::istream& is, const std::string& format,
std::chrono::time_point<std::chrono::system_clock, Duration>& tp);
You can input any istream, and a format string much like that used for
format and strftime, and a time_point of any
precision, and this function will attempt to extract the time_point from
the istream by using the format string. If not successful,
the time_point will not be altered.
Example use:
istringstream is("Montag 2016-04-04 00:02:19,656 +0200");
is.imbue(locale("de_DE"));
system_clock::time_point tp;
parse(is, "%A %F %T %z", tp);
cout << tp << '\n';
Which outputs:
2016-04-03 22:02:19.656000
Note that the locale associated with the istream is respected.
If the format string contains a %z which matches the input stream, this is
used to convert the value to UTC. If there is no %z, then no conversion
happens (you can assume whatever timezone you want). Note that fractional seconds are
accepted as long as one uses %T or %S, and the precision of the
time_point is fine enough to accept fractional seconds.
%Z is not accepted as the mapping from a timezone abbreviation to UTC is
in general, ambiguous. If you have a %Z in the format string, this will
result in is.fail() returning true after the call to
parse.
However, if you absolutely must parse a timestamp with a timezone abbreviation in it,
an extra parse overload is provided:
template <class Duration>
void
parse(std::istream& is, const std::string& format,
std::chrono::time_point<std::chrono::system_clock, Duration>& tp,
std::string& abbrev);
Now if %Z matches a word in is and if the rest of
is correctly parses according to format, then
abbrev will be assigned the word which matched %Z. This
will not have any impact on the value of tp (no timezone offset
applied). However perhaps there is enough a-priori knowledge in your application to
make use of the value of abbrev to correctly interpret the meaning of
the timestamp and the resulting value of tp.
As an example of how this option can be both useful and dangerous, consider an example where we need to parse the timestamp "Thu Apr 07 11:45:28 AEST 2016", and we want to discover what the corresponding time is in UTC, and what timezone this timestamp represents.
The following program parses this, and then searches the entire timezone database looking for timezones which have "AEST" as an abbreviation at a local time of Apr 07 11:45:28 2016. The program finds the first one, notes its UTC offset, and then searches for more. If it finds more, and the UTC offset is the same, it simply outputs the name of each additional timezone found. If the additional timezones have a different UTC offset, that is noted too by outputting the UTC timestamp associated with the additional timezone.
#include "tz.h"
#include <string>
#include <iostream>
#include <sstream>
#include <cassert>
int
main()
{
using namespace std::chrono;
using namespace date;
auto& db = get_tzdb();
std::istringstream in("Thu Apr 07 11:45:28 AEST 2016");
time_point<system_clock, seconds> tp_local;
std::string abbrev;
parse(in, "%a %b %d %T %Z %Y", tp_local, abbrev);
assert(!in.fail());
auto i = std::find_if(db.zones.begin(), db.zones.end(),
[&tp_local, &abbrev](auto const& z)
{
return z.get_info(tp_local, tz::local).abbrev == abbrev;
});
if (i != db.zones.end())
{
auto tp_utc = i->to_sys(tp_local);
std::cout << tp_utc << " UTC " << i->name() << '\n';
for (++i; i != db.zones.end(); ++i)
{
if (i->get_info(tp_local, tz::local).abbrev != abbrev)
continue;
auto tp = i->to_sys(tp_local);
if (tp != tp_utc)
std::cout << tp << " UTC ";
std::cout << i->name() << '\n';
}
}
}
This program outputs:
2016-04-07 01:45:28 UTC Australia/Brisbane Australia/Currie Australia/Hobart Australia/Lindeman Australia/Melbourne Australia/Sydney
This indicates that "Thu Apr 07 11:45:28 AEST 2016" unambiguously refers to 2016-04-07 01:45:28 UTC (a UTC offset of +1000). However which IANA timezone is referred to is ambiguous. This means that past or future timepoints using any of these timezones may or may not have the same UTC offsets (or abbreviations) among this set of timezones.
And this is a good case. Consider just altering the abbreviation in the above example from AEST to BST. Now the output is:
2016-04-07 10:45:28 UTC Europe/London 2016-04-07 00:45:28 UTC Pacific/Bougainville
Meaning: Not only do we not know what timezone this refers to, it could mean one of two different UTC timepoints!
So in summary, it is dangerous to parse timezone abbreviations. You should avoid it if at all possible. However, if you are forced to, this library has the power to find out every thing that is knowable about that timestamp.
The implementation of the database is as a singleton. An application can have only a single database. However the initial construction of that database is thread safe, and subsequent access is always const (unless you need to update to a new version of the database at run time). The initial construction thread safety is implemented with C++11 function local statics. If your compiler does not implement threadsafe function local statics, then you will need to arrange for your own thread safety during the initialization stage.
If you need to update the database at runtime via the reload_tzdb function,
you will need to provide your own thread safety if you are in a multithreaded application.
This could be done (for example) by ensuring that all of your calls into the database
occur with the permission of a C++14
std::shared_lock<std::shared_timed_mutex>, except for the calls to
reload_tzdb, which must be done with a
std::unique_lock<std::shared_timed_mutex>. Such facilities are not
provided by default with this library so that you don't pay for them if you don't need
them.
In case you do need to protect calls into the database with a mutex, here is a comprehensive list of the public API that accesses the database:
// Read/write access
const TZ_DB& reload_tzdb();
// Read-only access
const TZ_DB& get_tzdb();
// Zone functions
const Zone* locate_zone(const std::string& tz_name);
const Zone* current_zone();
Info Zone::get_info(std::chrono::system_clock::time_point tp, tz timezone) const;
template <class Rep, class Period>
auto
Zone::to_local(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const
-> std::pair<decltype(tp + get_info(tp, tz::utc).offset), std::string>;
template <class Rep, class Period>
auto
Zone::to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp) const
-> decltype(tp - get_info(tp, tz::local).offset);
template <class Rep, class Period>
auto
Zone::to_sys(std::chrono::time_point<std::chrono::system_clock,
std::chrono::duration<Rep, Period>> tp, choose z) const
-> decltype(tp - get_info(tp, tz::local).offset);
// leap second functions
utc_clock::time_point utc_clock::now() noexcept;
template <class Duration>
std::chrono::time_point<utc_clock,
typename std::common_type<Duration, std::chrono::seconds>::type>
utc_clock::sys_to_utc(std::chrono::time_point<std::chrono::system_clock, Duration> t);
template <class Duration>
std::chrono::time_point<std::chrono::system_clock,
typename std::common_type<Duration, std::chrono::seconds>::type>
utc_clock::utc_to_sys(std::chrono::time_point<utc_clock, Duration> t);
Those functions that explicitly return const references or const pointers (the first five) continue to have read access into the database after the call until the client drops that reference or pointer.
Emphasis: If you don't use the reload_tzdb function, then all access
is read-only and there is no need for synchronization with an external mutex.
There are only three files in the timezone library:
tz.h,
tz_private.h and
tz.cpp.
These are sources located at the github repository
https://github.com/HowardHinnant/date.
The source
tz.cpp
contains the following string near the top:
static std::string install{"~/Downloads/tzdata"}; // "c:\\tzdata" on Windows
You should set this such that install points to the directory
where your library or application can find the downloaded and uncompressed
IANA Time Zone Database.
There are three configuration macros that can be defined on the command line during compilation, or you can ignore them and they will take on default values.
HAS_REMOTE_APIDefaults to 1 on Linux and OS X, and to 0 on Windows AUTO_DOWNLOADDefaults to HAS_REMOTE_APILAZY_INITDefaults to 1
If HAS_REMOTE_API is 1 then the remote API exists,
else it doesn't:
std::string remote_version(); bool remote_download(const std::string& version); bool remote_install(const std::string& version);
The remote API requires linking against libcurl
(https://curl.haxx.se/libcurl).
On OS X and Linux this is done with -lcurl.
libcurl comes pre-installed on OS X and Linux, but not on Windows.
However one can download it for Windows.
If AUTO_DOWNLOAD is 1 then first access to the timezone database will install
it if it hasn't been installed, and if it has, will use the remote API to install the
latest version if not already installed.
If AUTO_DOWNLOAD is 1 then reload_tzdb() will automatically
check remote_version() against the local version, and if unchanged, will do
nothing. If remote_version() can't access the IANA website,
reload_tzdb() will re-initialize the database anyway (perhaps you changed it
manually because the net is down). If the remote_version() is confirmed to
have changed (and AUTO_DOWNLOAD is on), then reload_tzdb() will
download and install the latest version.
If AUTO_DOWNLOAD is off, reload_tzdb() re-initializes the
database without using the remote API (presumably because it has been manually updated).
If HAS_REMOTE_API is off and AUTO_DOWNLOAD is on, there will be
a compile-time error as AUTO_DOWNLOAD needs the remote API.
If LAZY_INIT is on, the Zones are not fully compiled upon first
access to the database. As each Zone is accessed individaully by the
programmer (when they are used), they are fully compiled at that point. However, this
further Zone compilation does not involve any access to the local copy of the
tz database files.
If LAZY_INIT is off, every Zone is fully compiled upon first
access to the database.
LAZY_INIT speeds up the initialization of the database, but slows down the
first use of any individual Zone. If you are only using a few
Zones then LAZY_INIT is a clear win. If you are immediately
using all of the Zones (say for some database analysis) then
LAZY_INIT is not a win.
If LAZY_INIT is off, and you are on multi-core hardware, and your application
has other unrelated initialization it has to take care of, spinning off timezone
initialization into a detached thread can be an attractive option:
int
main()
{
std::thread(date::get_tzdb).detach();
// other initialization ...
}
By the time your application actually needs to use the timezone database, it is likely to be fully initialized and ready to go. And if it is not, C++11 threadsafe function local statics ensure there is no race condition on the initialization.
If you would like to trade off functionality for size, you can reduce the size of the database in two ways:
You can limit geography by removing one or more of the files in this list:
const std::vector<const std::string> files =
{
"africa", "antarctica", "asia", "australasia", "backward", "etcetera", "europe",
"pacificnew", "northamerica", "southamerica", "systemv", "leapseconds"
};
You can limit history by setting min_year to something more recent such as:
CONSTDATA auto min_year = 2015_y;
When you do so, if you ask to convert a date prior to min_year, an exception
will be thrown.
The entire database consumes about 859Kb.
Compile
tz.cpp
in with the rest of your library or application.
If AUTO_DOWNLOAD is not enabled, you are responsible for keeping your
IANA Time Zone Database up to date. New
versions of it are released several times a year. This library is not bundled with a
specific version of the database already installed, nor is any specific version of the
database blessed.
There is no preprocessing of the IANA Time Zone Database required. This library efficiently initializes itself directly from the files of the IANA Time Zone Database. If your application is long running, and you anticipate the need to update the IANA Time Zone Database to a new version without recompiling, or even restarting your application, there is API to accomplish that.
If you constrain geography during installation, this can significantly reduce the time it takes to initialize the database. However constraining history has little (if any) impact on the performance of the initialization.
A database parser is nothing without its database. I would like to thank the founding contributor of the IANA Time Zone Database Arthur David Olson. I would also like to thank the entire group of people who continually maintain it, and especially the IESG-designated TZ Coordinator, Paul Eggert. Without the work of these people, this software would have no data to parse.