5

I'd like to serialize a std::chrono::local_time by sending it's time_since_epoch().count() value. My question is how is a non-C++ receiver supposed to interpret that value? Is it the actual number of ticks since the epoch at local midnight (1970-01-01T00:00:00)? What about daylight saving time changes? Is the time_since_epoch() bijective with the wall clock time? That is, can there be two values of std::chrono::local_time::time_since_spoch() that represent the same wall clock/calendar time?

I cannot find detailed information about the interpretation of std::chrono::local_time::time_since_spoch() at the usual places: cppreference, the latest C++ standard draft, or Howard Hinnant's date library documentation.

'Why even serialize a std::chrono::local_time?', you may ask. Well, a use case would be a building automation system that must perform a certain task at a given local time on a special day, regardless of timezones or daylight saving time. For example, "turn off the lights at 20:00 local time on Earth Day, 2021 (April 22).

EDIT: 'Why not serialize it as an ISO8601 date/time (without any offset), you may ask?'. I want to serialize it as a compact number using a binary protocol, such as CBOR.

Emile Cormier
  • 28,391
  • 15
  • 94
  • 122
  • I'm currently using Howard Hinnant's date library for this in lieu of not-yet-ready C++20 standard library implementations. Their APIs are nearly identical. – Emile Cormier Jun 03 '21 at 22:19
  • 2
    The epoch is usually 1970-01-01T00:00:00 UTC, not local midnight. This means it represents an exact instant in time, regardless of any human representations. This means that you don't have to think about timezones or daylight savings or leap seconds or anything – Mooing Duck Jun 03 '21 at 22:30
  • @MooingDuck I meant the "epoch" used by the `chrono::local_t` pseudclock. Not the actual Unix Epoch. Other clocks (`chrono::tai_clock`, `chrono::gps_clock`) have epochs that differ from the Unix Epoch. – Emile Cormier Jun 03 '21 at 22:33
  • `std::chrono::local_time` isn't interpretable without a Clock. I had assumed `std::chrono::utc_clock` since that's the most common clock for serialization + scheduling across languages and libraries. Which clock are you using? – Mooing Duck Jun 03 '21 at 22:36
  • @KamilCuk I meant the combination of "wall clock + wall calendar". With UTC, when a time_point straddles a daylight saving change, there can be two values of `utc_time::time_since_epoch` that point to the same wall clock time, thus it us not bijective. I'd like to know if the same thing happens with `local_time`. – Emile Cormier Jun 03 '21 at 22:40
  • I'm not sure the epoch is relevant at all to your use case tbh. Just send the local time and date. – Galik Jun 03 '21 at 22:41
  • @Galik: I want to serialize it as a compact number using a binary protocol, such as CBOR. `std::chrono::local_time::time_since_spoch()` provides this compact number; I just need to understand how it's supposed to be interpreted by a non-C++ peer. – Emile Cormier Jun 03 '21 at 22:49
  • It may provide *a* compact number but, as I mentioned, I don't think the epoch is at all relevant. If you send it, you also have to send your time-zone so the receiver can work back from your epoch & time zone to UTC so they can then calculate the local time. It adds an unnecessary complication when you only want to tell them the local time and date. – Galik Jun 03 '21 at 22:52
  • @Galik The peers in my use case are a GUI client and server running inside the building automation controller. The local time for the special event is in relation to the building automation controller, not someone living in a different time zone. – Emile Cormier Jun 03 '21 at 22:56
  • I still want to understand how the `std::chrono::local_time::time_since_spoch()` behaves regardless of my application. Hopefully Mr Hinnant will pop in and clarify things for everyone. – Emile Cormier Jun 03 '21 at 22:59
  • @Galik: You just made me realize that I could use a pair of numbers instead to avoid the Gregorian calendar computations and the longer ISO8601 string. One number would be `local_days::time_since_epoch()` for the date and the other a `chrono::duration` for the time of day. – Emile Cormier Jun 03 '21 at 23:10
  • Using the same reasoning, Unix Time is also calendar-independent. It is just a count of time since a distinct instant, and one can decode it into a wide variety of calendars. Indeed, this is the principle upon which "user written calendars" interoperate with C++20 chrono. They convert to and from `sys_days` and just add on the time-of-day. Example user written calendars are here: https://github.com/HowardHinnant/date/tree/master/include/date (Julian, ISO week based, Islamic, etc). – Howard Hinnant Jun 03 '21 at 23:12
  • 1
    @MooingDuck: "std::chrono::local_time isn't interpretable without a Clock". It's meant to be interpreted in a local context (hence its name) without any specific time zone. Interpret my use case as: "I want that building to shut off its lights on Earth Day at 20:00 using building's local time (when it gets dark at the building's site)". Clients interacting with the automation server will understand that it's relative to the building's local time. Also see the "New Year's Eve Party" note in https://howardhinnant.github.io/date/tz.html#loc_vs_sys – Emile Cormier Jun 04 '21 at 03:36
  • 1
    @MooingDuck: Also `local_time` uses the `std::chrono::local_t` pseudoclock. `local_time` is simply defined as `template using local_time = std::chrono_point`. To convert a `local_time` to a specific, universal point in time (say, in UTC), you have to include either a `time_zone` or an offset from UTC. – Emile Cormier Jun 04 '21 at 03:51

1 Answers1

4

The value in a local_time is the exact same value it would have in a sys_time. For example:

auto lt = local_days{June/3/2021} + 18h + 30min;

lt is a local time with the indicated value. All one has to do change this to a sys_time is change local_days to sys_days:

auto st = sys_days{June/3/2021} + 18h + 30min;

I.e. one can now assert that st.time_since_epoch() == lt.time_since_epoch(). The only difference between lt and st is semantics.

So you can tell clients to consume this number as if it is Unix Time, which it can then derive year, month, day, time-of-day information, but then treat that information as a local time in (presumably) their local time zone.

In doing that "reinterpret cast", it is quite possible that the local indicated time may not exist, or may be ambiguous because there are two of them. One can up the odds of not hitting such a situation by avoiding times of day in the range 00:00:00 - 04:00:00. If one does hit this situation, there is no one right answer on how you handle it. You'll just have to state a policy along with the rest of your documentation.

...

Or maybe they just write their parser in C++20... :-)

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • I think this is only true if the `local_time` came from a `std::chrono::utc_clock`, and almost certainly false for `std::chrono::steady_clock`, isn't it? – Mooing Duck Jun 03 '21 at 23:05
  • 1
    Nope. `steady_clock` is not part of this conversation. It doesn't exist in the world of calendars and local times. – Howard Hinnant Jun 03 '21 at 23:06
  • Instead of `local_time::time_since_epoch::count()`, would a combination of `local_days::time_since_epoch::count()` for the date and `chrono::duration` for the time of day help in avoiding the ambiguities you describe? – Emile Cormier Jun 03 '21 at 23:15
  • 1
    I don't believe it really avoids any of the problems. The total is simply `day_count * ticks_per_day + day_tick_count`. Or you can do the reverse computation with `%`. The two representations can represent exactly the same thing with no loss of information, or loss of problems when dealing with local time. :-) – Howard Hinnant Jun 03 '21 at 23:17
  • 1
    Ah yes, this derives from the property of `local_days` always having 86400 seconds due to the calendar math working the same as `sys_time` by design. I'm probably stating this inaccurately, but at least it's clear in my head now. – Emile Cormier Jun 03 '21 at 23:31
  • You _could_ just tell clients to subtract their current UTC offset from the number to get a new Unix Time and decode that. The result will be the UTC time for the event. That works great until their UTC offset changes between the time they compute, and the time of the event. That error may or may not be acceptable to your application. It is equivalent to a "choose::earliest" policy for disambiguating local times. – Howard Hinnant Jun 03 '21 at 23:48
  • Wait, is `choose::earliest` an actual thing I missed in your library, or is it just pseudocode for discussion purposes? – Emile Cormier Jun 03 '21 at 23:52
  • Oooo it's real! This should come in handy. https://en.cppreference.com/w/cpp/chrono/choose – Emile Cormier Jun 03 '21 at 23:54
  • Actual thing: https://howardhinnant.github.io/date/tz.html#choose But actually what I said isn't 100% accurate. If the computation occurs a "long time" prior to the event, and the event is "well after" the UTC offset change, then the scheduling is just going to be off by the UTC offset change amount. The tricky thing is that you want the UTC offset at the time of the event, as opposed to at the time of the computation. – Howard Hinnant Jun 03 '21 at 23:55
  • 1
    The best thing might be for the automation controller to compute the UTC time when a client passes a date + time-of-day, rejecting the non-existent times and asking the client to choose ambiguous times. Thereafter, it is always exchanged as UTC. One complication with that is if the automation controller is set to a different time zone after the schedules are programmed in. Anyways, you've given me great pointers to figure out the rest. I don't want to take up more of your time. Thanks! – Emile Cormier Jun 04 '21 at 00:08
  • Hope you don't have a controller or clients in Egypt (https://codeofmatt.com/time-zone-chaos-inevitable-in-egypt/). :-) – Howard Hinnant Jun 04 '21 at 00:23
  • @MooingDuck: `local_time` can come from user input without involving any "real" clocks such as `utc_clock`. E.g. `local_time tee_time = local_days{June/4/2021} + 12h + 34min`. – Emile Cormier Jun 04 '21 at 04:00