ruby-ulid 0.0.16 → 0.1.1
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 +173 -15
- data/lib/ulid.rb +245 -237
- data/lib/ulid/crockford_base32.rb +68 -0
- data/lib/ulid/monotonic_generator.rb +42 -21
- data/lib/ulid/uuid.rb +38 -0
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +56 -44
- metadata +4 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9517a4c0fe5e9feec91b2d8e19366baed6b8f1c1ccb42640389c1ad11066854
|
4
|
+
data.tar.gz: 24344ffd2549f7eae76f6f344169eb74baf3168e05499a9d7577a18fea63fc4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5420da3cddc622a02a9d84ba398c17f59c733eea62d122aef0e5fb39dfade8e76cb3e4608814a0afb5c2e0251c6ebbbb2a10c6b27ea68b4ff8e32ad9f2e31ee3
|
7
|
+
data.tar.gz: 7bb4f8f941ed3f3ec2d5ebef03b4559b1ba2b07eaf5a9970ee3936ba6157bf5ff818eabc63847ba973536910bd6e8b8c65ae3382b74a368af85e228fec6524dd
|
data/README.md
CHANGED
@@ -49,7 +49,7 @@ Should be installed!
|
|
49
49
|
Add this line to your application/library's `Gemfile` is needed in basic use-case
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
gem 'ruby-ulid', '0.0.
|
52
|
+
gem 'ruby-ulid', '>= 0.1.1', '< 0.2.0'
|
53
53
|
```
|
54
54
|
|
55
55
|
### Generator and Parser
|
@@ -62,7 +62,11 @@ require 'ulid'
|
|
62
62
|
|
63
63
|
ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
64
64
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
65
|
+
ulid.milliseconds #=> 1619544442826
|
65
66
|
ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
67
|
+
ulid.timestamp #=> "01F4A5Y1YA"
|
68
|
+
ulid.randomness #=> "QCYAYCTC7GRMJ9AA"
|
69
|
+
ulid.to_i #=> 1957909092946624190749577070267409738
|
66
70
|
ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
|
67
71
|
```
|
68
72
|
|
@@ -86,15 +90,16 @@ ulids.uniq(&:to_time).size #=> 1000
|
|
86
90
|
ulids.sort == ulids #=> true
|
87
91
|
```
|
88
92
|
|
89
|
-
`ULID.generate` can take fixed `Time` instance
|
93
|
+
`ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`
|
90
94
|
|
91
95
|
```ruby
|
92
96
|
time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
|
93
97
|
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
94
98
|
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
99
|
+
ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
|
95
100
|
|
96
101
|
ulids = 1000.times.map do |n|
|
97
|
-
ULID.
|
102
|
+
ULID.at(time + n)
|
98
103
|
end
|
99
104
|
ulids.sort == ulids #=> true
|
100
105
|
```
|
@@ -141,6 +146,8 @@ sample_ulids_by_the_time.take(5) #=>
|
|
141
146
|
ulids.sort == ulids #=> true
|
142
147
|
```
|
143
148
|
|
149
|
+
Same generator does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Thread::Mutex](https://github.com/ruby/ruby/blob/5f8bca32571fa9c651f6903d36f66082363f8879/thread_sync.c#L1572-L1582)
|
150
|
+
|
144
151
|
### Filtering IDs with `Time`
|
145
152
|
|
146
153
|
`ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
|
@@ -173,6 +180,13 @@ ulids.grep_v(one_of_the_above)
|
|
173
180
|
#=> I hope the results should be actually you want!
|
174
181
|
```
|
175
182
|
|
183
|
+
If you want to manually handle the Time objects, `ULID.floor` returns new `Time` with truncating excess precisions in ULID spec.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
|
187
|
+
ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
|
188
|
+
```
|
189
|
+
|
176
190
|
### Scanner for string (e.g. `JSON`)
|
177
191
|
|
178
192
|
For rough operations, `ULID.scan` might be useful.
|
@@ -207,25 +221,39 @@ EOD
|
|
207
221
|
|
208
222
|
ULID.scan(json).to_a
|
209
223
|
#=>
|
210
|
-
[ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
224
|
+
# [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
|
225
|
+
# ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
|
226
|
+
# ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
|
227
|
+
# ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
|
228
|
+
# ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
|
229
|
+
# ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
230
|
+
```
|
231
|
+
|
232
|
+
`ULID#patterns` is a util for text based operations.
|
233
|
+
The results and spec are not fixed. Should not be used except snippets/console operation
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
|
237
|
+
#=> returns like a fallowing Hash
|
238
|
+
{
|
239
|
+
named_captures: /(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)/i,
|
240
|
+
strict_named_captures: /\A(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)\z/i
|
241
|
+
}
|
216
242
|
```
|
217
243
|
|
218
244
|
### Some methods to help manipulations
|
219
245
|
|
220
246
|
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
221
247
|
|
248
|
+
It can take `Time` instance as an optional argument. Then returns min/max ID that has limit of randomness part in the time.
|
249
|
+
|
222
250
|
```ruby
|
223
251
|
ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
|
224
252
|
ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
|
225
253
|
|
226
254
|
time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
|
227
|
-
ULID.min(
|
228
|
-
ULID.max(
|
255
|
+
ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
|
256
|
+
ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
229
257
|
```
|
230
258
|
|
231
259
|
`ULID#next` and `ULID#succ` returns next(successor) ULID.
|
@@ -247,12 +275,66 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
|
|
247
275
|
ULID.parse('00000000000000000000000000').pred #=> nil
|
248
276
|
```
|
249
277
|
|
278
|
+
`ULID.sample` returns random ULIDs.
|
279
|
+
|
280
|
+
Basically ignores generating time.
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
284
|
+
ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
|
285
|
+
ULID.sample(0) #=> []
|
286
|
+
ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
|
287
|
+
ULID.sample(5)
|
288
|
+
#=>
|
289
|
+
#[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
|
290
|
+
# ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
|
291
|
+
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
|
292
|
+
# ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
|
293
|
+
# ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
|
294
|
+
```
|
295
|
+
|
296
|
+
You can specify a range object for the timestamp restriction, see also `ULID.range`.
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
300
|
+
ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
|
301
|
+
ulids = ULID.sample(1000, period: ulid1..ulid2)
|
302
|
+
ulids.uniq.size #=> 1000
|
303
|
+
ulids.take(10)
|
304
|
+
#=>
|
305
|
+
#[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
|
306
|
+
# ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
|
307
|
+
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
|
308
|
+
# ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
|
309
|
+
# ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
|
310
|
+
# ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
|
311
|
+
# ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
|
312
|
+
# ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
|
313
|
+
# ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
|
314
|
+
# ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
|
315
|
+
ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
|
316
|
+
#=>
|
317
|
+
# [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
|
318
|
+
# ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
|
319
|
+
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
|
320
|
+
# ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
|
321
|
+
# ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
|
322
|
+
# ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
|
323
|
+
# ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
|
324
|
+
# ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
|
325
|
+
# ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
|
326
|
+
# ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
|
327
|
+
```
|
328
|
+
|
250
329
|
### UUIDv4 converter for migration use-cases
|
251
330
|
|
252
331
|
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
253
|
-
The imported timestamp is meaningless. So ULID's benefit will lost
|
332
|
+
The imported timestamp is meaningless. So ULID's benefit will lost.
|
254
333
|
|
255
334
|
```ruby
|
335
|
+
# Currently experimental feature, so needed to load the extension.
|
336
|
+
require 'ulid/uuid'
|
337
|
+
|
256
338
|
# Basically reversible
|
257
339
|
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
258
340
|
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
@@ -306,13 +388,13 @@ Major methods can be replaced as below.
|
|
306
388
|
-ULID.generate
|
307
389
|
+ULID.generate.to_s
|
308
390
|
-ULID.at(time)
|
309
|
-
+ULID.
|
391
|
+
+ULID.at(time).to_s
|
310
392
|
-ULID.time(string)
|
311
393
|
+ULID.parse(string).to_time
|
312
394
|
-ULID.min_ulid_at(time)
|
313
|
-
+ULID.min(
|
395
|
+
+ULID.min(time).to_s
|
314
396
|
-ULID.max_ulid_at(time)
|
315
|
-
+ULID.max(
|
397
|
+
+ULID.max(time).to_s
|
316
398
|
```
|
317
399
|
|
318
400
|
NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
|
@@ -321,6 +403,82 @@ NOTE: It is still having precision issue similar as `ulid gem` in the both gener
|
|
321
403
|
1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
|
322
404
|
1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
|
323
405
|
|
406
|
+
### Generating benchmarks
|
407
|
+
|
408
|
+
This runs rough benchmarks
|
409
|
+
|
410
|
+
```console
|
411
|
+
$ rake benchmark_with_other_gems
|
412
|
+
(Do not use `bundle exec`!)
|
413
|
+
```
|
414
|
+
|
415
|
+
<details>
|
416
|
+
<summary>One of the result at 2021/05/10 on my machine</summary>
|
417
|
+
|
418
|
+
```plaintext
|
419
|
+
#### rafaelsales - ulid
|
420
|
+
cd ./benchmark/compare_with_othergems/rafaelsales && bundle install --quiet && bundle exec ruby -v ./generate.rb
|
421
|
+
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
|
422
|
+
Warming up --------------------------------------
|
423
|
+
ULID.generate 5.560k i/100ms
|
424
|
+
Calculating -------------------------------------
|
425
|
+
ULID.generate 52.655k (±11.0%) i/s - 261.320k in 5.029719s
|
426
|
+
"`ulid gem - 1.3.0` generated products: 371927 - sample: [\"01F59Y97807D2S67KE6X7ATK7Z\", \"01F59Y9AVRQJFAT5M2N7Z72BVF\", \"01F59Y95Z1042X4Z1K9729BSE3\", \"01F59Y95ZMVDFKD63Y8TT145GQ\", \"01F59Y94YQEZ3PH5STZ8PS1JPG\"]"
|
427
|
+
------------------------------------------------------------------------
|
428
|
+
#### abachman - ulid-ruby
|
429
|
+
cd ./benchmark/compare_with_othergems/abachman && bundle install --quiet && bundle exec ruby -v ./generate.rb
|
430
|
+
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
|
431
|
+
Warming up --------------------------------------
|
432
|
+
ULID.generate 3.862k i/100ms
|
433
|
+
Calculating -------------------------------------
|
434
|
+
ULID.generate 38.415k (±13.1%) i/s - 189.238k in 5.025788s
|
435
|
+
"`ulid-ruby gem - 1.0.0` generated products: 260625 - sample: [\"01F59Y9H9V17EPXTYNZDCXB9EZ\", \"01F59Y9J4S4XZ68MF5DJDWHTAC\", \"01F59Y9J8887VC8E850QSBDCDX\", \"01F59Y9JEJPD088EYXVHB86W3N\", \"01F59Y9GGAZFXGCB92EQD695CZ\"]"
|
436
|
+
------------------------------------------------------------------------
|
437
|
+
#### kachick - ruby-ulid(This one)
|
438
|
+
cd ./benchmark/compare_with_othergems/kachick && bundle install --quiet && bundle exec ruby -v ./generate.rb
|
439
|
+
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
|
440
|
+
Warming up --------------------------------------
|
441
|
+
ULID.generate.to_s 3.185k i/100ms
|
442
|
+
Calculating -------------------------------------
|
443
|
+
ULID.generate.to_s 31.934k (± 9.1%) i/s - 159.250k in 5.030707s
|
444
|
+
"`ruby-ulid gem (this one) - 0.1.0` generated products: 223867 - sample: [\"01F59Y9SPZHM6JCTYP50CHGVAX\", \"01F59Y9VB7X0SX32MMKF78KJR3\", \"01F59Y9W0C83RYCNYVH84R4JG3\", \"01F59Y9V218Q3D4YP3W74ET3EW\", \"01F59Y9X6DD8NX99WBGCR7RNXF\"]"
|
445
|
+
```
|
446
|
+
|
447
|
+
In another execution, Changed as below. So there doesn't seem to be a big difference.
|
448
|
+
|
449
|
+
```plaintext
|
450
|
+
#### rafaelsales - ulid
|
451
|
+
cd ./benchmark/compare_with_othergems/rafaelsales && bundle install --quiet && bundle exec ruby -v ./generate.rb
|
452
|
+
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
|
453
|
+
Warming up --------------------------------------
|
454
|
+
ULID.generate 2.473k i/100ms
|
455
|
+
Calculating -------------------------------------
|
456
|
+
ULID.generate 24.101k (±15.9%) i/s - 118.704k in 5.066190s
|
457
|
+
"`ulid gem - 1.3.0` generated products: 164763 - sample: [\"01F59YEGPFMXXZWC1YQ49TSK8Y\", \"01F59YEFF7VX5WAW91VTCSE2N9\", \"01F59YEEZ5P9428SDYEDYW8D27\", \"01F59YEHVK56DZBJSNSQK6V1W6\", \"01F59YEHE07M98PVV97ABBAKHM\"]"
|
458
|
+
------------------------------------------------------------------------
|
459
|
+
#### abachman - ulid-ruby
|
460
|
+
cd ./benchmark/compare_with_othergems/abachman && bundle install --quiet && bundle exec ruby -v ./generate.rb
|
461
|
+
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
|
462
|
+
Warming up --------------------------------------
|
463
|
+
ULID.generate 2.620k i/100ms
|
464
|
+
Calculating -------------------------------------
|
465
|
+
ULID.generate 27.571k (±14.2%) i/s - 136.240k in 5.056272s
|
466
|
+
"`ulid-ruby gem - 1.0.0` generated products: 186683 - sample: [\"01F59YEVX6GC9TC0RCZ74RC6Z3\", \"01F59YESJXWYGZ61TXHKRVKS97\", \"01F59YEVQ4QQKBED5T49RTV1MA\", \"01F59YEPJ6MMZY1N63DNW7C4SN\", \"01F59YEQK52K8TKTP1ESC6VC5X\"]"
|
467
|
+
------------------------------------------------------------------------
|
468
|
+
#### kachick - ruby-ulid(This one)
|
469
|
+
cd ./benchmark/compare_with_othergems/kachick && bundle install --quiet && bundle exec ruby -v ./generate.rb
|
470
|
+
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
|
471
|
+
Warming up --------------------------------------
|
472
|
+
ULID.generate.to_s 3.014k i/100ms
|
473
|
+
Calculating -------------------------------------
|
474
|
+
ULID.generate.to_s 31.612k (±10.1%) i/s - 156.728k in 5.013432s
|
475
|
+
"`ruby-ulid gem (this one) - 0.1.0` generated products: 212293 - sample: [\"01F59YF1WP49TT4GQPDN3E9JTJ\", \"01F59YF1MW1ZDQW93NX4J6RG4G\", \"01F59YF0KRX2CZKHDQQSN5HXHW\", \"01F59YEZVNH8YHP4ZHDK2ZRWSR\", \"01F59YF1J0FV3CVV099SHA2Q9A\"]"
|
476
|
+
```
|
477
|
+
|
478
|
+
I have an excuse, This gem does not aim `faster than other`.
|
479
|
+
So I think the results are acceptable.
|
480
|
+
</details>
|
481
|
+
|
324
482
|
## References
|
325
483
|
|
326
484
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
data/lib/ulid.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
# Copyright (C) 2021 Kenichi Kamiya
|
4
4
|
|
5
5
|
require 'securerandom'
|
6
|
-
require 'integer/base'
|
7
6
|
|
8
7
|
# @see https://github.com/ulid/spec
|
9
8
|
# @!attribute [r] milliseconds
|
@@ -16,63 +15,115 @@ class ULID
|
|
16
15
|
class Error < StandardError; end
|
17
16
|
class OverflowError < Error; end
|
18
17
|
class ParserError < Error; end
|
19
|
-
class
|
18
|
+
class UnexpectedError < Error; end
|
20
19
|
|
21
|
-
|
22
|
-
#
|
23
|
-
#
|
24
|
-
|
20
|
+
# Excluded I, L, O, U, -.
|
21
|
+
# This is the encoding patterns.
|
22
|
+
# The decoding issue is written in ULID::CrockfordBase32
|
23
|
+
CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
TIMESTAMP_ENCODED_LENGTH = 10
|
26
|
+
RANDOMNESS_ENCODED_LENGTH = 16
|
27
|
+
ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
|
29
28
|
TIMESTAMP_OCTETS_LENGTH = 6
|
30
29
|
RANDOMNESS_OCTETS_LENGTH = 10
|
31
30
|
OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
|
32
31
|
MAX_MILLISECONDS = 281474976710655
|
33
32
|
MAX_ENTROPY = 1208925819614629174706175
|
34
33
|
MAX_INTEGER = 340282366920938463463374607431768211455
|
35
|
-
PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
|
36
|
-
STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
|
37
34
|
|
38
|
-
#
|
39
|
-
|
35
|
+
# @see https://github.com/ulid/spec/pull/57
|
36
|
+
# Currently not used as a constant, but kept as a reference for now.
|
37
|
+
PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
|
38
|
+
|
39
|
+
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
|
40
|
+
|
41
|
+
# Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
|
42
|
+
# This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
|
43
|
+
SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
|
40
44
|
|
41
45
|
# Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
|
42
46
|
# @see https://bugs.ruby-lang.org/issues/15958
|
43
47
|
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
44
48
|
|
49
|
+
private_class_method :new
|
50
|
+
|
45
51
|
# @param [Integer, Time] moment
|
46
52
|
# @param [Integer] entropy
|
47
53
|
# @return [ULID]
|
48
54
|
def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
|
49
|
-
|
55
|
+
from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
|
50
56
|
end
|
51
57
|
|
52
|
-
#
|
58
|
+
# Short hand of `ULID.generate(moment: time)`
|
59
|
+
# @param [Time] time
|
60
|
+
# @return [ULID]
|
61
|
+
def self.at(time)
|
62
|
+
raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
|
63
|
+
from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param [Time, Integer] moment
|
53
67
|
# @return [ULID]
|
54
|
-
def self.min(moment
|
68
|
+
def self.min(moment=0)
|
55
69
|
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
56
70
|
end
|
57
71
|
|
58
|
-
# @param [
|
72
|
+
# @param [Time, Integer] moment
|
59
73
|
# @return [ULID]
|
60
|
-
def self.max(moment
|
74
|
+
def self.max(moment=MAX_MILLISECONDS)
|
61
75
|
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
62
76
|
end
|
63
77
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
78
|
+
RANDOM_INTEGER_GENERATOR = -> {
|
79
|
+
SecureRandom.random_number(MAX_INTEGER)
|
80
|
+
}
|
81
|
+
|
82
|
+
# @param [Range<Time>, Range<nil>, Range[ULID], nil] period
|
83
|
+
# @overload sample(number, period: nil)
|
84
|
+
# @param [Integer] number
|
85
|
+
# @return [Array<ULID>]
|
86
|
+
# @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
|
87
|
+
# @overload sample(period: nil)
|
88
|
+
# @return [ULID]
|
89
|
+
# @note Major difference of `Array#sample` interface is below
|
90
|
+
# * Do not ensure the uniqueness
|
91
|
+
# * Do not take random generator for the arguments
|
92
|
+
# * Raising error instead of truncating elements for the given number
|
93
|
+
def self.sample(*args, period: nil)
|
94
|
+
int_generator = if period
|
95
|
+
ulid_range = range(period)
|
96
|
+
min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
|
97
|
+
|
98
|
+
possibilities = (max - min) + (exclude_end ? 0 : 1)
|
99
|
+
raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
|
100
|
+
|
101
|
+
-> {
|
102
|
+
SecureRandom.random_number(possibilities) + min
|
103
|
+
}
|
71
104
|
else
|
72
|
-
|
105
|
+
RANDOM_INTEGER_GENERATOR
|
73
106
|
end
|
74
107
|
|
75
|
-
|
108
|
+
case args.size
|
109
|
+
when 0
|
110
|
+
from_integer(int_generator.call)
|
111
|
+
when 1
|
112
|
+
number = args.first
|
113
|
+
raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
|
114
|
+
|
115
|
+
if number > MAX_INTEGER || number.negative?
|
116
|
+
raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
|
117
|
+
end
|
118
|
+
|
119
|
+
if period && (number > possibilities)
|
120
|
+
raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
|
121
|
+
end
|
122
|
+
|
123
|
+
Array.new(number) { from_integer(int_generator.call) }
|
124
|
+
else
|
125
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
|
126
|
+
end
|
76
127
|
end
|
77
128
|
|
78
129
|
# @param [String, #to_str] string
|
@@ -80,77 +131,68 @@ class ULID
|
|
80
131
|
# @yieldparam [ULID] ulid
|
81
132
|
# @yieldreturn [self]
|
82
133
|
def self.scan(string)
|
83
|
-
string = string
|
134
|
+
string = String.try_convert(string)
|
135
|
+
raise ArgumentError, 'ULID.scan takes only strings' unless string
|
84
136
|
return to_enum(__callee__, string) unless block_given?
|
85
|
-
string.scan(
|
86
|
-
yield parse(
|
137
|
+
string.scan(SCANNING_PATTERN) do |matched|
|
138
|
+
yield parse(matched)
|
87
139
|
end
|
88
140
|
self
|
89
141
|
end
|
90
142
|
|
91
|
-
# @param [
|
92
|
-
# @return [ULID]
|
93
|
-
# @raise [ParserError] if the given format is not correct for UUIDv4 specs
|
94
|
-
def self.from_uuidv4(uuid)
|
95
|
-
begin
|
96
|
-
uuid = uuid.to_str
|
97
|
-
prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
|
98
|
-
raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
|
99
|
-
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
100
|
-
from_integer(normalized.to_i(16))
|
101
|
-
rescue => err
|
102
|
-
raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# @param [Integer, #to_int] integer
|
143
|
+
# @param [Integer] integer
|
107
144
|
# @return [ULID]
|
108
145
|
# @raise [OverflowError] if the given integer is larger than the ULID limit
|
109
146
|
# @raise [ArgumentError] if the given integer is negative number
|
110
|
-
# @todo Need optimized for performance
|
111
147
|
def self.from_integer(integer)
|
112
|
-
|
148
|
+
raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
|
113
149
|
raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
|
114
150
|
raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
|
115
151
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
152
|
+
n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
|
153
|
+
n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
|
154
|
+
n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
|
155
|
+
|
156
|
+
milliseconds = n32encoded_timestamp.to_i(32)
|
157
|
+
entropy = n32encoded_randomness.to_i(32)
|
121
158
|
|
122
|
-
new milliseconds: milliseconds, entropy: entropy
|
159
|
+
new milliseconds: milliseconds, entropy: entropy, integer: integer
|
123
160
|
end
|
124
161
|
|
125
|
-
# @param [Range<Time>, Range<nil
|
162
|
+
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
126
163
|
# @return [Range<ULID>]
|
127
|
-
# @raise [ArgumentError] if the given
|
128
|
-
def self.range(
|
129
|
-
raise
|
130
|
-
|
164
|
+
# @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
|
165
|
+
def self.range(period)
|
166
|
+
raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period
|
167
|
+
begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
|
168
|
+
return period if self === begin_element && self === end_element
|
131
169
|
|
132
|
-
case
|
170
|
+
case begin_element
|
133
171
|
when Time
|
134
|
-
begin_ulid = min(
|
172
|
+
begin_ulid = min(begin_element)
|
135
173
|
when nil
|
136
|
-
begin_ulid =
|
174
|
+
begin_ulid = MIN
|
175
|
+
when self
|
176
|
+
begin_ulid = begin_element
|
137
177
|
else
|
138
|
-
raise
|
178
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
|
139
179
|
end
|
140
180
|
|
141
|
-
case
|
181
|
+
case end_element
|
142
182
|
when Time
|
143
183
|
if exclude_end
|
144
|
-
end_ulid = min(
|
184
|
+
end_ulid = min(end_element)
|
145
185
|
else
|
146
|
-
end_ulid = max(
|
186
|
+
end_ulid = max(end_element)
|
147
187
|
end
|
148
188
|
when nil
|
149
189
|
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
150
|
-
end_ulid =
|
190
|
+
end_ulid = MAX
|
151
191
|
exclude_end = false
|
192
|
+
when self
|
193
|
+
end_ulid = end_element
|
152
194
|
else
|
153
|
-
raise
|
195
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
|
154
196
|
end
|
155
197
|
|
156
198
|
begin_ulid.freeze
|
@@ -162,6 +204,8 @@ class ULID
|
|
162
204
|
# @param [Time] time
|
163
205
|
# @return [Time]
|
164
206
|
def self.floor(time)
|
207
|
+
raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
|
208
|
+
|
165
209
|
if RUBY_VERSION >= '2.7'
|
166
210
|
time.floor(3)
|
167
211
|
else
|
@@ -169,184 +213,139 @@ class ULID
|
|
169
213
|
end
|
170
214
|
end
|
171
215
|
|
216
|
+
# @api private
|
172
217
|
# @return [Integer]
|
173
218
|
def self.current_milliseconds
|
174
219
|
milliseconds_from_time(Time.now)
|
175
220
|
end
|
176
221
|
|
222
|
+
# @api private
|
177
223
|
# @param [Time] time
|
178
224
|
# @return [Integer]
|
179
|
-
def self.milliseconds_from_time(time)
|
225
|
+
private_class_method def self.milliseconds_from_time(time)
|
180
226
|
(time.to_r * 1000).to_i
|
181
227
|
end
|
182
228
|
|
229
|
+
# @api private
|
183
230
|
# @param [Time, Integer] moment
|
184
231
|
# @return [Integer]
|
185
232
|
def self.milliseconds_from_moment(moment)
|
186
|
-
|
233
|
+
case moment
|
234
|
+
when Integer
|
235
|
+
moment
|
236
|
+
when Time
|
237
|
+
milliseconds_from_time(moment)
|
238
|
+
else
|
239
|
+
raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
|
240
|
+
end
|
187
241
|
end
|
188
242
|
|
189
243
|
# @return [Integer]
|
190
|
-
def self.reasonable_entropy
|
244
|
+
private_class_method def self.reasonable_entropy
|
191
245
|
SecureRandom.random_number(MAX_ENTROPY)
|
192
246
|
end
|
193
247
|
|
194
|
-
# @api private
|
195
|
-
# @deprecated Just exists to compare performance with old implementation. ref: https://github.com/kachick/ruby-ulid/issues/7
|
196
248
|
# @param [String, #to_str] string
|
197
249
|
# @return [ULID]
|
198
250
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
199
|
-
# @raise [OverflowError] if the given value is larger than the ULID limit
|
200
|
-
def self.parse_with_integer_base(string)
|
201
|
-
begin
|
202
|
-
string = string.to_str
|
203
|
-
unless string.size == ENCODED_ID_LENGTH
|
204
|
-
raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
|
205
|
-
end
|
206
|
-
timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
|
207
|
-
randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
208
|
-
milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
|
209
|
-
entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
|
210
|
-
rescue => err
|
211
|
-
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
212
|
-
end
|
213
|
-
|
214
|
-
new milliseconds: milliseconds, entropy: entropy
|
215
|
-
end
|
216
|
-
|
217
|
-
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
218
|
-
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
219
|
-
|
220
|
-
n32_char_by_number = {}
|
221
|
-
n32_chars.each_with_index do |char, index|
|
222
|
-
n32_char_by_number[index] = char
|
223
|
-
end
|
224
|
-
n32_char_by_number.freeze
|
225
|
-
|
226
|
-
# Currently supporting only for `subset for actual use-case`
|
227
|
-
# See below
|
228
|
-
# * https://github.com/ulid/spec/pull/57
|
229
|
-
# * https://github.com/kachick/ruby-ulid/issues/57
|
230
|
-
# * https://github.com/kachick/ruby-ulid/issues/78
|
231
|
-
crockford_base32_mappings = {
|
232
|
-
'J' => 18,
|
233
|
-
'K' => 19,
|
234
|
-
'M' => 20,
|
235
|
-
'N' => 21,
|
236
|
-
'P' => 22,
|
237
|
-
'Q' => 23,
|
238
|
-
'R' => 24,
|
239
|
-
'S' => 25,
|
240
|
-
'T' => 26,
|
241
|
-
'V' => 27,
|
242
|
-
'W' => 28,
|
243
|
-
'X' => 29,
|
244
|
-
'Y' => 30,
|
245
|
-
'Z' => 31
|
246
|
-
}.freeze
|
247
|
-
|
248
|
-
REPLACING_MAP = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
|
249
|
-
if n = crockford_base32_mappings[encoding_char]
|
250
|
-
char_32 = n32_char_by_number.fetch(n)
|
251
|
-
map[encoding_char] = char_32
|
252
|
-
end
|
253
|
-
end.freeze
|
254
|
-
raise SetupError, 'obvious bug exists in the mapping algorithm' unless REPLACING_MAP.keys == crockford_base32_mappings.keys
|
255
|
-
REPLACING_PATTERN = /[#{REPLACING_MAP.keys.join}]/.freeze
|
256
|
-
|
257
251
|
def self.parse(string)
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
264
|
-
milliseconds = timestamp.to_i(32)
|
265
|
-
entropy = randomness.to_i(32)
|
266
|
-
rescue => err
|
267
|
-
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
252
|
+
string = String.try_convert(string)
|
253
|
+
raise ArgumentError, 'ULID.parse takes only strings' unless string
|
254
|
+
|
255
|
+
unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
|
256
|
+
raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
|
268
257
|
end
|
269
258
|
|
270
|
-
|
271
|
-
end
|
272
|
-
|
273
|
-
# @api private
|
274
|
-
private_class_method def self.convert_crockford_base32_to_n32(string)
|
275
|
-
string.gsub(REPLACING_PATTERN, REPLACING_MAP)
|
259
|
+
from_integer(CrockfordBase32.decode(string))
|
276
260
|
end
|
277
261
|
|
262
|
+
# @param [String, #to_str] string
|
278
263
|
# @return [Boolean]
|
279
264
|
def self.valid?(string)
|
280
|
-
|
281
|
-
|
282
|
-
false
|
283
|
-
else
|
284
|
-
true
|
265
|
+
string = String.try_convert(string)
|
266
|
+
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
|
285
267
|
end
|
286
268
|
|
287
|
-
# @
|
288
|
-
# @
|
289
|
-
# @
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
269
|
+
# @param [ULID, #to_ulid] object
|
270
|
+
# @return [ULID, nil]
|
271
|
+
# @raise [TypeError] if `object.to_ulid` did not return ULID instance
|
272
|
+
def self.try_convert(object)
|
273
|
+
begin
|
274
|
+
converted = object.to_ulid
|
275
|
+
rescue NoMethodError
|
276
|
+
nil
|
277
|
+
else
|
278
|
+
if ULID === converted
|
279
|
+
converted
|
280
|
+
else
|
281
|
+
object_class_name = safe_get_class_name(object)
|
282
|
+
converted_class_name = safe_get_class_name(converted)
|
283
|
+
raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
|
284
|
+
end
|
295
285
|
end
|
296
|
-
digits.reverse!
|
297
286
|
end
|
298
287
|
|
299
|
-
# @
|
300
|
-
# @
|
301
|
-
|
302
|
-
|
303
|
-
def self.inverse_of_digits(reversed_digits)
|
304
|
-
base = 256
|
305
|
-
num = 0
|
306
|
-
reversed_digits.each do |digit|
|
307
|
-
num = (num * base) + digit
|
308
|
-
end
|
309
|
-
num
|
310
|
-
end
|
288
|
+
# @param [BasicObject] object
|
289
|
+
# @return [String]
|
290
|
+
private_class_method def self.safe_get_class_name(object)
|
291
|
+
fallback = 'UnknownObject'
|
311
292
|
|
312
|
-
|
313
|
-
|
314
|
-
|
293
|
+
begin
|
294
|
+
name = String.try_convert(object.class.name)
|
295
|
+
rescue Exception
|
296
|
+
fallback
|
297
|
+
else
|
298
|
+
name || fallback
|
299
|
+
end
|
315
300
|
end
|
316
301
|
|
317
|
-
attr_reader :milliseconds, :entropy
|
318
|
-
|
319
302
|
# @api private
|
320
303
|
# @param [Integer] milliseconds
|
321
304
|
# @param [Integer] entropy
|
322
|
-
# @return [
|
305
|
+
# @return [ULID]
|
323
306
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
324
307
|
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
325
|
-
def
|
326
|
-
milliseconds
|
327
|
-
entropy = entropy.to_int
|
308
|
+
def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
|
309
|
+
raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
|
328
310
|
raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
|
329
311
|
raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
|
330
312
|
raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
|
331
313
|
|
314
|
+
n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
315
|
+
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
316
|
+
integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)
|
317
|
+
|
318
|
+
new milliseconds: milliseconds, entropy: entropy, integer: integer
|
319
|
+
end
|
320
|
+
|
321
|
+
attr_reader :milliseconds, :entropy
|
322
|
+
|
323
|
+
# @api private
|
324
|
+
# @param [Integer] milliseconds
|
325
|
+
# @param [Integer] entropy
|
326
|
+
# @param [Integer] integer
|
327
|
+
# @return [void]
|
328
|
+
def initialize(milliseconds:, entropy:, integer:)
|
329
|
+
# All arguments check should be done with each constructors, not here
|
330
|
+
@integer = integer
|
332
331
|
@milliseconds = milliseconds
|
333
332
|
@entropy = entropy
|
334
333
|
end
|
335
334
|
|
336
335
|
# @return [String]
|
337
336
|
def to_s
|
338
|
-
@string ||=
|
337
|
+
@string ||= CrockfordBase32.encode(@integer).freeze
|
339
338
|
end
|
340
339
|
|
341
340
|
# @return [Integer]
|
342
341
|
def to_i
|
343
|
-
@integer
|
342
|
+
@integer
|
344
343
|
end
|
345
344
|
alias_method :hash, :to_i
|
346
345
|
|
347
346
|
# @return [Integer, nil]
|
348
347
|
def <=>(other)
|
349
|
-
|
348
|
+
(ULID === other) ? (@integer <=> other.to_i) : nil
|
350
349
|
end
|
351
350
|
|
352
351
|
# @return [String]
|
@@ -356,7 +355,7 @@ class ULID
|
|
356
355
|
|
357
356
|
# @return [Boolean]
|
358
357
|
def eql?(other)
|
359
|
-
|
358
|
+
equal?(other) || (ULID === other && @integer == other.to_i)
|
360
359
|
end
|
361
360
|
alias_method :==, :eql?
|
362
361
|
|
@@ -364,13 +363,9 @@ class ULID
|
|
364
363
|
def ===(other)
|
365
364
|
case other
|
366
365
|
when ULID
|
367
|
-
|
366
|
+
@integer == other.to_i
|
368
367
|
when String
|
369
|
-
|
370
|
-
self == self.class.parse(other)
|
371
|
-
rescue Exception
|
372
|
-
false
|
373
|
-
end
|
368
|
+
to_s == other.upcase
|
374
369
|
else
|
375
370
|
false
|
376
371
|
end
|
@@ -389,64 +384,69 @@ class ULID
|
|
389
384
|
|
390
385
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
391
386
|
def octets
|
392
|
-
|
387
|
+
digits = @integer.digits(256)
|
388
|
+
(OCTETS_LENGTH - digits.size).times do
|
389
|
+
digits.push 0
|
390
|
+
end
|
391
|
+
digits.reverse!
|
393
392
|
end
|
394
393
|
|
395
394
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
396
395
|
def timestamp_octets
|
397
|
-
|
396
|
+
octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
|
398
397
|
end
|
399
398
|
|
400
399
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
401
400
|
def randomness_octets
|
402
|
-
|
401
|
+
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
|
403
402
|
end
|
404
403
|
|
405
404
|
# @return [String]
|
406
405
|
def timestamp
|
407
|
-
@timestamp ||=
|
406
|
+
@timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
|
408
407
|
end
|
409
408
|
|
410
409
|
# @return [String]
|
411
410
|
def randomness
|
412
|
-
@randomness ||=
|
413
|
-
end
|
414
|
-
|
415
|
-
# @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
|
416
|
-
# @return [Regexp]
|
417
|
-
def pattern
|
418
|
-
@pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
411
|
+
@randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
|
419
412
|
end
|
420
413
|
|
421
|
-
# @
|
422
|
-
# @return [Regexp]
|
423
|
-
def
|
424
|
-
|
414
|
+
# @note Providing for rough operations. The keys and values is not fixed.
|
415
|
+
# @return [Hash{Symbol => Regexp, String}]
|
416
|
+
def patterns
|
417
|
+
named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
418
|
+
{
|
419
|
+
named_captures: named_captures,
|
420
|
+
strict_named_captures: /\A#{named_captures.source}\z/i.freeze
|
421
|
+
}
|
425
422
|
end
|
426
423
|
|
427
424
|
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
428
|
-
def
|
429
|
-
|
430
|
-
|
431
|
-
|
425
|
+
def succ
|
426
|
+
succ_int = @integer.succ
|
427
|
+
if succ_int >= MAX_INTEGER
|
428
|
+
if succ_int == MAX_INTEGER
|
429
|
+
MAX
|
430
|
+
else
|
431
|
+
nil
|
432
|
+
end
|
433
|
+
else
|
434
|
+
ULID.from_integer(succ_int)
|
435
|
+
end
|
432
436
|
end
|
433
|
-
alias_method :
|
437
|
+
alias_method :next, :succ
|
434
438
|
|
435
439
|
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
436
440
|
def pred
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
array = octets.pack('C*').unpack('NnnnnN')
|
447
|
-
array[2] = (array[2] & 0x0fff) | 0x4000
|
448
|
-
array[3] = (array[3] & 0x3fff) | 0x8000
|
449
|
-
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
441
|
+
pred_int = @integer.pred
|
442
|
+
if pred_int <= 0
|
443
|
+
if pred_int == 0
|
444
|
+
MIN
|
445
|
+
else
|
446
|
+
nil
|
447
|
+
end
|
448
|
+
else
|
449
|
+
ULID.from_integer(pred_int)
|
450
450
|
end
|
451
451
|
end
|
452
452
|
|
@@ -457,32 +457,40 @@ class ULID
|
|
457
457
|
super
|
458
458
|
end
|
459
459
|
|
460
|
-
|
460
|
+
# @return [self]
|
461
|
+
def to_ulid
|
462
|
+
self
|
463
|
+
end
|
461
464
|
|
462
|
-
# @return [
|
463
|
-
def
|
464
|
-
|
465
|
+
# @return [self]
|
466
|
+
def dup
|
467
|
+
self
|
465
468
|
end
|
466
469
|
|
470
|
+
# @return [self]
|
471
|
+
def clone(freeze: true)
|
472
|
+
self
|
473
|
+
end
|
474
|
+
|
475
|
+
undef_method :instance_variable_set
|
476
|
+
|
477
|
+
private
|
478
|
+
|
467
479
|
# @return [void]
|
468
480
|
def cache_all_instance_variables
|
469
481
|
inspect
|
470
|
-
|
471
|
-
|
472
|
-
succ
|
473
|
-
pred
|
474
|
-
strict_pattern
|
475
|
-
to_uuidv4
|
482
|
+
timestamp
|
483
|
+
randomness
|
476
484
|
end
|
477
485
|
end
|
478
486
|
|
479
487
|
require_relative 'ulid/version'
|
488
|
+
require_relative 'ulid/crockford_base32'
|
480
489
|
require_relative 'ulid/monotonic_generator'
|
481
490
|
|
482
491
|
class ULID
|
483
492
|
MIN = parse('00000000000000000000000000').freeze
|
484
493
|
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
485
|
-
MONOTONIC_GENERATOR = MonotonicGenerator.new
|
486
494
|
|
487
|
-
private_constant :
|
495
|
+
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
|
488
496
|
end
|