Subject: [Cbor] Multiple ways of expressing the same time via draft-bormann-cbor-time-tag

From: Emile Cormier <[email-masked]>
Date: Tue, 6 Apr 2021 17:35:17 -0300

I'm implementing tags 1001 and 1002 in my C++ library to exchange time
points and durations (refer to
https://www.ietf.org/archive/id/draft-bormann-cbor-time-tag-04.html).

I'm in the situation where I''m not sure which is the "expected" way to
encode time as nanoseconds since the Unix epoch. I'm specifically referring
to std::chrono::system_clock::time_point, which is a 64 bit integer. There
are currently three ways I could encode such a value.

1. The whole number of seconds under Key 1, with the remaining nanoseconds
under Key -9.

2. The total number of nanoseconds under Key -9, with a value of zero under
Key 1.

3. The total number of nanoseconds expressed as a Decimal Fraction under
Key 4.

Option #2 is the easiest and most efficient because it doesn't involve a
division/subtraction calculation. However, a receiver that discards Key -9
will end up with zero seconds. At least with option #1, the receiver will
know the number of whole seconds if Key -9 is ignored.

Option #3 requires that the sender and receiver understand Tag 4 (Decimal
Fraction), or at least the concept of decimal fractions within the context
of tags 1001-1003.

I'm guessing that option #1 is the "canonical" way, but I'm not entirely
sure. Some guidance would be appreciated. Perhaps a few words concerning
this should be added to the standard's text?

On the receiving side, my library would ideally handle all three scenarios.
It's on the sending side I'm more concerned about when I need to
interoperate with CBOR decoders other than mine.

I apologize if this has already been discussed before. I'm a bit late to
the CBOR party. :-)

Cheers,
Emile Cormier

---

From: Carsten Bormann <[email-masked]>
Date: Fri, 9 Apr 2021 11:18:28 +0200

Hi Emile,

On 2021-04-06, at 22:35, Emile Cormier <[email-masked]> wrote:
> 
> I'm implementing tags 1001 and 1002 in my C++ library to exchange time points and durations (refer to https://www.ietf.org/archive/id/draft-bormann-cbor-time-tag-04.html).
> 
> I'm in the situation where I''m not sure which is the "expected" way to encode time as nanoseconds since the Unix epoch. I'm specifically referring to std::chrono::system_clock::time_point, which is a 64 bit integer. There are currently three ways I could encode such a value.
> 
> 1. The whole number of seconds under Key 1, with the remaining nanoseconds under Key -9.
> 
> 2. The total number of nanoseconds under Key -9, with a value of zero under Key 1.
> 
> 3. The total number of nanoseconds expressed as a Decimal Fraction under Key 4.

Oh.  You are pointing out a deficiency of the current text.  The *intention* was that Key 1 is an integer and Key -3*n is supplying the fractional part.  E.g., like UNIX struct timeval for -6 (old) and struct timespec for -9 (new) do.
So that would be your Option 1.

But since you raise it, we probably should entertain the question.

The issue really is whether the onus is on the sender/encoder or the recipient/decoder.

Restricting the value for key -3*n to be less than 10**(3*n) places the onus on the sender, who would need to do a modulo if that is not the way the data is already represented on the sender’s platform.
Not adding this restriction places the onus on the recipient, who would then need to do the modulo in case the recipient’s platform representation is like one of the UNIX ones.

> Option #2 is the easiest and most efficient because it doesn't involve a division/subtraction calculation.

… for the sender.

> However, a receiver that discards Key -9 will end up with zero seconds. At least with option #1, the receiver will know the number of whole seconds if Key -9 is ignored.

Which indeed maybe also is a concern.

> Option #3 requires that the sender and receiver understand Tag 4 (Decimal Fraction), or at least the concept of decimal fractions within the context of tags 1001-1003.

That would seem to be the easier case; “understanding” Tag 4 in this context is limited to being able to parse the 4([-6, microseconds]) arrangement, unless the sender’s code looks for trailing zeros and does stunts like 4([-5, tens-of-microseconds]) in these cases, which of course cannot be excluded.

> I'm guessing that option #1 is the "canonical" way, but I'm not entirely sure. Some guidance would be appreciated. Perhaps a few words concerning this should be added to the standard's text?

Definitely, either way.

I think I’m mildly in favor of placing the onus on the sender, i.e., Option 1.

etime = #6.1001({
  ...
  1: int,
  ? (
    -3: uint .lt 1000 //
    -6: uint .lt 1000000 //
    -9: uint .lt 1000000000 //
    -12: uint .lt 1000000000000 //
    -15: uint .lt 1000000000000000 //
    -18: uint .lt 1000000000000000000
  )
  ...
})

> On the receiving side, my library would ideally handle all three scenarios. It's on the sending side I'm more concerned about when I need to interoperate with CBOR decoders other than mine.

Right, be conservative…liberal…

Thanks for bringing this up!

One always learns a lot from implementation efforts.

Grüße, Carsten

---

From: Emile Cormier <[email-masked]>
Date: Fri, 9 Apr 2021 16:13:40 -0300

Hi Carsten,

As always, thank you for your invaluable insights. I'll proceed with my C++
implementation assuming that my option #1 is what's preferred for the
sender. I did indeed strike me that the POSIX timespec struct, with its
separate fields for seconds and nanoseconds, seemed to be the inspiration
behind the specification.

The "penalty" for the division/modulo operation, for sending a
std::chrono::time_point via tag 1001, is probably insignificant. I'm sure
it's orders of magnitude faster than converting it to ISO8601 text.

By the way, my "scaled arbitrary ratio" idea from before turned out to be
useful as an internal, intermediate representation between various
application time types (like std::chrono::time_point, time_t, timespec,
etc...) and the various JSON/CBOR-like codecs that my library supports.

Cheers,
Emile Cormier

On Fri, Apr 9, 2021 at 6:18 AM Carsten Bormann <[email-masked]> wrote:

> Oh.  You are pointing out a deficiency of the current text.  The
> *intention* was that Key 1 is an integer and Key -3*n is supplying the
> fractional part.  E.g., like UNIX struct timeval for -6 (old) and struct
> timespec for -9 (new) do.
> Restricting the value for key -3*n to be less than 10**(3*n) places the
> onus on the sender, who would need to do a modulo if that is not the way
> the data is already represented on the sender’s platform.
> Not adding this restriction places the onus on the recipient, who would
> then need to do the modulo in case the recipient’s platform representation
> is like one of the UNIX ones.
>
> division/subtraction calculation.
>
> … for the sender.
>
> > However, a receiver that discards Key -9 will end up with zero seconds.
> At least with option #1, the receiver will know the number of whole seconds
> if Key -9 is ignored.
>
> > Option #3 requires that the sender and receiver understand Tag 4
> (Decimal Fraction), or at least the concept of decimal fractions within the
> context of tags 1001-1003.
>
> That would seem to be the easier case; “understanding” Tag 4 in this
> context is limited to being able to parse the 4([-6, microseconds])
> arrangement, unless the sender’s code looks for trailing zeros and does
> stunts like 4([-5, tens-of-microseconds]) in these cases, which of course
> cannot be excluded.
>
> > On the receiving side, my library would ideally handle all three
> scenarios. It's on the sending side I'm more concerned about when I need to