mareek / UUIDNext

A fast and modern .NET library to generate UUID/GUID that are either sequential and database friendly (versions 7), name based (versions 5) or random (version 4).
BSD Zero Clause License
240 stars 13 forks source link

Is UUIDv7 adding milliseconds correctly? #1

Closed stevesimmons closed 2 years ago

stevesimmons commented 2 years ago

The source code for UUIDv7 places the ms value (0-999) in the lower 12 bits (0-4095) of bytes 5 and 6:

        private void SetTimestampMs(Span<byte> bytes, TimeSpan unixTimeStamp)
        {
            short timestampMs = (short)unixTimeStamp.Milliseconds;
            Span<byte> timestampMsBytes = stackalloc byte[2];
            BinaryPrimitives.TryWriteInt16BigEndian(timestampMsBytes, timestampMs);
            // this byte is shared with the last 4 bits of the timestamp.
            // as the 6 upper bits of the milliseconds will alaways be 0 we can simply add the two bytes
            bytes[0] |= timestampMsBytes[0];
            bytes[1] = timestampMsBytes[1];
        }

Doesn't this mean the timestamps implied by UUIDNext's UUIIDv7 will be parsed as having fractional seconds ranging from x.000... secs to x.000244 secs (=999/4096), and then in the next millisecond jump to (x+1).000... secs? i.e. leaving a gap of 756us.

The proposed UUIDv7 spec says at L858 of https://github.com/uuid6/uuid6-ietf-draft/blob/master/draft-peabody-dispatch-new-uuid-format-02.txt:

   To perform the sub-second math, simply take the first (most
   significant/leftmost) N bits of subsec and divide it by 2^N.  Take
   for example:

   1.  To parse the first 16 bits, extract that value as an integer and
       divide it by 65536 (2 to the 16th).

   2.  If these 16 bits are 0101 0101 0101 0101, then treating that as
       an integer gives 0x5555 or 21845 in decimal, and dividing by
       65536 gives 0.3333282

   This sub-second encoding scheme provides maximum interoperability
   across systems where different levels of time precision are
   required/feasible/available.  The timestamp value derived from a
   UUIDv7 value SHOULD be "as close to the correct value as possible"
   when parsed, even across disparate systems.

In your case, the subsecond value comes from unixTimeStamp.Milliseconds rather than being a binary fraction. So I think you should multiply the number of ms by 4096 and take the remainder module 1000.

mareek commented 2 years ago

Thank you for reporting this bug. I may have skimmed the decoding part of the RFC when I implemented it. I'll try to fix it and update the Nuget Package this week.

mareek commented 2 years ago

Fixed by commit e6b0eb16771e6baf718d724249aa23c6a9308ec6