Class: UUIDv9

Inherits:
Object
  • Object
show all
Defined in:
lib/uuid-v9.rb

Constant Summary collapse

UUID_REGEX =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
BASE16_REGEX =
/^[0-9a-fA-F]+$/

Class Method Summary collapse

Class Method Details

.add_dashes(str) ⇒ Object



71
72
73
# File 'lib/uuid-v9.rb', line 71

def self.add_dashes(str)
  "#{str[0, 8]}-#{str[8, 4]}-#{str[12, 4]}-#{str[16, 4]}-#{str[20..]}"
end

.calc_checksum(hex_string) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/uuid-v9.rb', line 5

def self.calc_checksum(hex_string)
  data = hex_string.scan(/.{1,2}/).map { |byte| byte.to_i(16) }
  polynomial = 0x07
  crc = 0x00

  data.each do |byte|
    crc ^= byte
    8.times do
      if crc & 0x80 != 0
        crc = (crc << 1) ^ polynomial
      else
        crc <<= 1
      end
    end
  end

  crc &= 0xFF
  crc.to_s(16).rjust(2, '0')
end

.check_version(uuid, version = nil) ⇒ Object



31
32
33
34
35
# File 'lib/uuid-v9.rb', line 31

def self.check_version(uuid, version = nil)
  version_digit = uuid[14]
  variant_digit = uuid[19]
  (!version || (version_digit == version.to_s && "89abAB".include?(variant_digit)))
end

.generate(options = {}) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/uuid-v9.rb', line 75

def self.generate(options = {})
  prefix = options.fetch(:prefix, nil) # .to_s
  timestamp = options.fetch(:timestamp, true)
  checksum = options.fetch(:checksum, false)
  version = options.fetch(:version, false)
  legacy = options.fetch(:legacy, false)
  suffix = options.fetch(:suffix, nil) # .to_s

  if prefix && !prefix.empty?
    validate_prefix(prefix)
    prefix = prefix.downcase
  end

  if suffix && !suffix.empty?
    validate_suffix(suffix)
    suffix = suffix.downcase
  end

  center = case timestamp
          when true
            Time.now.to_i.to_s(16)
          when Time
            timestamp.to_i.to_s(16)
          when Numeric, String
            Time.at(timestamp.to_i).to_i.to_s(16)
          else
            ''
          end

  random_length = 32 - (prefix ? prefix.length : 0) - (suffix ? suffix.length : 0) - center.length - (checksum ? 2 : 0) - ((legacy || version) ? 2 : 0) # (legacy ? 2 : (version ? 1 : 0))
  random = random_bytes(random_length)

  joined = "#{prefix}#{center}#{random}#{suffix}"

  if legacy
    joined = "#{joined[0, 12]}#{center.length > 0 ? '1' : '4'}#{joined[12, 3]}#{random_char('89ab')}#{joined[15..]}"
  elsif version
    joined = "#{joined[0, 12]}9#{joined[12, 3]}#{random_char('89ab')}#{joined[15..]}" # {joined[12..]}
  end

  joined += calc_checksum(joined) if checksum

  add_dashes(joined)
end

.is_base16?(str) ⇒ Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/uuid-v9.rb', line 55

def self.is_base16?(str)
  BASE16_REGEX.match?(str)
end

.is_uuid?(uuid) ⇒ Boolean

Returns:

  • (Boolean)


37
38
39
# File 'lib/uuid-v9.rb', line 37

def self.is_uuid?(uuid)
  !uuid.empty? && UUID_REGEX.match?(uuid)
end

.is_valid_uuidv9?(uuid, options) ⇒ Boolean

Returns:

  • (Boolean)


41
42
43
44
45
# File 'lib/uuid-v9.rb', line 41

def self.is_valid_uuidv9?(uuid, options)
  is_uuid?(uuid) &&
    (!options.key?(:checksum) || !options[:checksum] || verify_checksum(uuid)) &&
    (!options.key?(:version) || !options[:version] || check_version(uuid, options[:version]))
end

.random_bytes(count) ⇒ Object



47
48
49
# File 'lib/uuid-v9.rb', line 47

def self.random_bytes(count)
  Array.new(count) { rand(0..15).to_s(16) }.join
end

.random_char(chars) ⇒ Object



51
52
53
# File 'lib/uuid-v9.rb', line 51

def self.random_char(chars)
  chars[rand(chars.length)]
end

.validate_prefix(prefix) ⇒ Object

Raises:

  • (ArgumentError)


59
60
61
62
63
# File 'lib/uuid-v9.rb', line 59

def self.validate_prefix(prefix)
  raise ArgumentError, 'Prefix must be a string' unless prefix.is_a?(String) # isinstance?(prefix, str)
  raise ArgumentError, 'Prefix must be no more than 8 characters' if prefix.length > 8
  raise ArgumentError, 'Prefix must be only hexadecimal characters' unless is_base16?(prefix)
end

.validate_suffix(suffix) ⇒ Object

Raises:

  • (ArgumentError)


65
66
67
68
69
# File 'lib/uuid-v9.rb', line 65

def self.validate_suffix(suffix)
  raise ArgumentError, 'Suffix must be a string' unless suffix.is_a?(String) # isinstance?(suffix, str)
  raise ArgumentError, 'Suffix must be no more than 4 characters' if suffix.length > 4
  raise ArgumentError, 'Suffix must be only hexadecimal characters' unless is_base16?(suffix)
end

.verify_checksum(uuid) ⇒ Object



25
26
27
28
29
# File 'lib/uuid-v9.rb', line 25

def self.verify_checksum(uuid)
  base16_string = uuid.delete('-')[0, 30]
  crc = calc_checksum(base16_string)
  crc == uuid[34, 2]
end