Local Time

In this post we will see how to solve the task from the previous post, but display the time point in local time instead of system time. The solution is simple and takes only two additional lines (you can scroll down towards the end), but I want to take this opportunity to offer some reflections about the concepts of time zones and daylight saving time.

The current method of representing time of day — measuring hours and minutes since midnight — worked good enough until people weren’t able to travel fast enough to different parts of the globe to observe that at the same point in time you can have noon in one place and midnight in another. If you associate daytime name such as 6:00 a.m. with the sunrise, you quickly observe that either sun rises at different times of day at different longitudes, or times of day themselves are different at different longitudes. This is just one problem, but there are others. Even at the same longitude sun rises at different times of day at different times of year. Further, even at the same longitude and at the same time of year sun rises at different times of day at different latitudes.

There are two ways to approach these issues. One is to scrap the current notion of times of day or their connection with night and day, and devise a system that takes into account all (or some) of the above problems. Another one is to apply patches on top of the existing model. The concept of time zones took the latter path. The majority of people agreed to divide the surface of the earth into regions called time zones; each time zone can have midnight at different time, so that when one timezone calls a given point in time “midnight”, another one can call it “two hours past midnight”. The division of the globe into time zones is arbitrary and political. It tries to take into account longitudes as well as country borders. It is illustrated in this map. One number that characterizes a timezone is its offset from GMT time. Usually it is a multiple of an hour, but sometimes it is quarters of an hour, and sometimes the offset is more than twelve hours, which may be surprising.

Now, on top of this, each country or state can introduce their daylight saving rules. Whether this procedure solves any of the above-listed problems, or if it solves any problem at all is debatable. For sure, daylight saving procedure causes a lot of problems and surprises. In countries with one-hour daylight saving time shift you have one day in a year that has 25 hours and one day that has 23 hours. If people from two countries with different daylight saving rules (such as US and UK) need to collaborate via meetings, you get the situation that if a weekly meeting is held regularly at the same time of day throughout the year in one country, it means its time of day changes throughout the year in the other country). You can get into even more counterintuitive situations if you fail to acknowledge that the time of day 1:30 a.m. occurs twice on certain days. For instance, a twin brother who was born earlier may be younger than his twin brother who was born later, the situation that really happened and has been described as a “riddle of the day” by Cape Cod Healthcare here.

Worse still, observing daylight saving time is a political decision that varies from country to country and from state to state (for instance Arizona refuses to acknowledge daylight saving time imposed by the Uniform Time Act that applies throughout the US), and changes over time. Countries adapt and abandon daylight saving times, or change their rules as the US did: since 2007 US observe a longer daylight saving time than before, which, as you may expect, caused a lot of stir also in computer systems. This means that you cannot say with confidence what the result would be of converting time 10:30, June 12, next year UTC into local time in country X, because you do not know what the governing body of country X will have decided by that time. And of course, there are more countries and states with different daylight saving rules in the world than time zones, which gives us an even finer-grained division into regions, which you can see here.

How can a computer system manage all these time zones and daylight saving times which change rapidly over time? You need something like a database or a table that is maintained and kept up to date with the political decisions and historical data. On your system, you will either have an inaccurate — simplified — version of time zones and DST regions, or the system will have to maintain the full information somewhere and try to keep it up to date, which implies an Internet connection. One such popular time zone resource is the IANA Time Zone Database. Does your system actually use it? From the system designer’s perspective this is a technical trade-off between simplicity and efficiency on the one hand, and the accuracy of the vary rare queries about the past or the future. If you really need to know that, because you are using local times for anything other than displaying the current local time, you have to consult your system’s documentation.

What does the <chrono> offer in C++20 for time zones? You get two interfaces: one for managing your time zone database. These are functions:

  • std::chrono::get_tzdb_list() — obtain the list of time zone databases maintained by the system.
  • std::chrono::get_tzdb() — access the primary time zone database maintained by the system.
  • std::chrono::get_tzdb().version — version of the primary time zone database.
  • std::chrono::reload_tzdb() — if there is a newer version of the remote time zone database, make it the system’s primary database.

This interface is compatible with that of IANA Time Zone Database, and the intention of the designers of the Standard is that implementations should use this database. But this is not strictly required. Technically implementations could decide what data they want to store in the database. For some more information on this topic, see this Stack Overflow question.

The other interface is for obtaining the definition of a given time zone:

  • std::chrono::locate_zone(name) — get time zone definition by name; the naming convention is database-specific.
  • std::chrono::current_zone() — get the time zone definition that represent the local time on the computer.

So much for the introduction. The first conclusion that one can draw from the above information is that if you are using local time for anything else than displaying current time, it may not be a good idea. If you want to know what March 21, 2005, 11:00 UTC was in New York local time, the answer will vary with the accuracy of your time zone database. If you want to know any future local time, the result will always be uncertain, because we do not know the future time-related legislation in different countries. If possible, it is recommended to use a time-zone-independent representation, such as GMT or UTC.

But our task was to display the current time, so we are safe. Let’s recall the original solution, using system time:

// C++20
#include <chrono>
#include <format>
#include <iostream>

int main()
{
  namespace krn = std::chrono;
  using TwentyMins = krn::duration<int, std::ratio<20 * 60>>;

  krn::time_point p1 = krn::system_clock::now();
  krn::time_point p2 = krn::floor<TwentyMins>(p1);

  std::cout << std::format("{:%Y-%m-%d %H:%M}\n", p2);
}

It is not obvious that this should work at all. The format string ("{:%Y-%m-%d %H:%M}\n") tells the system to display the month, day, etc., but how does it know what month/day it is in our time zone, or any time zone, given that we are only storing seconds (or twenty-minute ticks) from the epoch?

Here is how this works. time_points in this example are all in the scope of system_clock: they have system_clock as their template argument. This means that their epoch is 00:00:00 Jan 1, 1970 GMT (Unix Time). The “now” (the present moment) is independent of time zones: if the Earth suddenly explodes, it will be observable in all time zones at the very same moment. Only the epoch is time-zone-dependent, but we know the epoch’s time zone. Thus, the count of seconds from “now” to an epoch in a given time zone is unambiguous. Now, if we want to display the present time in a different time zone, the “now” stays the same, but we will have to add (or subtract) minutes, because we will be measuring time from an epoch from a different time zone. For this, we will have to have the time zone definition:

krn::time_zone const* myTimeZone = krn::current_zone(); 

myTimeZone points to the description of the time zone in the database: its offset from GMT, DST information (and hopefully leap seconds). The pointer will never be null: current_zone() would have thrown an exception if it couldn’t find the time zone definition. Once we have the definition, we have to adjust the duration in the time point from the system clock, so that it references an epoch from the new time zone:

krn::time_point p1 = krn::system_clock::now();
krn::time_point p2 = myTimeZone->to_local(p1);

The Clock template argument for time point p2 is std::chrono::local_t. It is a pseudo clock: it almost behaves like a clock with one exception: it doesn’t have static member function now().

Now p2 stores the count of seconds (or milliseconds) from the epoch (00:00:00 Jan 1, 1970) in the desired time zone. The count of seconds can be converted into years, months, days, etc.. Just round it up to twenty-minutes, and we are done:

krn::time_point p3 = krn::floor<TwentyMins>(p2);
std::cout << std::format("{:%Y-%m-%d %H:%M}\n", p3);

Here is the full example:

// C++20
#include <chrono>
#include <format>
#include <iostream>

int main()
{
  namespace krn = std::chrono;
  using TwentyMins = krn::duration<int, std::ratio<20*60>>;

  krn::time_zone const* myTimeZone = krn::current_zone(); 

  krn::time_point p1 = krn::system_clock::now();
  krn::time_point p2 = myTimeZone->to_local(p1);
  krn::time_point p3 = krn::floor<TwentyMins>(p2);

  std::cout << std::format("{:%Y-%m-%d %H:%M}\n", p3);
}

However, I know of no online compiler that would be able to demonstrate this. I tested this example in Visual Studio 2022, as this is the only compiler I have the access to that implements both <format> and local time in <chrono>.

This entry was posted in programming and tagged , . Bookmark the permalink.

6 Responses to Local Time

  1. Eric says:

    This reminds me of the decades old list of Falsehoods Programmers Believe About Time (https://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time). The initial list had been copied by Wired in 2012 (https://www.wired.com/2012/06/falsehoods-programmers-believe-about-time/).

  2. georg says:

    Typo:
    “Now, if we want do display the present time in a different time zone” -> “Now, if we want to display the present time in a different time zone”

  3. Probably a naive question (and likely better directed at Mr. Hinnant), but why doesn’t `std::chrono::current_zone()` return a reference if it can never be nullptr?

    • I suppose the interface is dictated by how this pointer is later used. The common use case would be to pass it to zoned_time. zoned_time takes this argument by pointer, because in general it allows any kinds of smart pointers: this parameter is used not only to get access to time zone data record, but also to manage its lifetime. In case of time_zone from tzdb the management is trivial: it is managed externally. But it does not have to be the case for other types. Howard’s GitHub documentation gives an example:

      https://howardhinnant.github.io/date/tz.html

      See section “Custom time zone”.

  4. Pingback: Converting tm_t to file_time_type - makemoneyonlinecom.com

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.