fast-uuid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a8b04df8089715f0156dc86feb6b73dd3abebea996b08c213e9b43844bfc9ce
4
+ data.tar.gz: a6e2cae4c744b1b238af21188dab84a6e2830e2108760db5e71c465a789205dd
5
+ SHA512:
6
+ metadata.gz: a629ab2256ed2043b278d6953b67a96a057888c60a01bed3cd740f9690047be5dcfcec16d0e3f92ec47e91eef051cd24a48f9d3d4d2ee6758fa9c6efe0a05cf4
7
+ data.tar.gz: 4037526e2102bbcf3cfff76b8a2a700ebcfe29ceb85c0793e9fb3b7aac35600ae898c312fa7130c1f6e594e4920e033fef8e2744d18fef79a3c3c5b0d2a932ba
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release: `Fast::UUID`, one immutable, frozen value class. Generate
6
+ v4 (122 random bits from `SecureRandom`) and v7 (48-bit `unix_ts_ms` plus
7
+ 74 fully random bits, no counter and no shared state, thread-safe), with
8
+ `generate` defaulting to v7. Parse and validate every RFC 9562 version and
9
+ variant — validity is the RFC 4 grammar and nothing more, case-insensitive,
10
+ Nil and Max included — with `parse`, `parse?` and `valid?`. `Fast::UUID.new`
11
+ builds from a 16-byte binary String.
12
+ - Timestamps: v1 and v6 read their 60-bit 100-ns Gregorian timestamp, v7 its
13
+ `unix_ts_ms`; every other version reports nil. Comparison is opaque
14
+ unsigned 128-bit byte order, so v6 and v7 sort chronologically; v1 does
15
+ not, its `time_low` sits first, and the README carries the caveat.
16
+ - `to_s`, `to_str`, `bytes`, `hex`, `urn`, `nil_uuid?`, `max_uuid?`,
17
+ `version`, `variant`, and `==`/`eql?`/`hash` over the 128-bit value alone.
18
+ Verified in-suite against all six RFC 9562 Appendix A test vectors.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # fast-uuid
2
+
3
+ > **Status:** _Alpha, pre-1.0_ The API surface may still move between v0.x and v1.0.
4
+
5
+ A UUID library for Ruby that is fast, complete, and RFC 9562-compliant.
6
+
7
+ Fast::UUID provides drop-in, api-compatible replacements for `SecureRandom`'s `.uuid_v4` and `.uuid_v7` that are as fast (within 2%) or faster while allocating half as many objects or fewer while using the same source of entropy.
8
+
9
+ ```ruby
10
+ # before
11
+ SecureRandom.uuid_v4 # => "cdbb6d1f-…"
12
+ SecureRandom.uuid_v7 # => "01a12861-…"
13
+
14
+ # after — the same calls, the same entropy, frozen, half the objects or fewer
15
+ Fast::UUID.uuid_v4 # => "e74ce176-…"
16
+ Fast::UUID.uuid_v7 # => "01a12861-…"
17
+ ```
18
+
19
+ Fast::UUID can also return immutable `Fast::UUID` objects that are directly `Comparable`. These also allocate fewer objects than `SecureRandom`'s `.uuid_v4` and `.uuid_v7`.
20
+
21
+ ```ruby
22
+ a = Fast::UUID.v7(time: Time.utc(2026, 10, 10))
23
+ b = Fast::UUID.v7(time: Time.utc(2026, 10, 11))
24
+ a < b # => true — v7 byte order is time order
25
+ [a, b].max.timestamp # => 2026-10-11 00:00:00 UTC
26
+ Fast::UUID.parse(a.to_s) == a # => true — identity is the 128-bit value alone
27
+ ```
28
+
29
+ Fast::UUID's goal is to cover every UUID use-case a project could have in a way that is correct and of course, fast.
30
+
31
+ ## Installation
32
+
33
+ Add to your Gemfile:
34
+
35
+ ```ruby
36
+ gem "fast-uuid"
37
+ ```
38
+ and `require "fast/uuid"`.
39
+
40
+ Fast::UUID has no dependencies and is implemented in pure Ruby.
41
+
42
+ ## Quickstart
43
+
44
+ ```ruby
45
+ require "fast/uuid"
46
+
47
+ uuid = Fast::UUID.generate # v7, the RFC's recommended default
48
+ Fast::UUID.uuid_v4 # => "b4c7b0f1-…", or uuid_v1 - uuid_v7. Returns a String just like SecureRandom
49
+ Fast::UUID.parse(uuid.to_s) == uuid # => true, from any conforming String, any case
50
+ Fast::UUID.v4.version # => 4
51
+ Fast::UUID.v7.timestamp # => a UTC Time, the 48-bit unix_ts_ms
52
+ Fast::UUID.v5(Fast::UUID::NAMESPACE_DNS, "www.example.com").to_s
53
+ # => "2ed6657d-e927-568b-95e1-2665a8aea6a2", same input, same UUID
54
+
55
+ sorted = [Fast::UUID.v7, Fast::UUID.v7].sort # v7 sorts chronologically, RFC 6.11
56
+ ```
57
+
58
+ `Fast::UUID.parse` raises `Fast::UUID::ParseError` for anything the RFC
59
+ 4 grammar rejects and `TypeError` for an object with no `to_s`; `parse?`
60
+ and `valid?` are the non-raising forms. Every version and variant
61
+ parses, Nil and Max included.
62
+
63
+ One caveat the RFC does not decide for you: opaque byte order makes v6 and
64
+ v7 sort chronologically, but **v1 does not**. The
65
+ [tutorial](docs/tutorials/generate-parse-and-sort-uuids.md) walks through the whole
66
+ story: generate, parse, introspect, sort, and a request-ID module at the end
67
+ of it.
68
+
69
+ ## Performance
70
+
71
+ Measured on the six operations the gem and its references share — generation
72
+ against `securerandom`, parsing against `uuidtools` (Ruby 4.0.7; conditions,
73
+ the allocation columns and how to reproduce on the
74
+ [benchmarks page](docs/explanation/benchmarks.md)):
75
+
76
+ - Generation is **ahead of the stdlib String for v4, within 10% for v7** —
77
+ 0.65M vs 0.64M i/s for v4, 0.58M vs 0.65M for v7, at half or fewer of the
78
+ objects — and the String forms win outright: `uuid_v4` 0.72M vs 0.64M with a
79
+ quarter of the objects, `uuid_v7` 0.64M vs 0.65M — parity — with two fifths.
80
+ - Parsing is **2.7x faster than uuidtools with 4.5x fewer objects** — 1.14M vs
81
+ 0.42M i/s, 6 objects per call against 27 — because it is a grammar check and
82
+ one `pack`, not a decoded field graph.
83
+
84
+ ## Documentation
85
+
86
+ ### Tutorials
87
+
88
+ - [Tutorial: generate, parse and sort UUIDs](docs/tutorials/generate-parse-and-sort-uuids.md) — generate, parse, introspect and sort, ending in a sortable request ID.
89
+
90
+ ### How-to guides
91
+
92
+ - [How to choose a UUID version](docs/how-to/choose-a-uuid-version.md) — v7 by default, v6 for a v1 fleet, v5 for name-derived determinism, v4 for randomness, v1 for legacy systems.
93
+ - [How to generate deterministic UUIDs](docs/how-to/generate-deterministic-uuids.md) — pin a v7 to an exact millisecond with `v7(time:)`, pin a v1 or v6 to the 100-ns tick, or derive a stable v5 from a namespace and name.
94
+ - [How to validate untrusted input](docs/how-to/validate-untrusted-input.md) — what `parse?` and `valid?` return, and what `parse` raises.
95
+ - [How to sort UUIDs](docs/how-to/sort-uuids.md) — `<=>` byte order, and the v1 caveat.
96
+ - [How to store a UUID as 16 bytes](docs/how-to/store-a-uuid-as-16-bytes.md) — `bytes` out, `new(bytes)` back.
97
+
98
+ ### Reference
99
+
100
+ - [Reference: Fast::UUID](docs/reference/uuid.md) — every class method, instance method, constant and error.
101
+
102
+ ### Explanation
103
+
104
+ - [What a UUID is, and what its versions are](docs/explanation/uuid-versions.md) — embedded time, natural sorting, name-derived determinism, and randomness as one option among five.
105
+ - [Why validity is the grammar and nothing more](docs/explanation/validity.md) — why no version or variant gate: the RFC defines none, and Nil and Max sit outside the 10xx variant by design.
106
+ - [In what order UUIDs sort](docs/explanation/comparison-order.md) — opaque bytes per RFC §6.11, v6/v7 chronological, v1 anti-chronological.
107
+ - [Benchmarks](docs/explanation/benchmarks.md) — generation against securerandom, parsing against uuidtools, and how to reproduce them.
108
+
109
+ ## Development
110
+
111
+ ```bash
112
+ bundle exec sus
113
+ bundle exec rubocop
114
+ bundle exec ruby benchmark/uuid.rb # full run, about a minute
115
+ BENCH_QUICK=1 bundle exec ruby benchmark/uuid.rb # quick run, seconds
116
+ ```
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fast
4
+ class UUID
5
+ class Error < StandardError; end
6
+
7
+ # A String that is not a UUID in the RFC 9562 canonical form.
8
+ class ParseError < Error; end
9
+ end
10
+ end
@@ -0,0 +1,375 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "securerandom"
5
+
6
+ module Fast
7
+ # An immutable, frozen RFC 9562 UUID value: generate v4/v7, time-based
8
+ # v1/v6 and name-based v3/v5, parse and validate every version and
9
+ # variant, compare as an opaque 128-bit value.
10
+ class UUID
11
+ include Comparable
12
+
13
+ # The canonical form, 4hexOctet-2hexOctet-2hexOctet-2hexOctet-6hexOctet,
14
+ # case-insensitive. Validity is this grammar and nothing more: the RFC
15
+ # defines no version or variant gate, and Nil and Max sit outside the
16
+ # 10xx variant by design.
17
+ CANONICAL = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
18
+
19
+ # 100-ns ticks between the Gregorian (1582-10-15) and Unix epoch baselines.
20
+ GREGORIAN_OFFSET = 122_192_928_000_000_000
21
+
22
+ # 128 random bits from the CSPRNG, version and variant stamped into
23
+ # place: 122 stay random, version 4 fills one nibble and the 10xx
24
+ # variant the top two bits.
25
+ def self.v4
26
+ new(v4_bytes)
27
+ end
28
+
29
+ # The same 128 random bits as .v4, rendered straight to the canonical
30
+ # form — drop-in for SecureRandom.uuid_v4, the value object skipped.
31
+ def self.uuid_v4
32
+ canonical(v4_bytes)
33
+ end
34
+
35
+ # The time is truncated to whole milliseconds for the 48-bit unix_ts_ms;
36
+ # rand_a and rand_b stay fully random, no counter and no shared state.
37
+ def self.v7(time: Time.now)
38
+ new(v7_bytes(time: time))
39
+ end
40
+
41
+ # The same 48-bit unix_ts_ms and random tail as .v7, rendered straight
42
+ # to the canonical form — drop-in for SecureRandom.uuid_v7.
43
+ def self.uuid_v7(time: Time.now)
44
+ canonical(v7_bytes(time: time))
45
+ end
46
+
47
+ # The 60-bit 100-ns Gregorian timestamp both time-based versions
48
+ # encode, masked to the field the way v7 masks unix_ts_ms.
49
+ def self.gregorian_ticks(time)
50
+ ((time.to_i * 10_000_000) + (time.nsec / 100) + GREGORIAN_OFFSET) & 0xFFFFFFFFFFFFFFF
51
+ end
52
+
53
+ # The node: nil draws a fresh CSPRNG value with the multicast bit set
54
+ # (RFC §6.10), an Integer-like input masks to 48 bits, a string-like
55
+ # input unpacks from its 6 binary bytes — the size is the requirement,
56
+ # not the class — and a Symbol goes by its #name, the frozen String it
57
+ # already carries. The 48 bits travel as hex — a Q> would land past the
58
+ # immediate Integers three times in four and cost an object when it
59
+ # does.
60
+ def self.node_id(node)
61
+ case node
62
+ when nil
63
+ SecureRandom.bytes(6).unpack1("H12").to_i(16) | (1 << 40)
64
+ when Integer
65
+ node & 0xFFFFFFFFFFFF
66
+ when String
67
+ raise TypeError, "a 6-byte node String is required (got #{node.bytesize} bytes)" unless node.bytesize == 6
68
+
69
+ node.b.unpack1("H12").to_i(16)
70
+ when Symbol
71
+ node_id(node.name)
72
+ else
73
+ return node.to_int & 0xFFFFFFFFFFFF if defined?(node.to_int)
74
+ return node_id(node.to_s) if defined?(node.to_s)
75
+
76
+ got = defined?(node.class) ? node.class : "a classless object"
77
+ raise TypeError, "an Integer-like or 6-byte string-like node is required (got #{got})"
78
+ end
79
+ end
80
+
81
+ # The low word v1 and v6 share: the 14-bit clock_seq over the 48-bit
82
+ # node, the variant's top two bits stamped in place after the pack. A
83
+ # nil clock_sequence draws a fresh CSPRNG value, as v4 draws its bits;
84
+ # an Integer-like one masks to 14 bits.
85
+ def self.sequence_low(node, clock_sequence)
86
+ clock_sequence = case clock_sequence
87
+ when Integer then clock_sequence
88
+ when nil then SecureRandom.bytes(2).unpack1("n") & 0x3FFF
89
+ else
90
+ unless defined?(clock_sequence.to_int)
91
+ got = defined?(clock_sequence.class) ? clock_sequence.class : "a classless object"
92
+ raise TypeError, "an Integer-like clock_sequence is required (got #{got})"
93
+ end
94
+
95
+ clock_sequence.to_int
96
+ end
97
+
98
+ ((clock_sequence & 0x3FFF) << 48) | node_id(node)
99
+ end
100
+
101
+ # The 60-bit Gregorian timestamp split least-significant-first:
102
+ # time_low, then time_mid, then time_hi under the version nibble 1.
103
+ # Where possible, prefer v7 (RFC §5.7): v1's least significant field
104
+ # sits first, so it does not sort chronologically.
105
+ def self.v1(time: Time.now, node: nil, clock_sequence: nil)
106
+ new(v1_bytes(time: time, node: node, clock_sequence: clock_sequence))
107
+ end
108
+
109
+ # The same 60-bit timestamp as .v1, rendered straight to the canonical
110
+ # form with the same node and clock sequence handling.
111
+ def self.uuid_v1(time: Time.now, node: nil, clock_sequence: nil)
112
+ canonical(v1_bytes(time: time, node: node, clock_sequence: clock_sequence))
113
+ end
114
+
115
+ # v1's timestamp reordered most-significant-first (RFC §5.6, the v1
116
+ # migration version): time_high, then time_mid, then time_low under the
117
+ # version nibble 6. Same node, clock sequence and variant as v1, and it
118
+ # sorts chronologically where v1 does not.
119
+ def self.v6(time: Time.now, node: nil, clock_sequence: nil)
120
+ new(v6_bytes(time: time, node: node, clock_sequence: clock_sequence))
121
+ end
122
+
123
+ # The same reordered timestamp as .v6, rendered straight to the
124
+ # canonical form.
125
+ def self.uuid_v6(time: Time.now, node: nil, clock_sequence: nil)
126
+ canonical(v6_bytes(time: time, node: node, clock_sequence: clock_sequence))
127
+ end
128
+
129
+ # MD5 over the namespace's 16 bytes followed by the name's, version and
130
+ # variant stamped into the whole 128-bit digest. Where possible, prefer
131
+ # v5.
132
+ def self.v3(namespace, name)
133
+ new(v3_bytes(namespace, name))
134
+ end
135
+
136
+ # The same digest as .v3, rendered straight to the canonical form.
137
+ def self.uuid_v3(namespace, name)
138
+ canonical(v3_bytes(namespace, name))
139
+ end
140
+
141
+ # SHA-1 over the same input as v3, the leftmost 128 bits of its 160-bit
142
+ # digest (the rightmost 32 discarded), version and variant stamped in
143
+ # place.
144
+ def self.v5(namespace, name)
145
+ new(v5_bytes(namespace, name))
146
+ end
147
+
148
+ # The same digest as .v5, rendered straight to the canonical form.
149
+ def self.uuid_v5(namespace, name)
150
+ canonical(v5_bytes(namespace, name))
151
+ end
152
+
153
+ # The RFC's recommended default: v7.
154
+ def self.generate
155
+ v7
156
+ end
157
+
158
+ # The RFC's recommended default as a String: the v7 canonical form.
159
+ def self.uuid
160
+ uuid_v7
161
+ end
162
+
163
+ # The input of both name-based versions: the namespace's 16 bytes
164
+ # followed by the name's. A UUID namespace is itself and anything else
165
+ # string-like parses as one; the name is the String form it carries —
166
+ # a name has no grammar, every String is one.
167
+ def self.digest_input(namespace, name)
168
+ name = implicit_string(name)
169
+ namespace = case namespace
170
+ when UUID then namespace
171
+ else parse(namespace)
172
+ end
173
+ namespace.bytes + name.b
174
+ end
175
+
176
+ private_class_method :digest_input
177
+
178
+ # The 16 raw bytes each version builds, version and variant nibbles
179
+ # stamped in place, shared by the object constructors and their uuid_*
180
+ # String twins.
181
+ def self.v4_bytes
182
+ stamp(SecureRandom.bytes(16), 4)
183
+ end
184
+
185
+ def self.v7_bytes(time:)
186
+ bytes = SecureRandom.bytes(16)
187
+ milliseconds = (time.to_i * 1000) + (time.nsec / 1_000_000)
188
+ milliseconds &= 0xFFFFFFFFFFFF
189
+ # The masked unix_ts_ms rides in the middle six bytes of an 8-byte
190
+ # word, leaving bytes 6-7 to the random rand_a they already hold.
191
+ bytes.bytesplice(0, 6, [milliseconds].pack("Q>"), 2, 6)
192
+ stamp(bytes, 7)
193
+ end
194
+
195
+ def self.v1_bytes(time:, node:, clock_sequence:)
196
+ ticks = gregorian_ticks(time)
197
+ bytes = [((ticks & 0xFFFFFFFF) << 32) | (((ticks >> 32) & 0xFFFF) << 16) | (ticks >> 48),
198
+ sequence_low(node, clock_sequence)].pack("Q> Q>")
199
+ stamp(bytes, 1)
200
+ end
201
+
202
+ def self.v6_bytes(time:, node:, clock_sequence:)
203
+ ticks = gregorian_ticks(time)
204
+ bytes = [((ticks >> 28) << 32) | (((ticks >> 12) & 0xFFFF) << 16) | (ticks & 0xFFF),
205
+ sequence_low(node, clock_sequence)].pack("Q> Q>")
206
+ stamp(bytes, 6)
207
+ end
208
+
209
+ def self.v3_bytes(namespace, name)
210
+ stamp(Digest::MD5.digest(digest_input(namespace, name)), 3)
211
+ end
212
+
213
+ def self.v5_bytes(namespace, name)
214
+ stamp(Digest::SHA1.digest(digest_input(namespace, name)).byteslice(0, 16), 5)
215
+ end
216
+
217
+ # The version nibble and the 10xx variant, masked into the finished 16
218
+ # bytes in place — setbyte arithmetic, zero objects.
219
+ def self.stamp(bytes, version)
220
+ bytes.setbyte(6, (bytes.getbyte(6) & 0x0F) | (version << 4))
221
+ bytes.setbyte(8, (bytes.getbyte(8) & 0x3F) | 0x80)
222
+ bytes
223
+ end
224
+
225
+ # The canonical dashed form of 16 stamped bytes: hexencode's lowercase
226
+ # hex, the hyphens inserted in place, frozen like #to_s.
227
+ def self.canonical(bytes)
228
+ Digest.hexencode(bytes).insert(8, "-").insert(13, "-").insert(18, "-").insert(23, "-").freeze
229
+ end
230
+
231
+ private_class_method :v4_bytes, :v7_bytes, :v1_bytes, :v6_bytes, :v3_bytes, :v5_bytes,
232
+ :stamp, :canonical
233
+
234
+ # The String a parse or valid? input carries: a String is itself, a
235
+ # Symbol's frozen #name is already the String it carries — no #to_s
236
+ # copy — anything else responding to #to_s converts, anything else has
237
+ # no string form. A BasicObject descendant has no #is_a?, #class or
238
+ # #respond_to? either, so the probe is defined? — and the TypeError
239
+ # cannot name the class.
240
+ def self.implicit_string(str)
241
+ raise TypeError, "no implicit conversion into Fast::UUID" unless defined?(str.to_s)
242
+
243
+ str = str.name if str.is_a?(Symbol)
244
+ str = str.to_s unless str.is_a?(String)
245
+ str
246
+ end
247
+
248
+ private_class_method :implicit_string
249
+
250
+ def self.parse(str)
251
+ str = implicit_string(str)
252
+ raise ParseError, "not a valid UUID: #{str}" unless CANONICAL.match?(str)
253
+
254
+ new([str.delete("-")].pack("H*"))
255
+ end
256
+
257
+ def self.parse?(str)
258
+ parse(str)
259
+ rescue TypeError, ParseError
260
+ nil
261
+ end
262
+
263
+ def self.valid?(str)
264
+ CANONICAL.match?(implicit_string(str))
265
+ rescue TypeError
266
+ false
267
+ end
268
+
269
+ # From a 16-byte binary String, or anything carrying one via #to_str,
270
+ # the implicit String protocol; an encoding tag on the input is
271
+ # normalized to binary, every other ill shape raises ArgumentError.
272
+ # The probe is defined?, as implicit_string's — a BasicObject
273
+ # descendant has no #is_a? and must ArgumentError, not NoMethodError.
274
+ def initialize(bytes)
275
+ bytes = bytes.to_str if defined?(bytes.to_str) && !bytes.is_a?(String)
276
+
277
+ case bytes
278
+ when String
279
+ unless bytes.bytesize == 16
280
+ raise ArgumentError, "a 16-byte binary String is required (got #{bytes.bytesize} bytes)"
281
+ end
282
+ else
283
+ got = defined?(bytes.class) ? bytes.class : "a classless object"
284
+ raise ArgumentError, "a 16-byte binary String is required (got #{got})"
285
+ end
286
+
287
+ @bytes = bytes.b.freeze
288
+ @string = Digest.hexencode(@bytes).insert(8, "-").insert(13, "-").insert(18, "-").insert(23, "-").freeze
289
+ freeze
290
+ end
291
+
292
+ def version
293
+ @bytes.getbyte(6) >> 4
294
+ end
295
+
296
+ def variant
297
+ case @bytes.getbyte(8) >> 5
298
+ when 0b100, 0b101 then :rfc9562
299
+ when 0b110 then :microsoft
300
+ when 0b111 then :future
301
+ else :ncs
302
+ end
303
+ end
304
+
305
+ def timestamp
306
+ case version
307
+ when 1
308
+ # time_low, time_mid, then time_hi_and_version on the wire.
309
+ gregorian(((@bytes[6, 2].unpack1("n") & 0x0FFF) << 48) | (@bytes[4, 2].unpack1("n") << 32) |
310
+ @bytes[0, 4].unpack1("N"))
311
+ when 6
312
+ # time_high, time_mid, time_low in descending order of significance.
313
+ gregorian((@bytes[0, 4].unpack1("N") << 28) | (@bytes[4, 2].unpack1("n") << 12) |
314
+ (@bytes[6, 2].unpack1("n") & 0x0FFF))
315
+ when 7
316
+ milliseconds = @bytes[0, 8].unpack1("Q>") >> 16
317
+ Time.at(milliseconds / 1000, (milliseconds % 1000) * 1_000_000, :nanosecond).utc
318
+ end
319
+ end
320
+
321
+ attr_reader :bytes
322
+
323
+ def to_s
324
+ @string
325
+ end
326
+
327
+ alias to_str to_s
328
+
329
+ def hex
330
+ Digest.hexencode(@bytes)
331
+ end
332
+
333
+ def urn
334
+ "urn:uuid:#{@string}"
335
+ end
336
+
337
+ def nil_uuid?
338
+ self == NIL
339
+ end
340
+
341
+ def max_uuid?
342
+ self == MAX
343
+ end
344
+
345
+ def ==(other)
346
+ other.is_a?(UUID) && other.bytes == bytes
347
+ end
348
+
349
+ alias eql? ==
350
+
351
+ def hash
352
+ bytes.hash
353
+ end
354
+
355
+ # Opaque unsigned 128-bit byte order: v6 and v7 sort chronologically as
356
+ # raw bytes; v1 does not, its time_low sits first.
357
+ def <=>(other)
358
+ bytes <=> other.bytes if other.is_a?(UUID)
359
+ end
360
+
361
+ private
362
+
363
+ def gregorian(ticks)
364
+ unix_ticks = ticks - GREGORIAN_OFFSET
365
+ Time.at(unix_ticks / 10_000_000, (unix_ticks % 10_000_000) * 100, :nanosecond).utc
366
+ end
367
+ end
368
+ end
369
+
370
+ Fast::UUID::NIL = Fast::UUID.new(("\0" * 16).b)
371
+ Fast::UUID::MAX = Fast::UUID.new(("\xFF" * 16).b)
372
+ Fast::UUID::NAMESPACE_DNS = Fast::UUID.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
373
+ Fast::UUID::NAMESPACE_URL = Fast::UUID.parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
374
+ Fast::UUID::NAMESPACE_OID = Fast::UUID.parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
375
+ Fast::UUID::NAMESPACE_X500 = Fast::UUID.parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fast
4
+ class UUID
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/fast/uuid.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "uuid/version"
4
+ require_relative "uuid/errors"
5
+ require_relative "uuid/uuid"
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast-uuid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eric Jacobs
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ executables: []
13
+ extensions: []
14
+ extra_rdoc_files: []
15
+ files:
16
+ - CHANGELOG.md
17
+ - README.md
18
+ - lib/fast/uuid.rb
19
+ - lib/fast/uuid/errors.rb
20
+ - lib/fast/uuid/uuid.rb
21
+ - lib/fast/uuid/version.rb
22
+ homepage: https://github.com/jetpks/fast-uuid
23
+ licenses:
24
+ - MIT
25
+ metadata:
26
+ homepage_uri: https://github.com/jetpks/fast-uuid
27
+ source_code_uri: https://github.com/jetpks/fast-uuid
28
+ changelog_uri: https://github.com/jetpks/fast-uuid/blob/main/CHANGELOG.md
29
+ rubygems_mfa_required: 'true'
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '3.3'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 4.0.20
45
+ specification_version: 4
46
+ summary: 'RFC 9562 UUIDs for Ruby: generate v4/v7, parse every version, compare, no
47
+ dependencies.'
48
+ test_files: []