ruby-ulid 0.6.1 → 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 +74 -106
- data/lib/ulid/crockford_base32.rb +17 -16
- data/lib/ulid/errors.rb +1 -0
- data/lib/ulid/monotonic_generator.rb +23 -16
- data/lib/ulid/utils.rb +99 -0
- data/lib/ulid/uuid.rb +62 -30
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +115 -194
- data/sig/ulid.rbs +162 -117
- metadata +8 -8
- data/lib/ulid/ractor_unshareable_constants.rb +0 -12
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
@@ -1,13 +1,14 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
-
[](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml?query=branch%3Amain)
|
4
4
|
[](http://badge.fury.io/rb/ruby-ulid)
|
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.6.1"
|
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
|
@@ -99,9 +91,9 @@ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT
|
|
99
91
|
ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
|
100
92
|
```
|
101
93
|
|
102
|
-
Also `ULID.encode` and `ULID.decode_time` can be used to get primitive values for most usecases.
|
94
|
+
Also `ULID.encode` and `ULID.decode_time` can be used to get primitive values for most usecases.
|
103
95
|
|
104
|
-
`ULID.encode` returns [normalized](#variants-of-format) String without ULID object creation
|
96
|
+
`ULID.encode` returns [normalized](#variants-of-format) String without ULID object creation.\
|
105
97
|
It can take same arguments as `ULID.generate`.
|
106
98
|
|
107
99
|
```ruby
|
@@ -116,17 +108,17 @@ ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1') #=> 2000-01-01 00:00:00 UTC
|
|
116
108
|
ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1', in: '+09:00') #=> 2000-01-01 09:00:00 +0900
|
117
109
|
```
|
118
110
|
|
119
|
-
This project does not prioritize the speed. However it actually works faster than others! :zap:
|
111
|
+
This project does not prioritize on the speed. However it actually works faster than others! :zap:
|
120
112
|
|
121
|
-
Snapshot on 0.
|
113
|
+
Snapshot on v0.8.0 with Ruby 3.2.1 is below
|
122
114
|
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
```
|
@@ -195,7 +184,7 @@ ulids.grep(one_of_the_above)
|
|
195
184
|
ulids.grep_v(one_of_the_above)
|
196
185
|
```
|
197
186
|
|
198
|
-
When want to filter ULIDs with `Time`, we should consider to handle the precision
|
187
|
+
When want to filter ULIDs with `Time`, we should consider to handle the precision.\
|
199
188
|
So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
|
200
189
|
|
201
190
|
```ruby
|
@@ -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.
|
@@ -294,7 +271,7 @@ ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
|
294
271
|
|
295
272
|
#### As element in Enumerable
|
296
273
|
|
297
|
-
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
274
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID.\
|
298
275
|
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
299
276
|
|
300
277
|
NOTE: However basically `Range[ULID]#each` should not be used. Incrementing 128 bits IDs are not reasonable operation in most cases.
|
@@ -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,37 +313,31 @@ 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
|
360
329
|
|
361
330
|
I'm afraid so, we should consider [Current ULID spec](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#universally-unique-lexicographically-sortable-identifier) has `orthographical variants of the format` possibilities.
|
362
331
|
|
363
|
-
>Case insensitive
|
332
|
+
> Case insensitive
|
364
333
|
|
365
|
-
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase
|
334
|
+
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase.\
|
366
335
|
However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
|
367
336
|
|
368
|
-
>Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
337
|
+
> Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
369
338
|
|
370
|
-
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0
|
371
|
-
And accepts freestyle inserting `Hyphens (-)
|
339
|
+
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.\
|
340
|
+
And accepts freestyle inserting `Hyphens (-)`.\
|
372
341
|
To consider this patterns or not is different in each implementations.
|
373
342
|
|
374
343
|
I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57).
|
@@ -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)
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
-
# shareable_constant_value: literal
|
4
3
|
|
5
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
5
|
|
6
|
+
require_relative('utils')
|
7
|
+
|
7
8
|
class ULID
|
8
|
-
# @see https://www.crockford.com/base32.html
|
9
|
+
# @see https://www.crockford.com/base32.html and https://www.rfc-editor.org/rfc/rfc4648
|
9
10
|
#
|
10
11
|
# This module supporting only `subset of original crockford for actual use-case` in ULID context.
|
11
12
|
# Original decoding spec allows other characters.
|
@@ -37,7 +38,7 @@ class ULID
|
|
37
38
|
}.freeze
|
38
39
|
|
39
40
|
# Excluded I, L, O, U, - from Base32
|
40
|
-
|
41
|
+
base32hex_to_crockford = {
|
41
42
|
'I' => 'J',
|
42
43
|
'J' => 'K',
|
43
44
|
'K' => 'M',
|
@@ -53,8 +54,8 @@ class ULID
|
|
53
54
|
'U' => 'Y',
|
54
55
|
'V' => 'Z'
|
55
56
|
}.freeze
|
56
|
-
|
57
|
-
CROCKFORD_TR_PATTERN =
|
57
|
+
BASE32HEX_TR_PATTERN = base32hex_to_crockford.keys.join.freeze
|
58
|
+
CROCKFORD_TR_PATTERN = base32hex_to_crockford.values.join.freeze
|
58
59
|
ENCODING_STRING = "#{same_definitions.values.join}#{CROCKFORD_TR_PATTERN}".freeze
|
59
60
|
|
60
61
|
variant_to_normarized = {
|
@@ -68,36 +69,36 @@ class ULID
|
|
68
69
|
VARIANT_TR_PATTERN = variant_to_normarized.keys.join.freeze
|
69
70
|
NORMALIZED_TR_PATTERN = variant_to_normarized.values.join.freeze
|
70
71
|
|
72
|
+
Utils.make_sharable_constants(self)
|
73
|
+
|
71
74
|
# @note Avoid to depend regex as possible. `tr(string, string)` is almost 2x Faster than `gsub(regex, hash)` in Ruby 3.1
|
72
75
|
|
73
|
-
# @api private
|
74
76
|
# @param [String] string
|
75
77
|
# @return [Integer]
|
76
78
|
def self.decode(string)
|
77
|
-
|
78
|
-
|
79
|
+
base32hex = string.upcase.tr(CROCKFORD_TR_PATTERN, BASE32HEX_TR_PATTERN)
|
80
|
+
Integer(base32hex, 32, exception: true)
|
79
81
|
end
|
80
82
|
|
81
|
-
# @api private
|
82
83
|
# @param [Integer] integer
|
83
84
|
# @return [String]
|
84
85
|
def self.encode(integer)
|
85
|
-
|
86
|
-
|
86
|
+
base32hex = integer.to_s(32)
|
87
|
+
from_base32hex(base32hex).rjust(ENCODED_LENGTH, '0')
|
87
88
|
end
|
88
89
|
|
89
|
-
# @api private
|
90
90
|
# @param [String] string
|
91
91
|
# @return [String]
|
92
92
|
def self.normalize(string)
|
93
93
|
string.delete('-').tr(VARIANT_TR_PATTERN, NORMALIZED_TR_PATTERN)
|
94
94
|
end
|
95
95
|
|
96
|
-
# @
|
97
|
-
# @param [String] n32encoded
|
96
|
+
# @param [String] base32hex
|
98
97
|
# @return [String]
|
99
|
-
def self.
|
100
|
-
|
98
|
+
def self.from_base32hex(base32hex)
|
99
|
+
base32hex.upcase.tr(BASE32HEX_TR_PATTERN, CROCKFORD_TR_PATTERN)
|
101
100
|
end
|
102
101
|
end
|
102
|
+
|
103
|
+
private_constant(:CrockfordBase32)
|
103
104
|
end
|
data/lib/ulid/errors.rb
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
|
5
5
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
6
|
|
7
|
+
require_relative('errors')
|
8
|
+
require_relative('utils')
|
9
|
+
|
7
10
|
class ULID
|
8
11
|
class MonotonicGenerator
|
9
12
|
# @note When use https://github.com/ko1/ractor-tvar might realize Ractor based thread safe monotonic generator.
|
@@ -11,18 +14,19 @@ class ULID
|
|
11
14
|
include(MonitorMixin)
|
12
15
|
|
13
16
|
# @return [ULID, nil]
|
14
|
-
|
17
|
+
attr_accessor(:last)
|
18
|
+
private(:last=)
|
15
19
|
|
16
20
|
undef_method(:instance_variable_set)
|
17
21
|
|
18
22
|
def initialize
|
19
23
|
super
|
20
|
-
@
|
24
|
+
@last = nil
|
21
25
|
end
|
22
26
|
|
23
27
|
# @return [String]
|
24
28
|
def inspect
|
25
|
-
"ULID::MonotonicGenerator(
|
29
|
+
"ULID::MonotonicGenerator(last: #{@last.inspect})"
|
26
30
|
end
|
27
31
|
alias_method(:to_s, :inspect)
|
28
32
|
|
@@ -31,27 +35,27 @@ class ULID
|
|
31
35
|
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
32
36
|
# @raise [UnexpectedError] if the generated ULID is an invalid value in monotonicity spec.
|
33
37
|
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
34
|
-
def generate(moment:
|
38
|
+
def generate(moment: Utils.current_milliseconds)
|
35
39
|
synchronize do
|
36
|
-
|
37
|
-
unless
|
38
|
-
ret = ULID.generate(moment:
|
39
|
-
@
|
40
|
+
prev = @last
|
41
|
+
unless prev
|
42
|
+
ret = ULID.generate(moment:)
|
43
|
+
@last = ret
|
40
44
|
return ret
|
41
45
|
end
|
42
46
|
|
43
|
-
milliseconds =
|
47
|
+
milliseconds = Utils.milliseconds_from_moment(moment)
|
44
48
|
|
45
49
|
ulid = (
|
46
|
-
if
|
50
|
+
if prev.milliseconds < milliseconds
|
47
51
|
ULID.generate(moment: milliseconds)
|
48
52
|
else
|
49
|
-
ULID.
|
53
|
+
ULID.generate(moment: prev.milliseconds, entropy: prev.entropy.succ)
|
50
54
|
end
|
51
55
|
)
|
52
56
|
|
53
|
-
unless ulid >
|
54
|
-
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}"
|
55
59
|
additional_information = (
|
56
60
|
if Thread.list == [Thread.main]
|
57
61
|
'# NOTE: looks single thread only exist'
|
@@ -63,15 +67,18 @@ class ULID
|
|
63
67
|
raise(UnexpectedError, base_message + additional_information)
|
64
68
|
end
|
65
69
|
|
66
|
-
@
|
70
|
+
@last = ulid
|
67
71
|
ulid
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
75
|
+
# Just providing similar api as `ULID.generate` and `ULID.encode` relation. No performance benefit exists in monotonic generator's one.
|
76
|
+
#
|
77
|
+
# @see https://github.com/kachick/ruby-ulid/pull/220
|
71
78
|
# @param [Time, Integer] moment
|
72
79
|
# @return [String]
|
73
|
-
def encode(moment:
|
74
|
-
generate(moment:
|
80
|
+
def encode(moment: Utils.current_milliseconds)
|
81
|
+
generate(moment:).encode
|
75
82
|
end
|
76
83
|
|
77
84
|
undef_method(:freeze)
|
data/lib/ulid/utils.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# shareable_constant_value: literal
|
4
|
+
|
5
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
6
|
+
|
7
|
+
require('securerandom')
|
8
|
+
|
9
|
+
class ULID
|
10
|
+
# @note I don't have confidence for the naming of `Utils`. However some standard libraries have same name.
|
11
|
+
# https://github.com/ruby/webrick/blob/14612a7540fdd7373344461851c4bfff64985b3e/lib/webrick/utils.rb#L17
|
12
|
+
# https://docs.ruby-lang.org/ja/latest/class/ERB=3a=3aUtil.html
|
13
|
+
# https://github.com/ruby/rss/blob/af1c3c9c9630ec0a48abec48ed1ef348ba82aa13/lib/rss/utils.rb#L9
|
14
|
+
module Utils
|
15
|
+
# @return [Integer]
|
16
|
+
def self.current_milliseconds
|
17
|
+
milliseconds_from_time(Time.now)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Time] time
|
21
|
+
# @return [Integer]
|
22
|
+
def self.milliseconds_from_time(time)
|
23
|
+
(time.to_r * 1000).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [Time, Integer] moment
|
27
|
+
# @return [Integer]
|
28
|
+
def self.milliseconds_from_moment(moment)
|
29
|
+
case moment
|
30
|
+
when Integer
|
31
|
+
moment
|
32
|
+
when Time
|
33
|
+
milliseconds_from_time(moment)
|
34
|
+
else
|
35
|
+
raise(ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Integer]
|
40
|
+
def self.reasonable_entropy
|
41
|
+
SecureRandom.random_number(MAX_ENTROPY)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Integer] milliseconds
|
45
|
+
# @param [Integer] entropy
|
46
|
+
# @return [String]
|
47
|
+
# @raise [OverflowError] if the given value is larger than the ULID limit
|
48
|
+
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
49
|
+
def self.encode_base32hex(milliseconds:, entropy:)
|
50
|
+
raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
|
51
|
+
raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
|
52
|
+
raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
|
53
|
+
raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
|
54
|
+
|
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
|
+
end
|
59
|
+
|
60
|
+
# @param [BasicObject] object
|
61
|
+
# @return [String]
|
62
|
+
def self.safe_get_class_name(object)
|
63
|
+
fallback = 'UnknownObject'
|
64
|
+
|
65
|
+
# This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
|
66
|
+
# ref: https://twitter.com/_kachick/status/1400064896759304196
|
67
|
+
klass = (
|
68
|
+
begin
|
69
|
+
object.class
|
70
|
+
rescue NoMethodError
|
71
|
+
# steep can't correctly handle singleton class assign. See https://github.com/soutaro/steep/pull/586 for further detail
|
72
|
+
# So this annotation is hack for the type infer.
|
73
|
+
# @type var object: BasicObject
|
74
|
+
# @type var singleton_class: untyped
|
75
|
+
singleton_class = class << object; self; end
|
76
|
+
(Class === singleton_class) ? singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) } : fallback
|
77
|
+
end
|
78
|
+
)
|
79
|
+
|
80
|
+
begin
|
81
|
+
name = String.try_convert(klass.name)
|
82
|
+
rescue Exception
|
83
|
+
fallback
|
84
|
+
else
|
85
|
+
name || fallback
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @note Call before Module#private_constant
|
90
|
+
def self.make_sharable_constants(mod)
|
91
|
+
mod.constants.each do |const_name|
|
92
|
+
value = mod.const_get(const_name)
|
93
|
+
Ractor.make_shareable(value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private_constant(:Utils)
|
99
|
+
end
|