ruby-ulid 0.0.15 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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