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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15a83604732cdc37f8015ee9382884e950852f8c9c82aba107ea4b3c065c5a4d
4
- data.tar.gz: 06e7b69a5838786f4d23da19f5e1f00fb3173014e7438459165b66703637851a
3
+ metadata.gz: d9517a4c0fe5e9feec91b2d8e19366baed6b8f1c1ccb42640389c1ad11066854
4
+ data.tar.gz: 24344ffd2549f7eae76f6f344169eb74baf3168e05499a9d7577a18fea63fc4d
5
5
  SHA512:
6
- metadata.gz: 6ca46525e80b1d832a703b8cb867466327de333853ee236654f722dc6abc30f46fd149ca9633c42ececcea08db2a01a6d82ab60f8eeb61ebb947f0441436dd96
7
- data.tar.gz: ccd08e78dfb047f82af02eeaf14ccd12fcb05fe882edeb2bf48c2e7ec5b9fd698feccf0b9f9e187aaff97c2e793f671a3f9d32c11e3741656c3df94f944bc808
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.16'
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.generate(moment: time + n)
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
- ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
212
- ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
213
- ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
214
- ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
215
- ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
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(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
228
- ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
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.generate(moment: time).to_s
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(moment: time).to_s
395
+ +ULID.min(time).to_s
314
396
  -ULID.max_ulid_at(time)
315
- +ULID.max(moment: time).to_s
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 SetupError < ScriptError; end
18
+ class UnexpectedError < Error; end
20
19
 
21
- encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
22
- # Crockford's Base32. Excluded I, L, O, U.
23
- # @see https://www.crockford.com/base32.html
24
- ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
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
- TIMESTAMP_PART_LENGTH = 10
27
- RANDOMNESS_PART_LENGTH = 16
28
- ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
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
- # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
39
- UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i.freeze
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
- new milliseconds: milliseconds_from_moment(moment), entropy: entropy
55
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
50
56
  end
51
57
 
52
- # @param [Integer, Time] moment
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: 0)
68
+ def self.min(moment=0)
55
69
  0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
56
70
  end
57
71
 
58
- # @param [Integer, Time] moment
72
+ # @param [Time, Integer] moment
59
73
  # @return [ULID]
60
- def self.max(moment: MAX_MILLISECONDS)
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
- # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
65
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
66
- # @return [ULID]
67
- def self.monotonic_generate
68
- warning = "`ULID.monotonic_generate` actually changes class state. Use `ULID::MonotonicGenerator` instead."
69
- if RUBY_VERSION >= '3.0'
70
- Warning.warn(warning, category: :deprecated)
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
- Warning.warn(warning)
105
+ RANDOM_INTEGER_GENERATOR
73
106
  end
74
107
 
75
- MONOTONIC_GENERATOR.generate
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.to_str
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(PATTERN) do |pair|
86
- yield parse(pair.join)
137
+ string.scan(SCANNING_PATTERN) do |matched|
138
+ yield parse(matched)
87
139
  end
88
140
  self
89
141
  end
90
142
 
91
- # @param [String, #to_str] uuid
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
- integer = integer.to_int
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
- octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
117
- time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
118
- randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
119
- milliseconds = inverse_of_digits(time_octets)
120
- entropy = inverse_of_digits(randomness_octets)
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>] time_range
162
+ # @param [Range<Time>, Range<nil>, Range[ULID]] period
126
163
  # @return [Range<ULID>]
127
- # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
128
- def self.range(time_range)
129
- raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
130
- begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
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 begin_time
170
+ case begin_element
133
171
  when Time
134
- begin_ulid = min(moment: begin_time)
172
+ begin_ulid = min(begin_element)
135
173
  when nil
136
- begin_ulid = min
174
+ begin_ulid = MIN
175
+ when self
176
+ begin_ulid = begin_element
137
177
  else
138
- raise argument_error_for_range_building(time_range)
178
+ raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
139
179
  end
140
180
 
141
- case end_time
181
+ case end_element
142
182
  when Time
143
183
  if exclude_end
144
- end_ulid = min(moment: end_time)
184
+ end_ulid = min(end_element)
145
185
  else
146
- end_ulid = max(moment: end_time)
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 = max
190
+ end_ulid = MAX
151
191
  exclude_end = false
192
+ when self
193
+ end_ulid = end_element
152
194
  else
153
- raise argument_error_for_range_building(time_range)
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
- moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
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
- begin
259
- string = string.to_str
260
- raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
261
- n32encoded = convert_crockford_base32_to_n32(string.upcase)
262
- timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
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
- new milliseconds: milliseconds, entropy: entropy
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
- parse(string)
281
- rescue Exception
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
- # @api private
288
- # @param [Integer] integer
289
- # @param [Integer] length
290
- # @return [Array<Integer>]
291
- def self.octets_from_integer(integer, length:)
292
- digits = integer.digits(256)
293
- (length - digits.size).times do
294
- digits.push 0
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
- # @api private
300
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
301
- # @param [Array<Integer>] reversed_digits
302
- # @return [Integer]
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
- # @return [ArgumentError]
313
- private_class_method def self.argument_error_for_range_building(argument)
314
- ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
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 [void]
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 initialize(milliseconds:, entropy:)
326
- milliseconds = milliseconds.to_int
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 ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
337
+ @string ||= CrockfordBase32.encode(@integer).freeze
339
338
  end
340
339
 
341
340
  # @return [Integer]
342
341
  def to_i
343
- @integer ||= self.class.inverse_of_digits(octets)
342
+ @integer
344
343
  end
345
344
  alias_method :hash, :to_i
346
345
 
347
346
  # @return [Integer, nil]
348
347
  def <=>(other)
349
- other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
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
- other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
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
- self == other
366
+ @integer == other.to_i
368
367
  when String
369
- begin
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
- @octets ||= (timestamp_octets + randomness_octets).freeze
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
- @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
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
- @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
401
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
403
402
  end
404
403
 
405
404
  # @return [String]
406
405
  def timestamp
407
- @timestamp ||= matchdata[:timestamp].freeze
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 ||= matchdata[:randomness].freeze
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
- # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
422
- # @return [Regexp]
423
- def strict_pattern
424
- @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
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 next
429
- next_int = to_i.next
430
- return nil if next_int > MAX_INTEGER
431
- @next ||= self.class.from_integer(next_int)
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 :succ, :next
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
- pre_int = to_i.pred
438
- return nil if pre_int.negative?
439
- @pred ||= self.class.from_integer(pre_int)
440
- end
441
-
442
- # @return [String]
443
- def to_uuidv4
444
- @uuidv4 ||= begin
445
- # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
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
- private
460
+ # @return [self]
461
+ def to_ulid
462
+ self
463
+ end
461
464
 
462
- # @return [MatchData]
463
- def matchdata
464
- @matchdata ||= STRICT_PATTERN.match(to_s).freeze
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
- octets
471
- to_i
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 :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :REPLACING_PATTERN, :REPLACING_MAP
495
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
488
496
  end