ruby-ulid 0.0.15 → 0.1.0

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: 6a620e80f82e91c778329ccf7da8cca96bda4b12e047610b9543768c5ec85d22
4
- data.tar.gz: ea28469d63348758f52e1460b5330ce177ad2281e63060800a0669054758f3ee
3
+ metadata.gz: f0aff39d0f8044f373d8dcdea149f26501e0118dfc0876f060b1b757a4d2a49e
4
+ data.tar.gz: 02df0ea7a5fe5f8dabd1179988cfe4c2574debf3ad7200d11445eef932dd7443
5
5
  SHA512:
6
- metadata.gz: acb07ae797c3a06a6626d34e15dc1056a30586e8bc151a3773770259d2f4047d0708593e702fbd67a7609758165f20a15f208fede068717a5555733535ed4bde
7
- data.tar.gz: 57c9819d86f2a0f002cf3b9a7b76b3a36bc412fa874d50136ec296f1217fb905fa19feb52de2c9d5250547da384580cf882121f0e331054cd665bd113d1f10e3
6
+ metadata.gz: ab036ed40e4f2740de9d37e44d2c6d4d233fa0071f95383accb2334c4d1f85b30b24eea902e0e29c34fefc8749b8caf5d1f9df6c52f93fe2e76c89aa0912d11e
7
+ data.tar.gz: d8ac11f63e5b301ed8b1eacd0d1211fd4b3a0bd79ccae8fe87050833cf305a7345da1dc136b95ecccfcafc6110cf9a281afb0f15db1fe2e01984bf320f82b7e5
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ruby-ulid
2
2
 
3
- A handy `ULID` library
3
+ ## Overview
4
4
 
5
5
  The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
6
6
  This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
@@ -33,16 +33,26 @@ Instead, herein is proposed ULID:
33
33
  - No special characters (URL safe)
34
34
  - Monotonic sort order (correctly detects and handles the same millisecond)
35
35
 
36
- ## Install
36
+ ## Usage
37
+
38
+ ### Install
37
39
 
38
40
  Require Ruby 2.6 or later
39
41
 
42
+ This command will install the latest version into your environment
43
+
40
44
  ```console
41
45
  $ gem install ruby-ulid
42
- #=> Installed
46
+ Should be installed!
43
47
  ```
44
48
 
45
- ## Usage
49
+ Add this line to your application/library's `Gemfile` is needed in basic use-case
50
+
51
+ ```ruby
52
+ gem 'ruby-ulid', '>= 0.1.0', '< 0.2.0'
53
+ ```
54
+
55
+ ### Generator and Parser
46
56
 
47
57
  The generated `ULID` is an object not just a string.
48
58
  It means easily get the timestamps and binary formats.
@@ -52,9 +62,12 @@ require 'ulid'
52
62
 
53
63
  ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
54
64
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
65
+ ulid.milliseconds #=> 1619544442826
55
66
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
67
+ ulid.timestamp #=> "01F4A5Y1YA"
68
+ ulid.randomness #=> "QCYAYCTC7GRMJ9AA"
69
+ ulid.to_i #=> 1957909092946624190749577070267409738
56
70
  ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
57
- ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
58
71
  ```
59
72
 
60
73
  You can get the objects from exists encoded ULIDs
@@ -64,6 +77,8 @@ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259
64
77
  ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
65
78
  ```
66
79
 
80
+ ### Sortable with the timestamp
81
+
67
82
  ULIDs are sortable when they are generated in different timestamp with milliseconds precision
68
83
 
69
84
  ```ruby
@@ -71,19 +86,20 @@ ulids = 1000.times.map do
71
86
  sleep(0.001)
72
87
  ULID.generate
73
88
  end
74
- ulids.sort == ulids #=> true
75
89
  ulids.uniq(&:to_time).size #=> 1000
90
+ ulids.sort == ulids #=> true
76
91
  ```
77
92
 
78
- `ULID.generate` can take fixed `Time` instance
93
+ `ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`
79
94
 
80
95
  ```ruby
81
- time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
96
+ time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
82
97
  ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
83
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)
84
100
 
85
101
  ulids = 1000.times.map do |n|
86
- ULID.generate(moment: time + n)
102
+ ULID.at(time + n)
87
103
  end
88
104
  ulids.sort == ulids #=> true
89
105
  ```
@@ -98,7 +114,9 @@ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in en
98
114
  ulids.sort == ulids #=> false
99
115
  ```
100
116
 
101
- If you want to ensure `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
117
+ ### How to keep `Sortable` even if in same timestamp
118
+
119
+ If you want to prefer `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
102
120
  (Though it starts with new random value when changed the timestamp)
103
121
 
104
122
  ```ruby
@@ -128,20 +146,47 @@ sample_ulids_by_the_time.take(5) #=>
128
146
  ulids.sort == ulids #=> true
129
147
  ```
130
148
 
131
- When filtering ULIDs by `Time`, we should consider to handle the precision.
132
- So this gem provides `ULID.range` to generate `Range[ULID]` from given `Range[Time]`
149
+ ### Filtering IDs with `Time`
150
+
151
+ `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
152
+
153
+ ```ruby
154
+ include_end = ulid1..ulid2
155
+ exclude_end = ulid1...ulid2
156
+
157
+ ulids.grep(one_of_the_above)
158
+ ulids.grep_v(one_of_the_above)
159
+ ```
160
+
161
+ When want to filter ULIDs with `Time`, we should consider to handle the precision.
162
+ So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
133
163
 
134
164
  ```ruby
135
165
  # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
136
166
  include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
137
167
  exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
138
168
 
169
+ # Below patterns are acceptable
170
+ pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
171
+ until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
172
+ until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
173
+ until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
174
+
139
175
  # So you can use the generated range objects as below
140
- ulids.grep(include_end)
141
- ulids.grep(exclude_end)
176
+ ulids.grep(one_of_the_above)
177
+ ulids.grep_v(one_of_the_above)
142
178
  #=> I hope the results should be actually you want!
143
179
  ```
144
180
 
181
+ If you want to manually handle the Time objects, `ULID.floor` returns new `Time` with truncating excess precisions in ULID spec.
182
+
183
+ ```ruby
184
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
185
+ ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
186
+ ```
187
+
188
+ ### Scanner for string (e.g. `JSON`)
189
+
145
190
  For rough operations, `ULID.scan` might be useful.
146
191
 
147
192
  ```ruby
@@ -174,26 +219,45 @@ EOD
174
219
 
175
220
  ULID.scan(json).to_a
176
221
  #=>
177
- [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
178
- ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
179
- ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
180
- ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
181
- ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
182
- ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
222
+ # [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
223
+ # ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
224
+ # ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
225
+ # ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
226
+ # ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
227
+ # ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
228
+ ```
229
+
230
+ `ULID#patterns` is a util for text based operations.
231
+ The results and spec are not fixed. Should not be used except snippets/console operation
232
+
233
+ ```ruby
234
+ ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
235
+ #=> returns like a fallowing Hash
236
+ {
237
+ named_captures: /(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)/i,
238
+ strict_named_captures: /\A(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)\z/i
239
+ }
183
240
  ```
184
241
 
242
+ ### Some methods to help manipulations
243
+
185
244
  `ULID.min` and `ULID.max` return termination values for ULID spec.
186
245
 
246
+ It can take `Time` instance as an optional argument. Then returns min/max ID that has limit of randomness part in the time.
247
+
187
248
  ```ruby
188
249
  ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
189
250
  ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
190
251
 
191
252
  time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
192
- ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
193
- ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
253
+ ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
254
+ ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
194
255
  ```
195
256
 
196
- `ULID#next` and `ULID#succ` returns next(successor) ULID
257
+ `ULID#next` and `ULID#succ` returns next(successor) ULID.
258
+ Especially `ULID#succ` makes it possible `Range[ULID]#each`.
259
+
260
+ NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
197
261
 
198
262
  ```ruby
199
263
  ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
@@ -209,12 +273,66 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
209
273
  ULID.parse('00000000000000000000000000').pred #=> nil
210
274
  ```
211
275
 
276
+ `ULID.sample` returns random ULIDs.
277
+
278
+ Basically ignores generating time.
279
+
280
+ ```ruby
281
+ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
282
+ ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
283
+ ULID.sample(0) #=> []
284
+ ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
285
+ ULID.sample(5)
286
+ #=>
287
+ #[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
288
+ # ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
289
+ # ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
290
+ # ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
291
+ # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
292
+ ```
293
+
294
+ You can specify a range object for the timestamp restriction, see also `ULID.range`.
295
+
296
+ ```ruby
297
+ ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
298
+ ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
299
+ ulids = ULID.sample(1000, period: ulid1..ulid2)
300
+ ulids.uniq.size #=> 1000
301
+ ulids.take(10)
302
+ #=>
303
+ #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
304
+ # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
305
+ # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
306
+ # ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
307
+ # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
308
+ # ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
309
+ # ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
310
+ # ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
311
+ # ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
312
+ # ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
313
+ ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
314
+ #=>
315
+ # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
316
+ # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
317
+ # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
318
+ # ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
319
+ # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
320
+ # ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
321
+ # ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
322
+ # ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
323
+ # ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
324
+ # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
325
+ ```
326
+
212
327
  ### UUIDv4 converter for migration use-cases
213
328
 
214
329
  `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
215
- The imported timestamp is meaningless. So ULID's benefit will lost
330
+ The imported timestamp is meaningless. So ULID's benefit will lost.
216
331
 
217
332
  ```ruby
333
+ # Currently experimental feature, so needed to load the extension.
334
+ require 'ulid/uuid'
335
+
218
336
  # Basically reversible
219
337
  ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
220
338
  ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
@@ -239,6 +357,50 @@ ULID.min == reversed_min #=> false
239
357
  ULID.max == reversed_max #=> false
240
358
  ```
241
359
 
360
+ ## How to migrate from other gems
361
+
362
+ As far as I know, major prior arts are below
363
+
364
+ ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
365
+
366
+ It is just providing basic `String` generator only.
367
+ So you can replace the code as below
368
+
369
+ ```diff
370
+ -ULID.generate
371
+ +ULID.generate.to_s
372
+ ```
373
+
374
+ NOTE: It had crucial issue for handling precision, in version before `1.3.0`, when you extract timestamps from old generated ULIDs, it might be not accurate value.
375
+
376
+ 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
377
+ 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
378
+ 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
379
+
380
+ ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
381
+
382
+ It is providing basic generator(except monotonic generator) and parser.
383
+ Major methods can be replaced as below.
384
+
385
+ ```diff
386
+ -ULID.generate
387
+ +ULID.generate.to_s
388
+ -ULID.at(time)
389
+ +ULID.at(time).to_s
390
+ -ULID.time(string)
391
+ +ULID.parse(string).to_time
392
+ -ULID.min_ulid_at(time)
393
+ +ULID.min(time).to_s
394
+ -ULID.max_ulid_at(time)
395
+ +ULID.max(time).to_s
396
+ ```
397
+
398
+ NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
399
+
400
+ 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
401
+ 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
402
+ 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
403
+
242
404
  ## References
243
405
 
244
406
  - [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
@@ -17,61 +16,113 @@ class ULID
17
16
  class OverflowError < Error; end
18
17
  class ParserError < Error; end
19
18
 
20
- encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
- # Crockford's Base32. Excluded I, L, O, U.
22
- # @see https://www.crockford.com/base32.html
23
- ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
19
+ # Excluded I, L, O, U, -.
20
+ # This is the encoding patterns.
21
+ # The decoding issue is written in ULID::CrockfordBase32
22
+ CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
24
23
 
25
- TIMESTAMP_PART_LENGTH = 10
26
- RANDOMNESS_PART_LENGTH = 16
27
- ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
24
+ TIMESTAMP_ENCODED_LENGTH = 10
25
+ RANDOMNESS_ENCODED_LENGTH = 16
26
+ ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
28
27
  TIMESTAMP_OCTETS_LENGTH = 6
29
28
  RANDOMNESS_OCTETS_LENGTH = 10
30
29
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
30
  MAX_MILLISECONDS = 281474976710655
32
31
  MAX_ENTROPY = 1208925819614629174706175
33
32
  MAX_INTEGER = 340282366920938463463374607431768211455
34
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
35
- STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
33
 
37
- # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
- 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
34
+ # @see https://github.com/ulid/spec/pull/57
35
+ # Currently not used as a constant, but kept as a reference for now.
36
+ 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
37
+
38
+ STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
39
+
40
+ # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
41
+ # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
42
+ SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
39
43
 
40
44
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
45
  # @see https://bugs.ruby-lang.org/issues/15958
42
46
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
43
47
 
48
+ private_class_method :new
49
+
44
50
  # @param [Integer, Time] moment
45
51
  # @param [Integer] entropy
46
52
  # @return [ULID]
47
53
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
48
- new milliseconds: milliseconds_from_moment(moment), entropy: entropy
54
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
49
55
  end
50
56
 
51
- # @param [Integer, Time] moment
57
+ # Short hand of `ULID.generate(moment: time)`
58
+ # @param [Time] time
52
59
  # @return [ULID]
53
- def self.min(moment: 0)
60
+ def self.at(time)
61
+ raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
62
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
63
+ end
64
+
65
+ # @param [Time, Integer] moment
66
+ # @return [ULID]
67
+ def self.min(moment=0)
54
68
  0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
55
69
  end
56
70
 
57
- # @param [Integer, Time] moment
71
+ # @param [Time, Integer] moment
58
72
  # @return [ULID]
59
- def self.max(moment: MAX_MILLISECONDS)
73
+ def self.max(moment=MAX_MILLISECONDS)
60
74
  MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
61
75
  end
62
76
 
63
- # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
64
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
65
- # @return [ULID]
66
- def self.monotonic_generate
67
- warning = "`ULID.monotonic_generate` actually changes class state. Use `ULID::MonotonicGenerator` instead."
68
- if RUBY_VERSION >= '3.0'
69
- Warning.warn(warning, category: :deprecated)
77
+ RANDOM_INTEGER_GENERATOR = -> {
78
+ SecureRandom.random_number(MAX_INTEGER)
79
+ }
80
+
81
+ # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
82
+ # @overload sample(number, period: nil)
83
+ # @param [Integer] number
84
+ # @return [Array<ULID>]
85
+ # @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
86
+ # @overload sample(period: nil)
87
+ # @return [ULID]
88
+ # @note Major difference of `Array#sample` interface is below
89
+ # * Do not ensure the uniqueness
90
+ # * Do not take random generator for the arguments
91
+ # * Raising error instead of truncating elements for the given number
92
+ def self.sample(*args, period: nil)
93
+ int_generator = if period
94
+ ulid_range = range(period)
95
+ min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
96
+
97
+ possibilities = (max - min) + (exclude_end ? 0 : 1)
98
+ raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
99
+
100
+ -> {
101
+ SecureRandom.random_number(possibilities) + min
102
+ }
70
103
  else
71
- Warning.warn(warning)
104
+ RANDOM_INTEGER_GENERATOR
72
105
  end
73
106
 
74
- MONOTONIC_GENERATOR.generate
107
+ case args.size
108
+ when 0
109
+ from_integer(int_generator.call)
110
+ when 1
111
+ number = args.first
112
+ raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
113
+
114
+ if number > MAX_INTEGER || number.negative?
115
+ raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
116
+ end
117
+
118
+ if period && (number > possibilities)
119
+ raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
120
+ end
121
+
122
+ Array.new(number) { from_integer(int_generator.call) }
123
+ else
124
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
125
+ end
75
126
  end
76
127
 
77
128
  # @param [String, #to_str] string
@@ -79,77 +130,68 @@ class ULID
79
130
  # @yieldparam [ULID] ulid
80
131
  # @yieldreturn [self]
81
132
  def self.scan(string)
82
- string = string.to_str
133
+ string = String.try_convert(string)
134
+ raise ArgumentError, 'ULID.scan takes only strings' unless string
83
135
  return to_enum(__callee__, string) unless block_given?
84
- string.scan(PATTERN) do |pair|
85
- yield parse(pair.join)
136
+ string.scan(SCANNING_PATTERN) do |matched|
137
+ yield parse(matched)
86
138
  end
87
139
  self
88
140
  end
89
141
 
90
- # @param [String, #to_str] uuid
91
- # @return [ULID]
92
- # @raise [ParserError] if the given format is not correct for UUIDv4 specs
93
- def self.from_uuidv4(uuid)
94
- begin
95
- uuid = uuid.to_str
96
- prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
97
- raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
98
- normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
99
- from_integer(normalized.to_i(16))
100
- rescue => err
101
- raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
102
- end
103
- end
104
-
105
- # @param [Integer, #to_int] integer
142
+ # @param [Integer] integer
106
143
  # @return [ULID]
107
144
  # @raise [OverflowError] if the given integer is larger than the ULID limit
108
145
  # @raise [ArgumentError] if the given integer is negative number
109
- # @todo Need optimized for performance
110
146
  def self.from_integer(integer)
111
- integer = integer.to_int
147
+ raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
112
148
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
113
149
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
114
150
 
115
- octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
116
- time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
117
- randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
118
- milliseconds = inverse_of_digits(time_octets)
119
- entropy = inverse_of_digits(randomness_octets)
151
+ n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
152
+ n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
153
+ n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
154
+
155
+ milliseconds = n32encoded_timestamp.to_i(32)
156
+ entropy = n32encoded_randomness.to_i(32)
120
157
 
121
- new milliseconds: milliseconds, entropy: entropy
158
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
122
159
  end
123
160
 
124
- # @param [Range<Time>, Range<nil>] time_range
161
+ # @param [Range<Time>, Range<nil>, Range[ULID]] period
125
162
  # @return [Range<ULID>]
126
- # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
127
- def self.range(time_range)
128
- raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
129
- begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
163
+ # @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
164
+ def self.range(period)
165
+ raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period
166
+ begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
167
+ return period if self === begin_element && self === end_element
130
168
 
131
- case begin_time
169
+ case begin_element
132
170
  when Time
133
- begin_ulid = min(moment: begin_time)
171
+ begin_ulid = min(begin_element)
134
172
  when nil
135
- begin_ulid = min
173
+ begin_ulid = MIN
174
+ when self
175
+ begin_ulid = begin_element
136
176
  else
137
- raise argument_error_for_range_building(time_range)
177
+ raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
138
178
  end
139
179
 
140
- case end_time
180
+ case end_element
141
181
  when Time
142
182
  if exclude_end
143
- end_ulid = min(moment: end_time)
183
+ end_ulid = min(end_element)
144
184
  else
145
- end_ulid = max(moment: end_time)
185
+ end_ulid = max(end_element)
146
186
  end
147
187
  when nil
148
188
  # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
149
- end_ulid = max
189
+ end_ulid = MAX
150
190
  exclude_end = false
191
+ when self
192
+ end_ulid = end_element
151
193
  else
152
- raise argument_error_for_range_building(time_range)
194
+ raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
153
195
  end
154
196
 
155
197
  begin_ulid.freeze
@@ -161,6 +203,8 @@ class ULID
161
203
  # @param [Time] time
162
204
  # @return [Time]
163
205
  def self.floor(time)
206
+ raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
207
+
164
208
  if RUBY_VERSION >= '2.7'
165
209
  time.floor(3)
166
210
  else
@@ -168,23 +212,34 @@ class ULID
168
212
  end
169
213
  end
170
214
 
215
+ # @api private
171
216
  # @return [Integer]
172
217
  def self.current_milliseconds
173
218
  milliseconds_from_time(Time.now)
174
219
  end
175
220
 
221
+ # @api private
176
222
  # @param [Time] time
177
223
  # @return [Integer]
178
- def self.milliseconds_from_time(time)
224
+ private_class_method def self.milliseconds_from_time(time)
179
225
  (time.to_r * 1000).to_i
180
226
  end
181
227
 
228
+ # @api private
182
229
  # @param [Time, Integer] moment
183
230
  # @return [Integer]
184
231
  def self.milliseconds_from_moment(moment)
185
- moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
232
+ case moment
233
+ when Integer
234
+ moment
235
+ when Time
236
+ milliseconds_from_time(moment)
237
+ else
238
+ raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
239
+ end
186
240
  end
187
241
 
242
+ # @api private
188
243
  # @return [Integer]
189
244
  def self.reasonable_entropy
190
245
  SecureRandom.random_number(MAX_ENTROPY)
@@ -193,61 +248,41 @@ class ULID
193
248
  # @param [String, #to_str] string
194
249
  # @return [ULID]
195
250
  # @raise [ParserError] if the given format is not correct for ULID specs
196
- # @raise [OverflowError] if the given value is larger than the ULID limit
197
251
  def self.parse(string)
198
- begin
199
- string = string.to_str
200
- unless string.size == ENCODED_ID_LENGTH
201
- raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
202
- end
203
- timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
204
- randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
205
- milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
206
- entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
207
- rescue => err
208
- 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}`"
209
257
  end
210
-
211
- new milliseconds: milliseconds, entropy: entropy
258
+
259
+ from_integer(CrockfordBase32.decode(string))
212
260
  end
213
261
 
262
+ # @param [String, #to_str] string
214
263
  # @return [Boolean]
215
264
  def self.valid?(string)
216
- parse(string)
217
- rescue Exception
218
- false
219
- else
220
- true
265
+ string = String.try_convert(string)
266
+ string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
221
267
  end
222
268
 
223
269
  # @api private
224
- # @param [Integer] integer
225
- # @param [Integer] length
226
- # @return [Array<Integer>]
227
- def self.octets_from_integer(integer, length:)
228
- digits = integer.digits(256)
229
- (length - digits.size).times do
230
- digits.push 0
231
- end
232
- digits.reverse!
233
- end
270
+ # @param [Integer] milliseconds
271
+ # @param [Integer] entropy
272
+ # @return [ULID]
273
+ # @raise [OverflowError] if the given value is larger than the ULID limit
274
+ # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
275
+ def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
276
+ raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
277
+ raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
278
+ raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
279
+ raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
234
280
 
235
- # @api private
236
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
237
- # @param [Array<Integer>] reversed_digits
238
- # @return [Integer]
239
- def self.inverse_of_digits(reversed_digits)
240
- base = 256
241
- num = 0
242
- reversed_digits.each do |digit|
243
- num = (num * base) + digit
244
- end
245
- num
246
- end
281
+ n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
282
+ n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
283
+ integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)
247
284
 
248
- # @return [ArgumentError]
249
- private_class_method def self.argument_error_for_range_building(argument)
250
- ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
285
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
251
286
  end
252
287
 
253
288
  attr_reader :milliseconds, :entropy
@@ -255,34 +290,29 @@ class ULID
255
290
  # @api private
256
291
  # @param [Integer] milliseconds
257
292
  # @param [Integer] entropy
293
+ # @param [Integer] integer
258
294
  # @return [void]
259
- # @raise [OverflowError] if the given value is larger than the ULID limit
260
- # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
261
- def initialize(milliseconds:, entropy:)
262
- milliseconds = milliseconds.to_int
263
- entropy = entropy.to_int
264
- raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
265
- raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
266
- raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
267
-
295
+ def initialize(milliseconds:, entropy:, integer:)
296
+ # All arguments check should be done with each constructors, not here
297
+ @integer = integer
268
298
  @milliseconds = milliseconds
269
299
  @entropy = entropy
270
300
  end
271
301
 
272
302
  # @return [String]
273
303
  def to_s
274
- @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
304
+ @string ||= CrockfordBase32.encode(@integer).freeze
275
305
  end
276
306
 
277
307
  # @return [Integer]
278
308
  def to_i
279
- @integer ||= self.class.inverse_of_digits(octets)
309
+ @integer
280
310
  end
281
311
  alias_method :hash, :to_i
282
312
 
283
313
  # @return [Integer, nil]
284
314
  def <=>(other)
285
- other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
315
+ (ULID === other) ? (@integer <=> other.to_i) : nil
286
316
  end
287
317
 
288
318
  # @return [String]
@@ -292,7 +322,7 @@ class ULID
292
322
 
293
323
  # @return [Boolean]
294
324
  def eql?(other)
295
- other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
325
+ equal?(other) || (ULID === other && @integer == other.to_i)
296
326
  end
297
327
  alias_method :==, :eql?
298
328
 
@@ -300,13 +330,9 @@ class ULID
300
330
  def ===(other)
301
331
  case other
302
332
  when ULID
303
- self == other
333
+ @integer == other.to_i
304
334
  when String
305
- begin
306
- self == self.class.parse(other)
307
- rescue Exception
308
- false
309
- end
335
+ to_s == other.upcase
310
336
  else
311
337
  false
312
338
  end
@@ -325,62 +351,69 @@ class ULID
325
351
 
326
352
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
327
353
  def octets
328
- @octets ||= (timestamp_octets + randomness_octets).freeze
354
+ digits = @integer.digits(256)
355
+ (OCTETS_LENGTH - digits.size).times do
356
+ digits.push 0
357
+ end
358
+ digits.reverse!
329
359
  end
330
360
 
331
361
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
332
362
  def timestamp_octets
333
- @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
363
+ octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
334
364
  end
335
365
 
336
366
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
337
367
  def randomness_octets
338
- @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
368
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
339
369
  end
340
370
 
341
371
  # @return [String]
342
372
  def timestamp
343
- @timestamp ||= matchdata[:timestamp].freeze
373
+ @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
344
374
  end
345
375
 
346
376
  # @return [String]
347
377
  def randomness
348
- @randomness ||= matchdata[:randomness].freeze
378
+ @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
349
379
  end
350
380
 
351
- # @return [Regexp]
352
- def pattern
353
- @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
354
- end
355
-
356
- # @return [Regexp]
357
- def strict_pattern
358
- @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
381
+ # @note Providing for rough operations. The keys and values is not fixed.
382
+ # @return [Hash{Symbol => Regexp, String}]
383
+ def patterns
384
+ named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
385
+ {
386
+ named_captures: named_captures,
387
+ strict_named_captures: /\A#{named_captures.source}\z/i.freeze
388
+ }
359
389
  end
360
390
 
361
391
  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
362
- def next
363
- next_int = to_i.next
364
- return nil if next_int > MAX_INTEGER
365
- @next ||= self.class.from_integer(next_int)
392
+ def succ
393
+ succ_int = @integer.succ
394
+ if succ_int >= MAX_INTEGER
395
+ if succ_int == MAX_INTEGER
396
+ MAX
397
+ else
398
+ nil
399
+ end
400
+ else
401
+ ULID.from_integer(succ_int)
402
+ end
366
403
  end
367
- alias_method :succ, :next
404
+ alias_method :next, :succ
368
405
 
369
406
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
370
407
  def pred
371
- pre_int = to_i.pred
372
- return nil if pre_int.negative?
373
- @pred ||= self.class.from_integer(pre_int)
374
- end
375
-
376
- # @return [String]
377
- def to_uuidv4
378
- @uuidv4 ||= begin
379
- # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
380
- array = octets.pack('C*').unpack('NnnnnN')
381
- array[2] = (array[2] & 0x0fff) | 0x4000
382
- array[3] = (array[3] & 0x3fff) | 0x8000
383
- ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
408
+ pred_int = @integer.pred
409
+ if pred_int <= 0
410
+ if pred_int == 0
411
+ MIN
412
+ else
413
+ nil
414
+ end
415
+ else
416
+ ULID.from_integer(pred_int)
384
417
  end
385
418
  end
386
419
 
@@ -391,32 +424,35 @@ class ULID
391
424
  super
392
425
  end
393
426
 
394
- private
427
+ # @return [self]
428
+ def dup
429
+ self
430
+ end
395
431
 
396
- # @return [MatchData]
397
- def matchdata
398
- @matchdata ||= STRICT_PATTERN.match(to_s).freeze
432
+ # @return [self]
433
+ def clone(freeze: true)
434
+ self
399
435
  end
400
436
 
437
+ undef_method :instance_variable_set
438
+
439
+ private
440
+
401
441
  # @return [void]
402
442
  def cache_all_instance_variables
403
443
  inspect
404
- octets
405
- to_i
406
- succ
407
- pred
408
- strict_pattern
409
- to_uuidv4
444
+ timestamp
445
+ randomness
410
446
  end
411
447
  end
412
448
 
413
449
  require_relative 'ulid/version'
450
+ require_relative 'ulid/crockford_base32'
414
451
  require_relative 'ulid/monotonic_generator'
415
452
 
416
453
  class ULID
417
454
  MIN = parse('00000000000000000000000000').freeze
418
455
  MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
419
- MONOTONIC_GENERATOR = MonotonicGenerator.new
420
456
 
421
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX
457
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
422
458
  end
@@ -0,0 +1,68 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ class ULID
6
+ # Currently supporting only for `subset` for actual use-case`
7
+ # Original decoding spec allows other characters.
8
+ # But I think ULID should allow `subset` of Crockford's Base32.
9
+ # See below
10
+ # * https://github.com/ulid/spec/pull/57
11
+ # * https://github.com/kachick/ruby-ulid/issues/57
12
+ # * https://github.com/kachick/ruby-ulid/issues/78
13
+ module CrockfordBase32
14
+ class SetupError < ScriptError; end
15
+
16
+ n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
17
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
18
+
19
+ n32_char_by_number = {}
20
+ n32_chars.each_with_index do |char, index|
21
+ n32_char_by_number[index] = char
22
+ end
23
+ n32_char_by_number.freeze
24
+
25
+ crockford_base32_mappings = {
26
+ 'J' => 18,
27
+ 'K' => 19,
28
+ 'M' => 20,
29
+ 'N' => 21,
30
+ 'P' => 22,
31
+ 'Q' => 23,
32
+ 'R' => 24,
33
+ 'S' => 25,
34
+ 'T' => 26,
35
+ 'V' => 27,
36
+ 'W' => 28,
37
+ 'X' => 29,
38
+ 'Y' => 30,
39
+ 'Z' => 31
40
+ }.freeze
41
+
42
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR = CROCKFORD_BASE32_ENCODING_STRING.chars.map(&:freeze).each_with_object({}) do |encoding_char, map|
43
+ if n = crockford_base32_mappings[encoding_char]
44
+ char_32 = n32_char_by_number.fetch(n)
45
+ map[encoding_char] = char_32
46
+ end
47
+ end.freeze
48
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
49
+ CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
50
+
51
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
52
+ N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
53
+
54
+ # @param [String] string
55
+ # @return [Integer]
56
+ def self.decode(string)
57
+ n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
58
+ n32encoded.to_i(32)
59
+ end
60
+
61
+ # @param [Integer] integer
62
+ # @return [String]
63
+ def self.encode(integer)
64
+ n32encoded = integer.to_s(32)
65
+ n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
66
+ end
67
+ end
68
+ end
@@ -8,6 +8,7 @@ class ULID
8
8
  attr_accessor :latest_milliseconds, :latest_entropy
9
9
 
10
10
  def initialize
11
+ @mutex = Thread::Mutex.new
11
12
  reset
12
13
  end
13
14
 
@@ -19,14 +20,15 @@ class ULID
19
20
  milliseconds = ULID.milliseconds_from_moment(moment)
20
21
  raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
21
22
 
22
- if @latest_milliseconds < milliseconds
23
- @latest_milliseconds = milliseconds
24
- @latest_entropy = ULID.reasonable_entropy
25
- else
26
- @latest_entropy += 1
23
+ @mutex.synchronize do
24
+ if @latest_milliseconds < milliseconds
25
+ @latest_milliseconds = milliseconds
26
+ @latest_entropy = ULID.reasonable_entropy
27
+ else
28
+ @latest_entropy += 1
29
+ end
30
+ ULID.from_milliseconds_and_entropy(milliseconds: @latest_milliseconds, entropy: @latest_entropy)
27
31
  end
28
-
29
- ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
30
32
  end
31
33
 
32
34
  # @api private
data/lib/ulid/uuid.rb ADDED
@@ -0,0 +1,38 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ # Extracted features around UUID from some reasons
6
+ # ref:
7
+ # * https://github.com/kachick/ruby-ulid/issues/105
8
+ # * https://github.com/kachick/ruby-ulid/issues/76
9
+ class ULID
10
+ # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
11
+ 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
12
+ private_constant :UUIDV4_PATTERN
13
+
14
+ # @param [String, #to_str] uuid
15
+ # @return [ULID]
16
+ # @raise [ParserError] if the given format is not correct for UUIDv4 specs
17
+ def self.from_uuidv4(uuid)
18
+ uuid = String.try_convert(uuid)
19
+ raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid
20
+
21
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
22
+ unless UUIDV4_PATTERN.match?(prefix_trimmed)
23
+ raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
24
+ end
25
+
26
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
27
+ from_integer(normalized.to_i(16))
28
+ end
29
+
30
+ # @return [String]
31
+ def to_uuidv4
32
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
33
+ array = octets.pack('C*').unpack('NnnnnN')
34
+ array[2] = (array[2] & 0x0fff) | 0x4000
35
+ array[3] = (array[3] & 0x3fff) | 0x8000
36
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
37
+ end
38
+ end
data/lib/ulid/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  class ULID
5
- VERSION = '0.0.15'
5
+ VERSION = '0.1.0'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -1,10 +1,10 @@
1
1
  # Classes
2
2
  class ULID
3
3
  VERSION: String
4
- ENCODING_CHARS: Array[String]
5
- TIMESTAMP_PART_LENGTH: 10
6
- RANDOMNESS_PART_LENGTH: 16
7
- ENCODED_ID_LENGTH: 26
4
+ CROCKFORD_BASE32_ENCODING_STRING: String
5
+ TIMESTAMP_ENCODED_LENGTH: 10
6
+ RANDOMNESS_ENCODED_LENGTH: 16
7
+ ENCODED_LENGTH: 26
8
8
  TIMESTAMP_OCTETS_LENGTH: 6
9
9
  RANDOMNESS_OCTETS_LENGTH: 10
10
10
  OCTETS_LENGTH: 16
@@ -12,10 +12,11 @@ class ULID
12
12
  MAX_ENTROPY: 1208925819614629174706175
13
13
  MAX_INTEGER: 340282366920938463463374607431768211455
14
14
  TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
15
- PATTERN: Regexp
16
- STRICT_PATTERN: Regexp
15
+ RANDOM_INTEGER_GENERATOR: ^() -> Integer
16
+ PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
17
+ STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
18
+ SCANNING_PATTERN: Regexp
17
19
  UUIDV4_PATTERN: Regexp
18
- MONOTONIC_GENERATOR: MonotonicGenerator
19
20
  MIN: ULID
20
21
  MAX: ULID
21
22
  include Comparable
@@ -32,6 +33,19 @@ class ULID
32
33
  class ParserError < Error
33
34
  end
34
35
 
36
+ module CrockfordBase32
37
+ class SetupError < ScriptError
38
+ end
39
+
40
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
41
+ CROCKFORD_BASE32_CHAR_PATTERN: Regexp
42
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
43
+ N32_CHAR_PATTERN: Regexp
44
+
45
+ def self.encode: (Integer integer) -> String
46
+ def self.decode: (String string) -> Integer
47
+ end
48
+
35
49
  class MonotonicGenerator
36
50
  attr_accessor latest_milliseconds: Integer
37
51
  attr_accessor latest_entropy: Integer
@@ -44,50 +58,43 @@ class ULID
44
58
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
45
59
  type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
46
60
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
61
+ type period = Range[Time] | Range[nil] | Range[ULID]
47
62
 
48
63
  @milliseconds: Integer
49
64
  @entropy: Integer
50
65
  @string: String?
51
- @integer: Integer?
52
- @octets: octets?
53
- @timestamp_octets: timestamp_octets?
54
- @randomness_octets: randomness_octets?
66
+ @integer: Integer
55
67
  @timestamp: String?
56
68
  @randomness: String?
57
69
  @inspect: String?
58
70
  @time: Time?
59
- @next: ULID?
60
- @pattern: Regexp?
61
- @strict_pattern: Regexp?
62
- @uuidv4: String?
63
- @matchdata: MatchData?
64
71
 
65
- def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
66
- def self.monotonic_generate: -> ULID
72
+ def self.generate: (?moment: moment, ?entropy: Integer) -> self
73
+ def self.at: (Time time) -> self
67
74
  def self.current_milliseconds: -> Integer
68
- def self.milliseconds_from_time: (Time time) -> Integer
69
75
  def self.milliseconds_from_moment: (moment moment) -> Integer
70
- def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
76
+ def self.range: (period period) -> Range[ULID]
71
77
  def self.floor: (Time time) -> Time
72
78
  def self.reasonable_entropy: -> Integer
73
- def self.parse: (String string) -> ULID
79
+ def self.parse: (String string) -> self
74
80
  def self.from_uuidv4: (String uuid) -> ULID
75
- def self.from_integer: (Integer integer) -> ULID
76
- def self.min: (?moment: moment) -> ULID
77
- def self.max: (?moment: moment) -> ULID
81
+ def self.from_integer: (Integer integer) -> self
82
+ def self.min: (?moment moment) -> ULID
83
+ def self.max: (?moment moment) -> ULID
84
+ def self.sample: (?period: period) -> self
85
+ | (Integer number, ?period: period) -> Array[self]
78
86
  def self.valid?: (untyped string) -> bool
79
- def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
80
- | (String string) { (ULID ulid) -> void } -> singleton(ULID)
81
- def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
82
- def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
87
+ def self.scan: (String string) -> Enumerator[self, singleton(ULID)]
88
+ | (String string) { (self ulid) -> void } -> singleton(ULID)
89
+ def self.from_milliseconds_and_entropy: (milliseconds: Integer, entropy: Integer) -> self
83
90
  attr_reader milliseconds: Integer
84
91
  attr_reader entropy: Integer
85
- def initialize: (milliseconds: Integer, entropy: Integer) -> void
92
+ def initialize: (milliseconds: Integer, entropy: Integer, integer: Integer) -> void
86
93
  def to_s: -> String
87
94
  def to_i: -> Integer
88
95
  alias hash to_i
89
96
  def <=>: (ULID other) -> Integer
90
- | (untyped other) -> Integer?
97
+ | (untyped other) -> nil
91
98
  def inspect: -> String
92
99
  def eql?: (untyped other) -> bool
93
100
  alias == eql?
@@ -95,8 +102,7 @@ class ULID
95
102
  def to_time: -> Time
96
103
  def timestamp: -> String
97
104
  def randomness: -> String
98
- def pattern: -> Regexp
99
- def strict_pattern: -> Regexp
105
+ def patterns: -> Hash[Symbol, Regexp | String]
100
106
  def octets: -> octets
101
107
  def timestamp_octets: -> timestamp_octets
102
108
  def randomness_octets: -> randomness_octets
@@ -105,9 +111,11 @@ class ULID
105
111
  alias succ next
106
112
  def pred: -> ULID?
107
113
  def freeze: -> self
114
+ def dup: -> self
115
+ # Same as https://github.com/ruby/rbs/blob/4fb4c33b2325d1a73d79ff7aaeb49f21cec1e0e5/core/object.rbs#L79
116
+ def clone: (?freeze: bool) -> self
108
117
 
109
118
  private
110
- def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
111
- def matchdata: -> MatchData
119
+ def self.milliseconds_from_time: (Time time) -> Integer
112
120
  def cache_all_instance_variables: -> void
113
121
  end
metadata CHANGED
@@ -1,35 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Kamiya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-04 00:00:00.000000000 Z
11
+ date: 2021-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: integer-base
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 0.1.2
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: 0.2.0
23
- type: :runtime
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 0.1.2
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: 0.2.0
33
13
  - !ruby/object:Gem::Dependency
34
14
  name: rbs
35
15
  requirement: !ruby/object:Gem::Requirement
@@ -84,10 +64,10 @@ dependencies:
84
64
  - - "<"
85
65
  - !ruby/object:Gem::Version
86
66
  version: '2'
87
- description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
88
- useful specs for applications (e.g. `Database key`). \n This gem aims to provide
89
- the generator, monotonic generator, parser and handy manipulation features around
90
- the ULID.\n Also providing `rbs` signature files.\n"
67
+ description: |2
68
+ The ULID(Universally Unique Lexicographically Sortable Identifier) has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
69
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
70
+ Also providing `ruby/rbs` signature files.
91
71
  email:
92
72
  - kachick1+ruby@gmail.com
93
73
  executables: []
@@ -97,7 +77,9 @@ files:
97
77
  - LICENSE
98
78
  - README.md
99
79
  - lib/ulid.rb
80
+ - lib/ulid/crockford_base32.rb
100
81
  - lib/ulid/monotonic_generator.rb
82
+ - lib/ulid/uuid.rb
101
83
  - lib/ulid/version.rb
102
84
  - sig/ulid.rbs
103
85
  homepage: https://github.com/kachick/ruby-ulid