ruby-ulid 0.0.16 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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