ruby-ulid 0.7.0 → 0.8.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 +4 -4
- data/README.md +63 -95
- data/lib/ulid/crockford_base32.rb +12 -12
- data/lib/ulid/errors.rb +1 -0
- data/lib/ulid/monotonic_generator.rb +14 -14
- data/lib/ulid/utils.rb +6 -24
- data/lib/ulid/uuid.rb +61 -31
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +102 -116
- data/sig/ulid.rbs +137 -88
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 323b750a4e11157bd492b31d74833df2042192775c8627917880ef386dee4c1c
|
4
|
+
data.tar.gz: 2dc8a91cbed7b473d6f9e28480dd227ab3469828ab0833f48a754111bd6e926c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e78a5289863c1300a3f4c186b72a0f1d97cd709ce6043b69729d851f5e6ffcedac001e3bb691397d761ee497d609b5292c300db7f2ec3f8cf5be0ee5c095af80
|
7
|
+
data.tar.gz: e19197709d7a896a79c6ebdc40fa919c2e7876284fa1dbf8895498ad7b80e989634cdd3e651362db5cf9f7c627fe17439ccc21e75c8e6598b002da6b19858aa0
|
data/README.md
CHANGED
@@ -5,9 +5,10 @@
|
|
5
5
|
|
6
6
|
## Overview
|
7
7
|
|
8
|
-
[ulid/spec](https://github.com/ulid/spec)
|
9
|
-
|
10
|
-
This gem aims to provide the generator, optional monotonicity, parser and other
|
8
|
+
[ulid/spec](https://github.com/ulid/spec) defines some useful features.\
|
9
|
+
In particular, it has uniqueness, randomness, extractable timestamps, and sortability.\
|
10
|
+
This gem aims to provide the generator, optional monotonicity, parser, and other manipulations around ULID.\
|
11
|
+
[RBS](https://github.com/ruby/rbs) definitions are also included.
|
11
12
|
|
12
13
|
---
|
13
14
|
|
@@ -37,31 +38,22 @@ Instead, herein is proposed ULID:
|
|
37
38
|
|
38
39
|
### Install
|
39
40
|
|
40
|
-
|
41
|
+
Tested only in the last 2 Rubies. So you need Ruby 3.1 or higher.
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
```console
|
45
|
-
$ gem install ruby-ulid
|
46
|
-
Should be installed!
|
47
|
-
```
|
48
|
-
|
49
|
-
Add this line in your Gemfile.
|
43
|
+
Add this line to your `Gemfile`.
|
50
44
|
|
51
45
|
```ruby
|
52
|
-
gem('ruby-ulid', '~> 0.
|
46
|
+
gem('ruby-ulid', '~> 0.8.0')
|
53
47
|
```
|
54
48
|
|
55
|
-
|
49
|
+
And load it.
|
56
50
|
|
57
51
|
```ruby
|
58
52
|
require 'ulid'
|
59
|
-
|
60
|
-
ULID::VERSION
|
61
|
-
# => "0.7.0"
|
62
53
|
```
|
63
54
|
|
64
|
-
NOTE: This README
|
55
|
+
NOTE: This README contains information about the development version.\
|
56
|
+
If you would like to see released version's one. [Look at the ref](https://github.com/kachick/ruby-ulid/tree/v0.8.0).
|
65
57
|
|
66
58
|
### Generator and Parser
|
67
59
|
|
@@ -77,7 +69,7 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
|
|
77
69
|
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
78
70
|
```
|
79
71
|
|
80
|
-
It
|
72
|
+
It has inspector methods.
|
81
73
|
|
82
74
|
```ruby
|
83
75
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
@@ -118,15 +110,15 @@ ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1', in: '+09:00') #=> 2000-01-01 09:0
|
|
118
110
|
|
119
111
|
This project does not prioritize on the speed. However it actually works faster than others! :zap:
|
120
112
|
|
121
|
-
Snapshot on
|
113
|
+
Snapshot on v0.8.0 with Ruby 3.2.1 is below
|
122
114
|
|
123
|
-
- Generator is 1.
|
124
|
-
- Generator is
|
125
|
-
- Parser is
|
115
|
+
- Generator is 1.9x faster than - [ulid gem - v1.4.0](https://github.com/rafaelsales/ulid)
|
116
|
+
- Generator is 2.0x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby)
|
117
|
+
- Parser is 3.1x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby)
|
126
118
|
|
127
119
|
You can see further detail at [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
|
128
120
|
|
129
|
-
### Sortable
|
121
|
+
### Sortable by timestamp
|
130
122
|
|
131
123
|
ULIDs are sortable when they are generated in different timestamp with milliseconds precision.
|
132
124
|
|
@@ -139,7 +131,7 @@ ulids.uniq(&:to_time).size #=> 1000
|
|
139
131
|
ulids.sort == ulids #=> true
|
140
132
|
```
|
141
133
|
|
142
|
-
|
134
|
+
The basic generator prefers `randomness`, the results in the same milliseconds are not sortable.
|
143
135
|
|
144
136
|
```ruby
|
145
137
|
ulids = 10000.times.map do
|
@@ -151,8 +143,9 @@ ulids.sort == ulids #=> false
|
|
151
143
|
|
152
144
|
### How to keep `Sortable` even if in same timestamp
|
153
145
|
|
154
|
-
If you
|
155
|
-
|
146
|
+
If you prefer `sortability`, you can use `MonotonicGenerator` instead.\
|
147
|
+
It is referred to as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) in the spec.\
|
148
|
+
(Although it starts with a new random value when the timestamp is changed)
|
156
149
|
|
157
150
|
```ruby
|
158
151
|
monotonic_generator = ULID::MonotonicGenerator.new
|
@@ -163,20 +156,16 @@ sample_ulids_by_the_time = ulids.uniq(&:to_time)
|
|
163
156
|
sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
|
164
157
|
|
165
158
|
# In same milliseconds creation, it just increments the end of randomness part
|
166
|
-
ulids.take(
|
159
|
+
ulids.take(3) #=>
|
167
160
|
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
168
161
|
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
|
169
|
-
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6)
|
170
|
-
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
|
171
|
-
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
|
162
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6)]
|
172
163
|
|
173
164
|
# When the milliseconds is updated, it starts with new randomness
|
174
|
-
sample_ulids_by_the_time.take(
|
165
|
+
sample_ulids_by_the_time.take(3) #=>
|
175
166
|
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
176
167
|
# ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
|
177
|
-
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K)
|
178
|
-
# ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
|
179
|
-
# ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
|
168
|
+
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K)]
|
180
169
|
|
181
170
|
ulids.sort == ulids #=> true
|
182
171
|
```
|
@@ -265,18 +254,6 @@ ULID.scan(json).to_a
|
|
265
254
|
# ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
266
255
|
```
|
267
256
|
|
268
|
-
`ULID#patterns` is a util for text based operations.
|
269
|
-
The results and spec are not fixed. Should not be used except snippets/console operation.
|
270
|
-
|
271
|
-
```ruby
|
272
|
-
ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
|
273
|
-
#=> returns like a fallowing Hash
|
274
|
-
{
|
275
|
-
named_captures: /(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)/i,
|
276
|
-
strict_named_captures: /\A(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)\z/i
|
277
|
-
}
|
278
|
-
```
|
279
|
-
|
280
257
|
#### Get boundary ULIDs
|
281
258
|
|
282
259
|
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
@@ -324,13 +301,11 @@ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
|
324
301
|
ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
|
325
302
|
ULID.sample(0) #=> []
|
326
303
|
ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
|
327
|
-
ULID.sample(
|
304
|
+
ULID.sample(3)
|
328
305
|
#=>
|
329
306
|
#[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
|
330
307
|
# ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
|
331
|
-
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0)
|
332
|
-
# ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
|
333
|
-
# ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
|
308
|
+
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0)]
|
334
309
|
```
|
335
310
|
|
336
311
|
You can specify a range object for the timestamp restriction, see also `ULID.range`.
|
@@ -338,22 +313,16 @@ You can specify a range object for the timestamp restriction, see also `ULID.ran
|
|
338
313
|
```ruby
|
339
314
|
ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
340
315
|
ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
|
341
|
-
ulids = ULID.sample(
|
342
|
-
ulids.uniq.size #=> 1000
|
343
|
-
ulids.take(5)
|
316
|
+
ulids = ULID.sample(3, period: ulid1..ulid2)
|
344
317
|
#=>
|
345
318
|
#[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
|
346
319
|
# ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
|
347
|
-
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW)
|
348
|
-
|
349
|
-
# ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3)]
|
350
|
-
ULID.sample(5, period: ulid1.to_time..ulid2.to_time)
|
320
|
+
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW)]
|
321
|
+
ULID.sample(3, period: ulid1.to_time..ulid2.to_time)
|
351
322
|
#=>
|
352
323
|
# [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
|
353
324
|
# ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
|
354
|
-
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW)
|
355
|
-
# ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
|
356
|
-
# ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G)]
|
325
|
+
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW)]
|
357
326
|
```
|
358
327
|
|
359
328
|
#### Variants of format
|
@@ -385,37 +354,40 @@ ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true
|
|
385
354
|
ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
|
386
355
|
```
|
387
356
|
|
388
|
-
####
|
389
|
-
|
390
|
-
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.\
|
391
|
-
The imported timestamp is meaningless. So ULID's benefit will lost.
|
392
|
-
|
393
|
-
```ruby
|
394
|
-
# Currently experimental feature, so needed to load the extension.
|
395
|
-
require 'ulid/uuid'
|
396
|
-
|
397
|
-
# Basically reversible
|
398
|
-
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
399
|
-
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
357
|
+
#### UUID
|
400
358
|
|
401
|
-
|
402
|
-
uuid_v4s.uniq.size == 10000 #=> Probably `true`
|
359
|
+
Both ULID and UUID are 128-bit IDs. But with different specs. Especially UUID has some versions probably UUIDv4.
|
403
360
|
|
404
|
-
|
405
|
-
|
361
|
+
All UUIDv4s can be converted to ULID, but this will not have the correct "timestamp".\
|
362
|
+
Most ULIDs cannot be converted to UUIDv4 while maintaining reversibility, because UUIDv4 requires version and variants in the fields.
|
406
363
|
|
407
|
-
|
364
|
+
See also [ulid/spec#64](https://github.com/ulid/spec/issues/64) for further detail.
|
408
365
|
|
409
|
-
|
410
|
-
ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
366
|
+
For now, this gem provides 4 methods for UUIDs.
|
411
367
|
|
412
|
-
|
413
|
-
|
414
|
-
reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
|
368
|
+
- Reversibility is preferred: `ULID.from_uuidish`, `ULID.to_uuidish`
|
369
|
+
- Prefer UUIDv4 specification: `ULID.from_uuidv4`, `ULID.to_uuidv4`
|
415
370
|
|
416
|
-
|
417
|
-
|
418
|
-
|
371
|
+
```ruby
|
372
|
+
# All UUIDv4 IDs can be reversible even if converted to ULID
|
373
|
+
uuid = SecureRandom.uuid
|
374
|
+
ULID.from_uuidish(uuid) == ULID.from_uuidv4(uuid) #=> true
|
375
|
+
ULID.from_uuidish(uuid).to_uuidish == ULID.from_uuidv4(uuid).to_uuidv4 #=> true
|
376
|
+
|
377
|
+
# But most ULIDs cannot be converted to UUIDv4
|
378
|
+
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA')
|
379
|
+
ulid.to_uuidv4 #=> ULID::IrreversibleUUIDError
|
380
|
+
# So 2 ways to get substitute strings that might satisfy the use case
|
381
|
+
ulid.to_uuidv4(force: true) #=> "0179145f-07ca-4b3c-af33-4c3c3149254a" this cannot be reverse to source ULID
|
382
|
+
ulid == ULID.from_uuidv4(ulid.to_uuidv4(force: true)) #=> false
|
383
|
+
ulid.to_uuidish #=> "0179145f-07ca-bb3c-af33-4c3c3149254a" does not satisfy UUIDv4 spec
|
384
|
+
ulid == ULID.from_uuidish(ulid.to_uuidish) #=> true
|
385
|
+
|
386
|
+
# Seeing boundary IDs makes it easier to understand
|
387
|
+
ULID.min.to_uuidish #=> "00000000-0000-0000-0000-000000000000"
|
388
|
+
ULID.min.to_uuidv4(force: true) #=> "00000000-0000-4000-8000-000000000000"
|
389
|
+
ULID.max.to_uuidish #=> "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
390
|
+
ULID.max.to_uuidv4(force: true) #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
419
391
|
```
|
420
392
|
|
421
393
|
## Migration from other gems
|
@@ -424,13 +396,8 @@ See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-
|
|
424
396
|
|
425
397
|
## RBS
|
426
398
|
|
427
|
-
Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
|
428
|
-
|
429
|
-
I have checked the behavior with [ruby/rbs@2.6.0](https://github.com/ruby/rbs) + [soutaro/steep@1.0.1](https://github.com/soutaro/steep) + [soutaro/steep-vscode](https://github.com/soutaro/steep-vscode).
|
430
|
-
|
431
|
-
- 
|
432
|
-
- 
|
433
|
-
- 
|
399
|
+
- Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
|
400
|
+
- See the overview in [our wiki page for RBS](https://github.com/kachick/ruby-ulid/wiki/RBS)
|
434
401
|
|
435
402
|
## References
|
436
403
|
|
@@ -440,5 +407,6 @@ I have checked the behavior with [ruby/rbs@2.6.0](https://github.com/ruby/rbs) +
|
|
440
407
|
|
441
408
|
## Note
|
442
409
|
|
443
|
-
- [UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-
|
444
|
-
|
410
|
+
- [UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-02.html) is another choice for sortable and randomness ID.
|
411
|
+
\
|
412
|
+
However they remain in draft state. Our tracker is: [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
@@ -6,7 +6,7 @@
|
|
6
6
|
require_relative('utils')
|
7
7
|
|
8
8
|
class ULID
|
9
|
-
# @see https://www.crockford.com/base32.html
|
9
|
+
# @see https://www.crockford.com/base32.html and https://www.rfc-editor.org/rfc/rfc4648
|
10
10
|
#
|
11
11
|
# This module supporting only `subset of original crockford for actual use-case` in ULID context.
|
12
12
|
# Original decoding spec allows other characters.
|
@@ -38,7 +38,7 @@ class ULID
|
|
38
38
|
}.freeze
|
39
39
|
|
40
40
|
# Excluded I, L, O, U, - from Base32
|
41
|
-
|
41
|
+
base32hex_to_crockford = {
|
42
42
|
'I' => 'J',
|
43
43
|
'J' => 'K',
|
44
44
|
'K' => 'M',
|
@@ -54,8 +54,8 @@ class ULID
|
|
54
54
|
'U' => 'Y',
|
55
55
|
'V' => 'Z'
|
56
56
|
}.freeze
|
57
|
-
|
58
|
-
CROCKFORD_TR_PATTERN =
|
57
|
+
BASE32HEX_TR_PATTERN = base32hex_to_crockford.keys.join.freeze
|
58
|
+
CROCKFORD_TR_PATTERN = base32hex_to_crockford.values.join.freeze
|
59
59
|
ENCODING_STRING = "#{same_definitions.values.join}#{CROCKFORD_TR_PATTERN}".freeze
|
60
60
|
|
61
61
|
variant_to_normarized = {
|
@@ -69,22 +69,22 @@ class ULID
|
|
69
69
|
VARIANT_TR_PATTERN = variant_to_normarized.keys.join.freeze
|
70
70
|
NORMALIZED_TR_PATTERN = variant_to_normarized.values.join.freeze
|
71
71
|
|
72
|
-
Utils.
|
72
|
+
Utils.make_sharable_constants(self)
|
73
73
|
|
74
74
|
# @note Avoid to depend regex as possible. `tr(string, string)` is almost 2x Faster than `gsub(regex, hash)` in Ruby 3.1
|
75
75
|
|
76
76
|
# @param [String] string
|
77
77
|
# @return [Integer]
|
78
78
|
def self.decode(string)
|
79
|
-
|
80
|
-
Integer(
|
79
|
+
base32hex = string.upcase.tr(CROCKFORD_TR_PATTERN, BASE32HEX_TR_PATTERN)
|
80
|
+
Integer(base32hex, 32, exception: true)
|
81
81
|
end
|
82
82
|
|
83
83
|
# @param [Integer] integer
|
84
84
|
# @return [String]
|
85
85
|
def self.encode(integer)
|
86
|
-
|
87
|
-
|
86
|
+
base32hex = integer.to_s(32)
|
87
|
+
from_base32hex(base32hex).rjust(ENCODED_LENGTH, '0')
|
88
88
|
end
|
89
89
|
|
90
90
|
# @param [String] string
|
@@ -93,10 +93,10 @@ class ULID
|
|
93
93
|
string.delete('-').tr(VARIANT_TR_PATTERN, NORMALIZED_TR_PATTERN)
|
94
94
|
end
|
95
95
|
|
96
|
-
# @param [String]
|
96
|
+
# @param [String] base32hex
|
97
97
|
# @return [String]
|
98
|
-
def self.
|
99
|
-
|
98
|
+
def self.from_base32hex(base32hex)
|
99
|
+
base32hex.upcase.tr(BASE32HEX_TR_PATTERN, CROCKFORD_TR_PATTERN)
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
data/lib/ulid/errors.rb
CHANGED
@@ -14,19 +14,19 @@ class ULID
|
|
14
14
|
include(MonitorMixin)
|
15
15
|
|
16
16
|
# @return [ULID, nil]
|
17
|
-
attr_accessor(:
|
18
|
-
private(:
|
17
|
+
attr_accessor(:last)
|
18
|
+
private(:last=)
|
19
19
|
|
20
20
|
undef_method(:instance_variable_set)
|
21
21
|
|
22
22
|
def initialize
|
23
23
|
super
|
24
|
-
@
|
24
|
+
@last = nil
|
25
25
|
end
|
26
26
|
|
27
27
|
# @return [String]
|
28
28
|
def inspect
|
29
|
-
"ULID::MonotonicGenerator(
|
29
|
+
"ULID::MonotonicGenerator(last: #{@last.inspect})"
|
30
30
|
end
|
31
31
|
alias_method(:to_s, :inspect)
|
32
32
|
|
@@ -37,25 +37,25 @@ class ULID
|
|
37
37
|
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
38
38
|
def generate(moment: Utils.current_milliseconds)
|
39
39
|
synchronize do
|
40
|
-
|
41
|
-
unless
|
42
|
-
ret = ULID.generate(moment:
|
43
|
-
@
|
40
|
+
prev = @last
|
41
|
+
unless prev
|
42
|
+
ret = ULID.generate(moment:)
|
43
|
+
@last = ret
|
44
44
|
return ret
|
45
45
|
end
|
46
46
|
|
47
47
|
milliseconds = Utils.milliseconds_from_moment(moment)
|
48
48
|
|
49
49
|
ulid = (
|
50
|
-
if
|
50
|
+
if prev.milliseconds < milliseconds
|
51
51
|
ULID.generate(moment: milliseconds)
|
52
52
|
else
|
53
|
-
ULID.generate(moment:
|
53
|
+
ULID.generate(moment: prev.milliseconds, entropy: prev.entropy.succ)
|
54
54
|
end
|
55
55
|
)
|
56
56
|
|
57
|
-
unless ulid >
|
58
|
-
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{
|
57
|
+
unless ulid > prev
|
58
|
+
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{prev.inspect}"
|
59
59
|
additional_information = (
|
60
60
|
if Thread.list == [Thread.main]
|
61
61
|
'# NOTE: looks single thread only exist'
|
@@ -67,7 +67,7 @@ class ULID
|
|
67
67
|
raise(UnexpectedError, base_message + additional_information)
|
68
68
|
end
|
69
69
|
|
70
|
-
@
|
70
|
+
@last = ulid
|
71
71
|
ulid
|
72
72
|
end
|
73
73
|
end
|
@@ -78,7 +78,7 @@ class ULID
|
|
78
78
|
# @param [Time, Integer] moment
|
79
79
|
# @return [String]
|
80
80
|
def encode(moment: Utils.current_milliseconds)
|
81
|
-
generate(moment:
|
81
|
+
generate(moment:).encode
|
82
82
|
end
|
83
83
|
|
84
84
|
undef_method(:freeze)
|
data/lib/ulid/utils.rb
CHANGED
@@ -46,15 +46,15 @@ class ULID
|
|
46
46
|
# @return [String]
|
47
47
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
48
48
|
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
49
|
-
def self.
|
49
|
+
def self.encode_base32hex(milliseconds:, entropy:)
|
50
50
|
raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
|
51
51
|
raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
|
52
52
|
raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
|
53
53
|
raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
"#{
|
55
|
+
base32hex_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
56
|
+
base32hex_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
57
|
+
"#{base32hex_timestamp}#{base32hex_randomness}"
|
58
58
|
end
|
59
59
|
|
60
60
|
# @param [BasicObject] object
|
@@ -86,31 +86,13 @@ class ULID
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
def self.make_sharable_value(value)
|
90
|
-
value.freeze
|
91
|
-
if defined?(Ractor)
|
92
|
-
case value
|
93
|
-
when ULID, Time
|
94
|
-
if ractor_can_make_shareable_time?
|
95
|
-
Ractor.make_shareable(value)
|
96
|
-
end
|
97
|
-
else
|
98
|
-
Ractor.make_shareable(value)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
89
|
# @note Call before Module#private_constant
|
104
|
-
def self.
|
90
|
+
def self.make_sharable_constants(mod)
|
105
91
|
mod.constants.each do |const_name|
|
106
92
|
value = mod.const_get(const_name)
|
107
|
-
|
93
|
+
Ractor.make_shareable(value)
|
108
94
|
end
|
109
95
|
end
|
110
|
-
|
111
|
-
def self.ractor_can_make_shareable_time?
|
112
|
-
RUBY_VERSION >= '3.1'
|
113
|
-
end
|
114
96
|
end
|
115
97
|
|
116
98
|
private_constant(:Utils)
|
data/lib/ulid/uuid.rb
CHANGED
@@ -1,45 +1,75 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
+
# shareable_constant_value: literal
|
3
4
|
|
4
5
|
# Copyright (C) 2021 Kenichi Kamiya
|
5
6
|
|
6
7
|
require_relative('errors')
|
8
|
+
require_relative('utils')
|
7
9
|
|
8
|
-
# Extracted features around UUID from some reasons
|
9
|
-
# ref:
|
10
|
-
# * https://github.com/kachick/ruby-ulid/issues/105
|
11
|
-
# * https://github.com/kachick/ruby-ulid/issues/76
|
12
10
|
class ULID
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
11
|
+
module UUID
|
12
|
+
BASE_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\z/i
|
13
|
+
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
14
|
+
V4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i
|
15
|
+
|
16
|
+
def self.parse_any_to_int(uuidish)
|
17
|
+
encoded = String.try_convert(uuidish)
|
18
|
+
raise(ArgumentError, 'should pass a string for UUID parser') unless encoded
|
19
|
+
|
20
|
+
prefix_trimmed = encoded.delete_prefix('urn:uuid:')
|
21
|
+
unless BASE_PATTERN.match?(prefix_trimmed)
|
22
|
+
raise(ParserError, "given `#{encoded}` does not match to `#{BASE_PATTERN.inspect}`")
|
23
|
+
end
|
24
|
+
|
25
|
+
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
26
|
+
Integer(normalized, 16, exception: true)
|
28
27
|
end
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
def self.parse_v4_to_int(uuid)
|
30
|
+
encoded = String.try_convert(uuid)
|
31
|
+
raise(ArgumentError, 'should pass a string for UUID parser') unless encoded
|
32
|
+
|
33
|
+
prefix_trimmed = encoded.delete_prefix('urn:uuid:')
|
34
|
+
unless V4_PATTERN.match?(prefix_trimmed)
|
35
|
+
raise(ParserError, "given `#{encoded}` does not match to `#{V4_PATTERN.inspect}`")
|
36
|
+
end
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
37
|
-
array = octets.pack('C*').unpack('NnnnnN')
|
38
|
-
ref2, ref3 = array[2], array[3]
|
39
|
-
raise unless Integer === ref2 && Integer === ref3
|
38
|
+
parse_any_to_int(encoded)
|
39
|
+
end
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
# @see https://www.rfc-editor.org/rfc/rfc4122#section-4.1.2
|
42
|
+
# @todo Replace to Data class after dropped Ruby 3.1
|
43
|
+
# @note Using `Fields = Struct.new` syntax made https://github.com/kachick/ruby-ulid/issues/233 again. So use class syntax instead
|
44
|
+
class Fields < Struct.new(:time_low, :time_mid, :time_hi_and_version, :clock_seq_hi_and_res, :clk_seq_low, :node, keyword_init: true)
|
45
|
+
def self.raw_from_octets(octets)
|
46
|
+
case octets.pack('C*').unpack('NnnnnN')
|
47
|
+
in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node]
|
48
|
+
new(time_low:, time_mid:, time_hi_and_version:, clock_seq_hi_and_res:, clk_seq_low:, node:).freeze
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.forced_v4_from_octets(octets)
|
53
|
+
case octets.pack('C*').unpack('NnnnnN')
|
54
|
+
in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node]
|
55
|
+
new(
|
56
|
+
time_low:,
|
57
|
+
time_mid:,
|
58
|
+
time_hi_and_version: (time_hi_and_version & 0x0fff) | 0x4000,
|
59
|
+
clock_seq_hi_and_res: (clock_seq_hi_and_res & 0x3fff) | 0x8000,
|
60
|
+
clk_seq_low:,
|
61
|
+
node:
|
62
|
+
).freeze
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % values
|
68
|
+
end
|
69
|
+
end
|
44
70
|
end
|
71
|
+
|
72
|
+
Ractor.make_shareable(UUID)
|
73
|
+
|
74
|
+
private_constant(:UUID)
|
45
75
|
end
|
data/lib/ulid/version.rb
CHANGED