ruby-ulid 0.7.0 → 0.9.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 +86 -98
- data/lib/ulid/crockford_base32.rb +12 -12
- data/lib/ulid/errors.rb +1 -0
- data/lib/ulid/monotonic_generator.rb +15 -15
- data/lib/ulid/utils.rb +6 -24
- data/lib/ulid/uuid/fields.rb +41 -0
- data/lib/ulid/uuid.rb +32 -31
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +130 -134
- data/sig/ulid.rbs +149 -88
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c0ca476ab05b4b53ce593e893ffdfd6aa72b3c08563116b95f5a22f43a7926b
|
4
|
+
data.tar.gz: 7454acd393c8df299696d2907a71f994643dacc45103e2874561d186e1fa85c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bfe19e20452a12edd7472ddaf3b9583d2eaa5640e03a081621c35bcfdf218f9ccf5d7c7625b0850312447f61c0592796d333aa4578a6c2f0281e6ff4e8392efd
|
7
|
+
data.tar.gz: cdd8dba77214852ea70d07a137f75098558097db686a4409dd14e611d533de33f4c510b9a38ee232e918dd0aae6b028f19a4dd7312ac42172890d726091f282b
|
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,33 @@ 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.2 or higher.
|
41
42
|
|
42
|
-
|
43
|
+
Add this line to your `Gemfile`.
|
43
44
|
|
44
|
-
```
|
45
|
-
|
46
|
-
Should be installed!
|
45
|
+
```ruby
|
46
|
+
gem('ruby-ulid', '~> 0.9.0')
|
47
47
|
```
|
48
48
|
|
49
|
-
|
49
|
+
And load it.
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
|
52
|
+
require 'ulid'
|
53
53
|
```
|
54
54
|
|
55
|
-
|
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.9.0).
|
56
57
|
|
57
|
-
|
58
|
-
require 'ulid'
|
58
|
+
In [Nix](https://nixos.org/), you can skip the installation steps for both ruby and ruby-ulid to try.
|
59
59
|
|
60
|
-
|
61
|
-
#
|
62
|
-
|
60
|
+
```console
|
61
|
+
> nix run github:kachick/ruby-ulid#ruby -- -e 'p ULID.generate'
|
62
|
+
ULID(2024-03-03 18:37:06.152 UTC: 01HR2SNY789ZZ027EDJEHAGQ62)
|
63
63
|
|
64
|
-
|
64
|
+
> nix run github:kachick/ruby-ulid#irb
|
65
|
+
irb(main):001:0> ULID.parse('01H66XG2A9WWYRCYGPA62T4AZA')
|
66
|
+
=> ULID(2023-07-25 16:18:12.937 UTC: 01H66XG2A9WWYRCYGPA62T4AZA)
|
67
|
+
```
|
65
68
|
|
66
69
|
### Generator and Parser
|
67
70
|
|
@@ -77,7 +80,7 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
|
|
77
80
|
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
78
81
|
```
|
79
82
|
|
80
|
-
It
|
83
|
+
It has inspector methods.
|
81
84
|
|
82
85
|
```ruby
|
83
86
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
@@ -118,15 +121,15 @@ ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1', in: '+09:00') #=> 2000-01-01 09:0
|
|
118
121
|
|
119
122
|
This project does not prioritize on the speed. However it actually works faster than others! :zap:
|
120
123
|
|
121
|
-
Snapshot on
|
124
|
+
Snapshot on v0.8.0 with Ruby 3.2.1 is below
|
122
125
|
|
123
|
-
- Generator is 1.
|
124
|
-
- Generator is
|
125
|
-
- Parser is
|
126
|
+
- Generator is 1.9x faster than - [ulid gem - v1.4.0](https://github.com/rafaelsales/ulid)
|
127
|
+
- Generator is 2.0x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby)
|
128
|
+
- Parser is 3.1x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby)
|
126
129
|
|
127
130
|
You can see further detail at [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
|
128
131
|
|
129
|
-
### Sortable
|
132
|
+
### Sortable by timestamp
|
130
133
|
|
131
134
|
ULIDs are sortable when they are generated in different timestamp with milliseconds precision.
|
132
135
|
|
@@ -139,7 +142,7 @@ ulids.uniq(&:to_time).size #=> 1000
|
|
139
142
|
ulids.sort == ulids #=> true
|
140
143
|
```
|
141
144
|
|
142
|
-
|
145
|
+
The basic generator prefers `randomness`, the results in the same milliseconds are not sortable.
|
143
146
|
|
144
147
|
```ruby
|
145
148
|
ulids = 10000.times.map do
|
@@ -151,8 +154,9 @@ ulids.sort == ulids #=> false
|
|
151
154
|
|
152
155
|
### How to keep `Sortable` even if in same timestamp
|
153
156
|
|
154
|
-
If you
|
155
|
-
|
157
|
+
If you prefer `sortability`, you can use `MonotonicGenerator` instead.\
|
158
|
+
It is referred to as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) in the spec.\
|
159
|
+
(Although it starts with a new random value when the timestamp is changed)
|
156
160
|
|
157
161
|
```ruby
|
158
162
|
monotonic_generator = ULID::MonotonicGenerator.new
|
@@ -163,20 +167,16 @@ sample_ulids_by_the_time = ulids.uniq(&:to_time)
|
|
163
167
|
sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
|
164
168
|
|
165
169
|
# In same milliseconds creation, it just increments the end of randomness part
|
166
|
-
ulids.take(
|
170
|
+
ulids.take(3) #=>
|
167
171
|
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
168
172
|
# 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)]
|
173
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6)]
|
172
174
|
|
173
175
|
# When the milliseconds is updated, it starts with new randomness
|
174
|
-
sample_ulids_by_the_time.take(
|
176
|
+
sample_ulids_by_the_time.take(3) #=>
|
175
177
|
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
176
178
|
# 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)]
|
179
|
+
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K)]
|
180
180
|
|
181
181
|
ulids.sort == ulids #=> true
|
182
182
|
```
|
@@ -265,18 +265,6 @@ ULID.scan(json).to_a
|
|
265
265
|
# ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
266
266
|
```
|
267
267
|
|
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
268
|
#### Get boundary ULIDs
|
281
269
|
|
282
270
|
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
@@ -292,7 +280,7 @@ ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
|
|
292
280
|
ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
293
281
|
```
|
294
282
|
|
295
|
-
#### As element in Enumerable
|
283
|
+
#### As an element in Enumerable and Range
|
296
284
|
|
297
285
|
`ULID#next` and `ULID#succ` returns next(successor) ULID.\
|
298
286
|
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
@@ -313,6 +301,17 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
|
|
313
301
|
ULID.parse('00000000000000000000000000').pred #=> nil
|
314
302
|
```
|
315
303
|
|
304
|
+
`ULID#+` is also provided to realize `Range#step` since [ruby-3.4.0 spec changes](https://bugs.ruby-lang.org/issues/18368).
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
# This code works only in ruby-3.4.0dev or later
|
308
|
+
(ULID.min...).step(42).take(3)
|
309
|
+
# =>
|
310
|
+
[ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000),
|
311
|
+
ULID(1970-01-01 00:00:00.000 UTC: 0000000000000000000000001A),
|
312
|
+
ULID(1970-01-01 00:00:00.000 UTC: 0000000000000000000000002M)]
|
313
|
+
```
|
314
|
+
|
316
315
|
#### Test helpers
|
317
316
|
|
318
317
|
`ULID.sample` returns random ULIDs.
|
@@ -324,13 +323,11 @@ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
|
324
323
|
ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
|
325
324
|
ULID.sample(0) #=> []
|
326
325
|
ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
|
327
|
-
ULID.sample(
|
326
|
+
ULID.sample(3)
|
328
327
|
#=>
|
329
328
|
#[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
|
330
329
|
# 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)]
|
330
|
+
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0)]
|
334
331
|
```
|
335
332
|
|
336
333
|
You can specify a range object for the timestamp restriction, see also `ULID.range`.
|
@@ -338,22 +335,16 @@ You can specify a range object for the timestamp restriction, see also `ULID.ran
|
|
338
335
|
```ruby
|
339
336
|
ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
340
337
|
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)
|
338
|
+
ulids = ULID.sample(3, period: ulid1..ulid2)
|
344
339
|
#=>
|
345
340
|
#[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
|
346
341
|
# 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)
|
342
|
+
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW)]
|
343
|
+
ULID.sample(3, period: ulid1.to_time..ulid2.to_time)
|
351
344
|
#=>
|
352
345
|
# [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
|
353
346
|
# 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)]
|
347
|
+
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW)]
|
357
348
|
```
|
358
349
|
|
359
350
|
#### Variants of format
|
@@ -385,60 +376,57 @@ ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true
|
|
385
376
|
ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
|
386
377
|
```
|
387
378
|
|
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"
|
379
|
+
#### UUID
|
400
380
|
|
401
|
-
|
402
|
-
uuid_v4s.uniq.size == 10000 #=> Probably `true`
|
381
|
+
Both ULID and UUID are 128-bit IDs. But with different specs. Especially UUID has some versions probably UUIDv4.
|
403
382
|
|
404
|
-
|
405
|
-
|
383
|
+
All UUIDv4s can be converted to ULID, but this will not have the correct "timestamp".\
|
384
|
+
Most ULIDs cannot be converted to UUIDv4 while maintaining reversibility, because UUIDv4 requires version and variants in the fields.
|
406
385
|
|
407
|
-
|
386
|
+
See also [ulid/spec#64](https://github.com/ulid/spec/issues/64) for further detail.
|
408
387
|
|
409
|
-
|
410
|
-
ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
388
|
+
For now, this gem provides 4 methods for UUIDs.
|
411
389
|
|
412
|
-
|
413
|
-
|
414
|
-
reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
|
390
|
+
- Reversibility is preferred: `ULID.from_uuidish`, `ULID.to_uuidish`
|
391
|
+
- Prefer UUIDv4 specification: `ULID.from_uuidv4`, `ULID.to_uuidv4`
|
415
392
|
|
416
|
-
|
417
|
-
|
418
|
-
|
393
|
+
```ruby
|
394
|
+
# All UUIDv4 IDs can be reversible even if converted to ULID
|
395
|
+
uuid = SecureRandom.uuid
|
396
|
+
ULID.from_uuidish(uuid) == ULID.from_uuidv4(uuid) #=> true
|
397
|
+
ULID.from_uuidish(uuid).to_uuidish == ULID.from_uuidv4(uuid).to_uuidv4 #=> true
|
398
|
+
|
399
|
+
# But most ULIDs cannot be converted to UUIDv4
|
400
|
+
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA')
|
401
|
+
ulid.to_uuidv4 #=> ULID::IrreversibleUUIDError
|
402
|
+
# So 2 ways to get substitute strings that might satisfy the use case
|
403
|
+
ulid.to_uuidv4(force: true) #=> "0179145f-07ca-4b3c-af33-4c3c3149254a" this cannot be reverse to source ULID
|
404
|
+
ulid == ULID.from_uuidv4(ulid.to_uuidv4(force: true)) #=> false
|
405
|
+
ulid.to_uuidish #=> "0179145f-07ca-bb3c-af33-4c3c3149254a" does not satisfy UUIDv4 spec
|
406
|
+
ulid == ULID.from_uuidish(ulid.to_uuidish) #=> true
|
407
|
+
|
408
|
+
# Seeing boundary IDs makes it easier to understand
|
409
|
+
ULID.min.to_uuidish #=> "00000000-0000-0000-0000-000000000000"
|
410
|
+
ULID.min.to_uuidv4(force: true) #=> "00000000-0000-4000-8000-000000000000"
|
411
|
+
ULID.max.to_uuidish #=> "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
412
|
+
ULID.max.to_uuidv4(force: true) #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
419
413
|
```
|
420
414
|
|
415
|
+
[UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-02.html) are other candidates for sortable and randomness ID.\
|
416
|
+
Latest [ruby/securerandom merged the UUIDv7 generator](https://github.com/ruby/securerandom/pull/19).\
|
417
|
+
See [tracker](https://bugs.ruby-lang.org/issues/19735) for further detail.
|
418
|
+
|
421
419
|
## Migration from other gems
|
422
420
|
|
423
421
|
See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-migration).
|
424
422
|
|
425
423
|
## RBS
|
426
424
|
|
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
|
-
- 
|
425
|
+
- Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
|
426
|
+
- See the overview in [our wiki page for RBS](https://github.com/kachick/ruby-ulid/wiki/RBS)
|
434
427
|
|
435
428
|
## References
|
436
429
|
|
437
430
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
438
431
|
- [API documents](https://kachick.github.io/ruby-ulid/)
|
439
432
|
- [ulid/spec](https://github.com/ulid/spec)
|
440
|
-
|
441
|
-
## Note
|
442
|
-
|
443
|
-
- [UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html) is another choice for sortable and randomness ID.\
|
444
|
-
However they are stayed in draft state. ref: [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
@@ -10,23 +10,23 @@ require_relative('utils')
|
|
10
10
|
class ULID
|
11
11
|
class MonotonicGenerator
|
12
12
|
# @note When use https://github.com/ko1/ractor-tvar might realize Ractor based thread safe monotonic generator.
|
13
|
-
# However it is a C
|
13
|
+
# However it is a C extension, I'm pending to use it for now.
|
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)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# shareable_constant_value: literal
|
4
|
+
|
5
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
6
|
+
|
7
|
+
class ULID
|
8
|
+
module UUID
|
9
|
+
# @see https://www.rfc-editor.org/rfc/rfc4122#section-4.1.2
|
10
|
+
# @note
|
11
|
+
# - Using `Fields = Data.define do; end` syntax made https://github.com/kachick/ruby-ulid/issues/233 again. So use class syntax instead
|
12
|
+
# - This file is extracted to avoid YARD warnings "Ruby::ClassHandler: Undocumentable superclass" https://github.com/lsegal/yard/issues/737
|
13
|
+
# Partially avoiding is hard in YARD, so extracting the code and using exclude filter in yardopts...
|
14
|
+
class Fields < Data.define(:time_low, :time_mid, :time_hi_and_version, :clock_seq_hi_and_res, :clk_seq_low, :node)
|
15
|
+
def self.raw_from_octets(octets)
|
16
|
+
case octets.pack('C*').unpack('NnnnnN')
|
17
|
+
in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node]
|
18
|
+
new(time_low:, time_mid:, time_hi_and_version:, clock_seq_hi_and_res:, clk_seq_low:, node:).freeze
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.forced_v4_from_octets(octets)
|
23
|
+
case octets.pack('C*').unpack('NnnnnN')
|
24
|
+
in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node]
|
25
|
+
new(
|
26
|
+
time_low:,
|
27
|
+
time_mid:,
|
28
|
+
time_hi_and_version: (time_hi_and_version & 0x0fff) | 0x4000,
|
29
|
+
clock_seq_hi_and_res: (clock_seq_hi_and_res & 0x3fff) | 0x8000,
|
30
|
+
clk_seq_low:,
|
31
|
+
node:
|
32
|
+
).freeze
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % deconstruct
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|