Module: Tou

Defined in:
lib/tou.rb,
lib/tou/version.rb,
lib/tou/generator.rb

Overview

A generator for time-ordered UUIDs

Defined Under Namespace

Modules: Generator

Constant Summary collapse

VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

.uuid(**params_for_uuid_bytes) ⇒ String

Generates the bag of 16 bytes with UUID in binary form. This the canonical representation but can be converted into one.

Parameters:

  • random (#bytes)

    Source of randomness. Normally SecureRandom will be used, but it can be replaced by a mock or a ‘Random` object with a known seed, for testing or speed.

  • time (#to_f)

    A time value that is convertible to a floating-point number of seconds since epoch

Returns:

  • (String)

    in binary encoding



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/tou.rb', line 60

def self.uuid(**params_for_uuid_bytes)
  # N : 32 bit unsigned int
  # n : 16 bit unsigned int
  # This will separate the whole random bytestring into a couple groups
  # (as various sized integers) that match the uuid group size
  # so [32bit int, 16bit int, 16bit int, 16bit int, 16bit int, 32bit int]
  # Which then can be shown in hex into the string literal to make up the final uuid.
  ary = uuid_bytes(**params_for_uuid_bytes).unpack("NnnnnN")
  # format in hex notation with dashes
  "%08x-%04x-%04x-%04x-%04x%08x" % ary
end

.uuid_bytes(random: SecureRandom, time: Time.now) ⇒ String

Generates the bag of 16 bytes with UUID in binary form. This is not the canonical representation but can be converted into one.

Parameters:

  • random (#bytes) (defaults to: SecureRandom)

    Source of randomness. Normally SecureRandom will be used, but it can be replaced by a mock or a ‘Random` object with a known seed, for testing or speed.

  • time (#to_f) (defaults to: Time.now)

    A time value that is convertible to a floating-point number of seconds since epoch

Returns:

  • (String)

    in binary encoding



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/tou.rb', line 16

def self.uuid_bytes(random: SecureRandom, time: Time.now)
  # Use microseconds for the timestamp
  epoch_micros = (time.to_f * 1_000_000).round
  # Encode an 8-byte unsigned big-endian uint, and skip the first byte since it's 0, so we're left with 7 bytes.
  # This gives us sufficient timestamp resolution to last us into year
  # This will limit our timestamp to the year 4307.
  # Q> : 64 bit unsigned big-endian int
  # We want more significant bytes first for better sorting in Postgres
  epoch_micros_unsigned_uint_bytes = [epoch_micros].pack("Q>")[1..]
  ts_bytes = epoch_micros_unsigned_uint_bytes

  # Use the remaining bytes for randomness
  byte_str = ts_bytes + random.bytes(16 - ts_bytes.bytesize)

  # This last part encodes the version, the timecode and the variant.
  # To prevent we have a variant, 4 bits of random and then the timecode I move up the timecode 4 bits.
  # We end up with: 4bit variant - 8bit timecode - 4bit random from original byte string, hence 2 bytes in total.
  #
  # V4 random UUIDs use 4 bits to indicate a version and another 2-3 bits to indicate a variant.
  # Most V4s (including these ones) are variant 1, which is 2 bits.
  version = "4" # version 4, 4 bits.
  timepart = ts_bytes[-1].unpack1("H*") # last byte of timecode (ts_bytes) as hex, 8 bits.
  rest = byte_str[7].unpack1("h") # last 4 bits of 7th byte as hex.

  # Our uuid with encoded timestamp will look something like this 16 bytes in total:
  # ts = timestamp byte
  # rnd = random byte
  # |ts| ts, ts, ts, ts, ts, version(4bits) + first half ts(4bits), last half of ts(4bits) + rnd, variant(2bits) + rnd, rnd, rnd, rnd, rnd, rnd, rnd, rnd]
  byte_str_part_with_time_and_variant = [version + timepart + rest].pack("H*")

  byte_str.setbyte(6, byte_str_part_with_time_and_variant.getbyte(0))
  byte_str.setbyte(7, byte_str_part_with_time_and_variant.getbyte(1))
  byte_str.setbyte(8, (byte_str.getbyte(8) & 0x3f) | 0x80) # variant 1 (10 binary)

  byte_str
end