ruby-ulid 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
- ![rbs overview](./assets/ulid-rbs-overview.png?raw=true.png)
|
432
|
-
- ![rbs mix](./assets/ulid-rbs-mix.png?raw=true.png)
|
433
|
-
- ![rbs ng-to_str](./assets/ulid-rbs-ng-to_str.png?raw=true.png)
|
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