ruby-ulid 0.0.12 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +197 -58
  3. data/lib/ulid.rb +169 -39
  4. data/lib/ulid/version.rb +1 -1
  5. data/sig/ulid.rbs +21 -4
  6. metadata +11 -18
  7. data/Steepfile +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0adfc974863e1b2d248eac28305322e20d6b2dc94465b7ca3462fdd2c8676352
4
- data.tar.gz: caecb83a030fa1eda5534e748ee3206208ebf25a1d9f76d33ad04030b4ea0f0c
3
+ metadata.gz: 29a9e3cd7ec91b93c24691976fef74ea5ab0cbc8f4fa237f449fa9322e42c6d2
4
+ data.tar.gz: 9a3c0e12e7d2fe8b6057662ebf6a392c749f7254ee41c4e77221a2e9f45ff6ac
5
5
  SHA512:
6
- metadata.gz: 4a216a8032385e9be00252fb0d9faf1aaa18bdfe75a0ae4b0a2cb1aa438a8786a7e9c477debd87fb6871274c0a50e1579cee41584513199c47b0198c51c6ac6a
7
- data.tar.gz: 664d85fe6070a9c0017e831727a33a87799d1b10e7c4992b44b77be288fcc3cab51ecb528ce01fe0ce5d7a6262f61a2786ce30c54dcd90e8ef36c698f3f76f45
6
+ metadata.gz: 53a12c330c32f76f13e651cef96ce29a6a442cf3dc7742faf2409a0ee17c7dfdcee1031c17696cd51d7bd3cf5b22b2df34b8be5a203cd1d4fd5df079eb357b82
7
+ data.tar.gz: f6c8f8850272353bbd32cc15d437d0b021c13d9bc93c5c988255d6c50fe21ff41aef5b719261ca95b00c129024fe325e2d3c8683c70f9b55e89a09a261fff31f
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # ruby-ulid
2
2
 
3
- A handy `ULID` library
3
+ ## Overview
4
4
 
5
- The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
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.
7
- Also providing rbs signature files.
7
+ Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
8
8
 
9
9
  ---
10
10
 
@@ -28,19 +28,31 @@ Instead, herein is proposed ULID:
28
28
  - 1.21e+24 unique ULIDs per millisecond
29
29
  - Lexicographically sortable!
30
30
  - Canonically encoded as a 26 character string, as opposed to the 36 character UUID
31
- - Uses Crockford's base32 for better efficiency and readability (5 bits per character)
31
+ - Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) # See also exists issues in [Note](#note)
32
32
  - Case insensitive
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
39
+
40
+ Require Ruby 2.6 or later
41
+
42
+ This command will install the latest version into your environment
37
43
 
38
44
  ```console
39
45
  $ gem install ruby-ulid
40
- #=> Installed
46
+ Should be installed!
41
47
  ```
42
48
 
43
- ## 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.0.16'
53
+ ```
54
+
55
+ ### Generator and Parser
44
56
 
45
57
  The generated `ULID` is an object not just a string.
46
58
  It means easily get the timestamps and binary formats.
@@ -52,36 +64,17 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
52
64
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
53
65
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
54
66
  ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
55
- ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
56
- ```
57
-
58
- Generator can take `Time` instance
59
-
60
- ```ruby
61
- time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
62
- ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
63
- ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
64
-
65
- ulids = 1000.times.map do
66
- ULID.generate(moment: time)
67
- end
68
- ulids.sort == ulids #=> false
69
-
70
- ulids = 1000.times.map do |n|
71
- ULID.generate(moment: time + n)
72
- end
73
- ulids.sort == ulids #=> true
74
67
  ```
75
68
 
76
- You can parse from exists IDs
77
-
78
- FYI: Current parser/validator/matcher implementation aims `strict`, It might be changed in [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57).
69
+ You can get the objects from exists encoded ULIDs
79
70
 
80
71
  ```ruby
81
72
  ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
82
73
  ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
83
74
  ```
84
75
 
76
+ ### Sortable with the timestamp
77
+
85
78
  ULIDs are sortable when they are generated in different timestamp with milliseconds precision
86
79
 
87
80
  ```ruby
@@ -89,8 +82,21 @@ ulids = 1000.times.map do
89
82
  sleep(0.001)
90
83
  ULID.generate
91
84
  end
92
- ulids.sort == ulids #=> true
93
85
  ulids.uniq(&:to_time).size #=> 1000
86
+ ulids.sort == ulids #=> true
87
+ ```
88
+
89
+ `ULID.generate` can take fixed `Time` instance
90
+
91
+ ```ruby
92
+ time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
93
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
94
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
95
+
96
+ ulids = 1000.times.map do |n|
97
+ ULID.generate(moment: time + n)
98
+ end
99
+ ulids.sort == ulids #=> true
94
100
  ```
95
101
 
96
102
  The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
@@ -103,32 +109,72 @@ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in en
103
109
  ulids.sort == ulids #=> false
104
110
  ```
105
111
 
106
- If you want to prefer `sortable` rather than the `strict randomness`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
112
+ ### How to keep `Sortable` even if in same timestamp
113
+
114
+ 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.
107
115
  (Though it starts with new random value when changed the timestamp)
108
116
 
109
117
  ```ruby
110
118
  monotonic_generator = ULID::MonotonicGenerator.new
111
- monotonic_ulids = 10000.times.map do
119
+ ulids = 10000.times.map do
112
120
  monotonic_generator.generate
113
121
  end
114
- sample_ulids_by_the_time = monotonic_ulids.uniq(&:to_time)
115
- sample_ulids_by_the_time.size #=> 34 (the size is not fixed, might be changed in environment)
116
- sample_ulids_by_the_time.take(10).map(&:randomness)
117
- #=>
118
- ["JZW56CTA8704D5AQ",
119
- "JGEBH2A2B2EA97MW",
120
- "0XPE4NS3MZH0NAJ4",
121
- "E0S3ZAVADFBPW57Y",
122
- "E5CX1T6281443THQ",
123
- "3SK8WHSH03CVF7J2",
124
- "DDS35BT0R20P3V49",
125
- "60KG2W9FVEN1ZX8C",
126
- "X59YJVXXVH7AXJJE",
127
- "1ZBQ7SNGFKXGH1Y4"]
122
+ sample_ulids_by_the_time = ulids.uniq(&:to_time)
123
+ sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
124
+
125
+ # In same milliseconds creation, it just increments the end of randomness part
126
+ ulids.take(5) #=>
127
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
128
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
129
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
130
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
131
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
132
+
133
+ # When the milliseconds is updated, it starts with new randomness
134
+ sample_ulids_by_the_time.take(5) #=>
135
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
136
+ # ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
137
+ # ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K),
138
+ # ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
139
+ # ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
128
140
 
129
- monotonic_ulids.sort == monotonic_ulids #=> true
141
+ ulids.sort == ulids #=> true
142
+ ```
143
+
144
+ ### Filtering IDs with `Time`
145
+
146
+ `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
147
+
148
+ ```ruby
149
+ include_end = ulid1..ulid2
150
+ exclude_end = ulid1...ulid2
151
+
152
+ ulids.grep(one_of_the_above)
153
+ ulids.grep_v(one_of_the_above)
154
+ ```
155
+
156
+ When want to filter ULIDs with `Time`, we should consider to handle the precision.
157
+ So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
158
+
159
+ ```ruby
160
+ # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
161
+ include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
162
+ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
163
+
164
+ # Below patterns are acceptable
165
+ pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
166
+ 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)
167
+ until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
168
+ until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
169
+
170
+ # So you can use the generated range objects as below
171
+ ulids.grep(one_of_the_above)
172
+ ulids.grep_v(one_of_the_above)
173
+ #=> I hope the results should be actually you want!
130
174
  ```
131
175
 
176
+ ### Scanner for string (e.g. `JSON`)
177
+
132
178
  For rough operations, `ULID.scan` might be useful.
133
179
 
134
180
  ```ruby
@@ -161,14 +207,16 @@ EOD
161
207
 
162
208
  ULID.scan(json).to_a
163
209
  #=>
164
- [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
165
- ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
166
- ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
167
- ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
168
- ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
169
- ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
210
+ # [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
211
+ # ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
212
+ # ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
213
+ # ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
214
+ # ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
215
+ # ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
170
216
  ```
171
217
 
218
+ ### Some methods to help manipulations
219
+
172
220
  `ULID.min` and `ULID.max` return termination values for ULID spec.
173
221
 
174
222
  ```ruby
@@ -180,7 +228,10 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
180
228
  ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
181
229
  ```
182
230
 
183
- `ULID#next` and `ULID#succ` returns next(successor) ULID
231
+ `ULID#next` and `ULID#succ` returns next(successor) ULID.
232
+ Especially `ULID#succ` makes it possible `Range[ULID]#each`.
233
+
234
+ NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
184
235
 
185
236
  ```ruby
186
237
  ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
@@ -196,15 +247,103 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
196
247
  ULID.parse('00000000000000000000000000').pred #=> nil
197
248
  ```
198
249
 
199
- UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
250
+ `ULID.sample` returns random ULIDs ignoring the generating time
251
+
252
+ ```ruby
253
+ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
254
+ ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
255
+ ULID.sample(0) #=> []
256
+ ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
257
+ ULID.sample(5)
258
+ #=>
259
+ #[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
260
+ # ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
261
+ # ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
262
+ # ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
263
+ # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
264
+ ```
265
+
266
+ ### UUIDv4 converter for migration use-cases
267
+
268
+ `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
269
+ The imported timestamp is meaningless. So ULID's benefit will lost
200
270
 
201
271
  ```ruby
202
- ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
203
- #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
272
+ # Basically reversible
273
+ ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
274
+ ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
275
+
276
+ uuid_v4s = 10000.times.map { SecureRandom.uuid }
277
+ uuid_v4s.uniq.size == 10000 #=> Probably `true`
278
+
279
+ ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
280
+ ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
281
+
282
+ # NOTE: Some boundary values are not reversible. See below.
283
+
284
+ ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
285
+ ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
286
+
287
+ # These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
288
+ reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
289
+ reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
290
+
291
+ # But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
292
+ ULID.min == reversed_min #=> false
293
+ ULID.max == reversed_max #=> false
294
+ ```
295
+
296
+ ## How to migrate from other gems
297
+
298
+ As far as I know, major prior arts are below
299
+
300
+ ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
301
+
302
+ It is just providing basic `String` generator only.
303
+ So you can replace the code as below
304
+
305
+ ```diff
306
+ -ULID.generate
307
+ +ULID.generate.to_s
204
308
  ```
205
309
 
310
+ 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.
311
+
312
+ 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
313
+ 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
314
+ 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
315
+
316
+ ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
317
+
318
+ It is providing basic generator(except monotonic generator) and parser.
319
+ Major methods can be replaced as below.
320
+
321
+ ```diff
322
+ -ULID.generate
323
+ +ULID.generate.to_s
324
+ -ULID.at(time)
325
+ +ULID.generate(moment: time).to_s
326
+ -ULID.time(string)
327
+ +ULID.parse(string).to_time
328
+ -ULID.min_ulid_at(time)
329
+ +ULID.min(moment: time).to_s
330
+ -ULID.max_ulid_at(time)
331
+ +ULID.max(moment: time).to_s
332
+ ```
333
+
334
+ NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
335
+
336
+ 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
337
+ 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
338
+ 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
339
+
206
340
  ## References
207
341
 
342
+ - [Repository](https://github.com/kachick/ruby-ulid)
208
343
  - [API documents](https://kachick.github.io/ruby-ulid/)
209
344
  - [ulid/spec](https://github.com/ulid/spec)
210
- - [Another choices are UUIDv6, UUIDv7, UUIDv8. But they are still in draft state](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html)
345
+
346
+ ## Note
347
+
348
+ - Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
349
+ - Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
data/lib/ulid.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  # Copyright (C) 2021 Kenichi Kamiya
4
4
 
5
5
  require 'securerandom'
6
- require 'integer/base'
7
6
 
8
7
  # @see https://github.com/ulid/spec
9
8
  # @!attribute [r] milliseconds
@@ -16,6 +15,7 @@ class ULID
16
15
  class Error < StandardError; end
17
16
  class OverflowError < Error; end
18
17
  class ParserError < Error; end
18
+ class SetupError < ScriptError; end
19
19
 
20
20
  encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
21
  # Crockford's Base32. Excluded I, L, O, U.
@@ -35,12 +35,15 @@ class ULID
35
35
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
36
 
37
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
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
39
39
 
40
40
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
41
  # @see https://bugs.ruby-lang.org/issues/15958
42
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
43
43
 
44
+ UNDEFINED = BasicObject.new
45
+ Kernel.instance_method(:freeze).bind(UNDEFINED).call
46
+
44
47
  # @param [Integer, Time] moment
45
48
  # @param [Integer] entropy
46
49
  # @return [ULID]
@@ -51,27 +54,38 @@ class ULID
51
54
  # @param [Integer, Time] moment
52
55
  # @return [ULID]
53
56
  def self.min(moment: 0)
54
- generate(moment: moment, entropy: 0)
57
+ 0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
55
58
  end
56
59
 
57
60
  # @param [Integer, Time] moment
58
61
  # @return [ULID]
59
62
  def self.max(moment: MAX_MILLISECONDS)
60
- generate(moment: moment, entropy: MAX_ENTROPY)
61
- end
62
-
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)
63
+ MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
64
+ end
65
+
66
+ # @param [Integer] number
67
+ # @return [ULID, Array<ULID>]
68
+ # @raise [ArgumentError] if the given number is lager than ULID spec limits or given negative number
69
+ # @note Major difference of `Array#sample` interface is below
70
+ # * Do not ensure the uniqueness
71
+ # * Do not take random generator for the arguments
72
+ # * Raising error instead of truncating elements for the given number
73
+ def self.sample(number=UNDEFINED)
74
+ if UNDEFINED.equal?(number)
75
+ from_integer(SecureRandom.random_number(MAX_INTEGER))
70
76
  else
71
- Warning.warn(warning)
72
- end
77
+ begin
78
+ int = number.to_int
79
+ rescue
80
+ # Can not use `number.to_s` and `number.inspect` for considering BasicObject here
81
+ raise TypeError, 'accepts no argument or integer only'
82
+ end
73
83
 
74
- MONOTONIC_GENERATOR.generate
84
+ if int > MAX_INTEGER || int.negative?
85
+ raise ArgumentError, "given number is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
86
+ end
87
+ int.times.map { from_integer(SecureRandom.random_number(MAX_INTEGER)) }
88
+ end
75
89
  end
76
90
 
77
91
  # @param [String, #to_str] string
@@ -121,6 +135,43 @@ class ULID
121
135
  new milliseconds: milliseconds, entropy: entropy
122
136
  end
123
137
 
138
+ # @param [Range<Time>, Range<nil>] time_range
139
+ # @return [Range<ULID>]
140
+ # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
141
+ def self.range(time_range)
142
+ raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
143
+ begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
144
+
145
+ case begin_time
146
+ when Time
147
+ begin_ulid = min(moment: begin_time)
148
+ when nil
149
+ begin_ulid = MIN
150
+ else
151
+ raise argument_error_for_range_building(time_range)
152
+ end
153
+
154
+ case end_time
155
+ when Time
156
+ if exclude_end
157
+ end_ulid = min(moment: end_time)
158
+ else
159
+ end_ulid = max(moment: end_time)
160
+ end
161
+ when nil
162
+ # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
163
+ end_ulid = MAX
164
+ exclude_end = false
165
+ else
166
+ raise argument_error_for_range_building(time_range)
167
+ end
168
+
169
+ begin_ulid.freeze
170
+ end_ulid.freeze
171
+
172
+ Range.new(begin_ulid, end_ulid, exclude_end)
173
+ end
174
+
124
175
  # @param [Time] time
125
176
  # @return [Time]
126
177
  def self.floor(time)
@@ -131,49 +182,99 @@ class ULID
131
182
  end
132
183
  end
133
184
 
185
+ # @api private
134
186
  # @return [Integer]
135
187
  def self.current_milliseconds
136
188
  milliseconds_from_time(Time.now)
137
189
  end
138
190
 
191
+ # @api private
139
192
  # @param [Time] time
140
193
  # @return [Integer]
141
194
  def self.milliseconds_from_time(time)
142
195
  (time.to_r * 1000).to_i
143
196
  end
144
197
 
198
+ # @api private
145
199
  # @param [Time, Integer] moment
146
200
  # @return [Integer]
147
201
  def self.milliseconds_from_moment(moment)
148
202
  moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
149
203
  end
150
204
 
205
+ # @api private
151
206
  # @return [Integer]
152
207
  def self.reasonable_entropy
153
208
  SecureRandom.random_number(MAX_ENTROPY)
154
209
  end
155
210
 
211
+ n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
212
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
213
+
214
+ n32_char_by_number = {}
215
+ n32_chars.each_with_index do |char, index|
216
+ n32_char_by_number[index] = char
217
+ end
218
+ n32_char_by_number.freeze
219
+
220
+ # Currently supporting only for `subset for actual use-case`
221
+ # See below
222
+ # * https://github.com/ulid/spec/pull/57
223
+ # * https://github.com/kachick/ruby-ulid/issues/57
224
+ # * https://github.com/kachick/ruby-ulid/issues/78
225
+ crockford_base32_mappings = {
226
+ 'J' => 18,
227
+ 'K' => 19,
228
+ 'M' => 20,
229
+ 'N' => 21,
230
+ 'P' => 22,
231
+ 'Q' => 23,
232
+ 'R' => 24,
233
+ 'S' => 25,
234
+ 'T' => 26,
235
+ 'V' => 27,
236
+ 'W' => 28,
237
+ 'X' => 29,
238
+ 'Y' => 30,
239
+ 'Z' => 31
240
+ }.freeze
241
+
242
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
243
+ if n = crockford_base32_mappings[encoding_char]
244
+ char_32 = n32_char_by_number.fetch(n)
245
+ map[encoding_char] = char_32
246
+ end
247
+ end.freeze
248
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
249
+ CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
250
+
251
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
252
+ N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
253
+
156
254
  # @param [String, #to_str] string
157
255
  # @return [ULID]
158
256
  # @raise [ParserError] if the given format is not correct for ULID specs
159
- # @raise [OverflowError] if the given value is larger than the ULID limit
160
257
  def self.parse(string)
161
258
  begin
162
259
  string = string.to_str
163
- unless string.size == ENCODED_ID_LENGTH
164
- raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
165
- end
166
- timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
167
- randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
168
- milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
169
- entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
260
+ raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
261
+ n32encoded = convert_crockford_base32_to_n32(string.upcase)
262
+ timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
263
+ randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
264
+ milliseconds = timestamp.to_i(32)
265
+ entropy = randomness.to_i(32)
170
266
  rescue => err
171
267
  raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
172
268
  end
173
-
269
+
174
270
  new milliseconds: milliseconds, entropy: entropy
175
271
  end
176
272
 
273
+ # @api private
274
+ private_class_method def self.convert_crockford_base32_to_n32(string)
275
+ string.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
276
+ end
277
+
177
278
  # @return [Boolean]
178
279
  def self.valid?(string)
179
280
  parse(string)
@@ -208,6 +309,11 @@ class ULID
208
309
  num
209
310
  end
210
311
 
312
+ # @return [ArgumentError]
313
+ private_class_method def self.argument_error_for_range_building(argument)
314
+ ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
315
+ end
316
+
211
317
  attr_reader :milliseconds, :entropy
212
318
 
213
319
  # @api private
@@ -228,10 +334,9 @@ class ULID
228
334
  end
229
335
 
230
336
  # @return [String]
231
- def to_str
232
- @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
337
+ def to_s
338
+ @string ||= convert_n32_to_crockford_base32(to_i.to_s(32).rjust(ENCODED_ID_LENGTH, '0').upcase).freeze
233
339
  end
234
- alias_method :to_s, :to_str
235
340
 
236
341
  # @return [Integer]
237
342
  def to_i
@@ -246,7 +351,7 @@ class ULID
246
351
 
247
352
  # @return [String]
248
353
  def inspect
249
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
354
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
250
355
  end
251
356
 
252
357
  # @return [Boolean]
@@ -307,11 +412,13 @@ class ULID
307
412
  @randomness ||= matchdata[:randomness].freeze
308
413
  end
309
414
 
415
+ # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
310
416
  # @return [Regexp]
311
417
  def pattern
312
418
  @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
313
419
  end
314
420
 
421
+ # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
315
422
  # @return [Regexp]
316
423
  def strict_pattern
317
424
  @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
@@ -332,23 +439,45 @@ class ULID
332
439
  @pred ||= self.class.from_integer(pre_int)
333
440
  end
334
441
 
442
+ # @return [String]
443
+ def to_uuidv4
444
+ @uuidv4 ||= begin
445
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
446
+ array = octets.pack('C*').unpack('NnnnnN')
447
+ array[2] = (array[2] & 0x0fff) | 0x4000
448
+ array[3] = (array[3] & 0x3fff) | 0x8000
449
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
450
+ end
451
+ end
452
+
335
453
  # @return [self]
336
454
  def freeze
337
- # Evaluate all caching
338
- inspect
339
- octets
340
- to_i
341
- succ
342
- pred
343
- strict_pattern
455
+ # Need to cache before freezing, because frozen objects can't assign instance variables
456
+ cache_all_instance_variables
344
457
  super
345
458
  end
346
459
 
347
460
  private
348
461
 
462
+ # @api private
463
+ def convert_n32_to_crockford_base32(string)
464
+ string.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR)
465
+ end
466
+
349
467
  # @return [MatchData]
350
468
  def matchdata
351
- @matchdata ||= STRICT_PATTERN.match(to_str).freeze
469
+ @matchdata ||= STRICT_PATTERN.match(to_s).freeze
470
+ end
471
+
472
+ # @return [void]
473
+ def cache_all_instance_variables
474
+ inspect
475
+ octets
476
+ to_i
477
+ succ
478
+ pred
479
+ strict_pattern
480
+ to_uuidv4
352
481
  end
353
482
  end
354
483
 
@@ -356,7 +485,8 @@ require_relative 'ulid/version'
356
485
  require_relative 'ulid/monotonic_generator'
357
486
 
358
487
  class ULID
359
- MONOTONIC_GENERATOR = MonotonicGenerator.new
488
+ MIN = parse('00000000000000000000000000').freeze
489
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
360
490
 
361
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
491
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :CROCKFORD_BASE32_CHAR_PATTERN, :N32_CHAR_BY_CROCKFORD_BASE32_CHAR, :CROCKFORD_BASE32_CHAR_BY_N32_CHAR, :N32_CHAR_PATTERN, :UNDEFINED
362
492
  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.12'
5
+ VERSION = '0.0.17'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -15,9 +15,16 @@ class ULID
15
15
  PATTERN: Regexp
16
16
  STRICT_PATTERN: Regexp
17
17
  UUIDV4_PATTERN: Regexp
18
- MONOTONIC_GENERATOR: MonotonicGenerator
18
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
19
+ CROCKFORD_BASE32_CHAR_PATTERN: Regexp
20
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
21
+ N32_CHAR_PATTERN: Regexp
22
+ MIN: ULID
23
+ MAX: ULID
24
+ UNDEFINED: BasicObject
19
25
  include Comparable
20
26
 
27
+ # The `moment` is a `Time` or `Intger of the milliseconds`
21
28
  type moment = Time | Integer
22
29
 
23
30
  class Error < StandardError
@@ -29,6 +36,9 @@ class ULID
29
36
  class ParserError < Error
30
37
  end
31
38
 
39
+ class SetupError < ScriptError
40
+ end
41
+
32
42
  class MonotonicGenerator
33
43
  attr_accessor latest_milliseconds: Integer
34
44
  attr_accessor latest_entropy: Integer
@@ -56,13 +66,14 @@ class ULID
56
66
  @next: ULID?
57
67
  @pattern: Regexp?
58
68
  @strict_pattern: Regexp?
69
+ @uuidv4: String?
59
70
  @matchdata: MatchData?
60
71
 
61
72
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
62
- def self.monotonic_generate: -> ULID
63
73
  def self.current_milliseconds: -> Integer
64
74
  def self.milliseconds_from_time: (Time time) -> Integer
65
75
  def self.milliseconds_from_moment: (moment moment) -> Integer
76
+ def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
66
77
  def self.floor: (Time time) -> Time
67
78
  def self.reasonable_entropy: -> Integer
68
79
  def self.parse: (String string) -> ULID
@@ -70,6 +81,8 @@ class ULID
70
81
  def self.from_integer: (Integer integer) -> ULID
71
82
  def self.min: (?moment: moment) -> ULID
72
83
  def self.max: (?moment: moment) -> ULID
84
+ def self.sample: -> ULID
85
+ | (Integer number) -> Array[ULID]
73
86
  def self.valid?: (untyped string) -> bool
74
87
  def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
75
88
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
@@ -78,8 +91,7 @@ class ULID
78
91
  attr_reader milliseconds: Integer
79
92
  attr_reader entropy: Integer
80
93
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
81
- def to_str: -> String
82
- alias to_s to_str
94
+ def to_s: -> String
83
95
  def to_i: -> Integer
84
96
  alias hash to_i
85
97
  def <=>: (ULID other) -> Integer
@@ -96,11 +108,16 @@ class ULID
96
108
  def octets: -> octets
97
109
  def timestamp_octets: -> timestamp_octets
98
110
  def randomness_octets: -> randomness_octets
111
+ def to_uuidv4: -> String
99
112
  def next: -> ULID?
100
113
  alias succ next
101
114
  def pred: -> ULID?
102
115
  def freeze: -> self
103
116
 
104
117
  private
118
+ def self.convert_crockford_base32_to_n32: (String) -> String
119
+ def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
120
+ def convert_n32_to_crockford_base32: (String) -> String
105
121
  def matchdata: -> MatchData
122
+ def cache_all_instance_variables: -> void
106
123
  end
metadata CHANGED
@@ -1,35 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.17
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-02 00:00:00.000000000 Z
11
+ date: 2021-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: integer-base
14
+ name: rbs
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.2
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: 0.2.0
23
- type: :runtime
19
+ version: 1.2.0
20
+ type: :development
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
- version: 0.1.2
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: 0.2.0
26
+ version: 1.2.0
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: benchmark-ips
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,10 +64,10 @@ dependencies:
70
64
  - - "<"
71
65
  - !ruby/object:Gem::Version
72
66
  version: '2'
73
- description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
74
- useful specs for applications (e.g. `Database key`). \n This gem aims to provide
75
- the generator, monotonic generator, parser and handy manipulation features around
76
- 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.
77
71
  email:
78
72
  - kachick1+ruby@gmail.com
79
73
  executables: []
@@ -82,7 +76,6 @@ extra_rdoc_files: []
82
76
  files:
83
77
  - LICENSE
84
78
  - README.md
85
- - Steepfile
86
79
  - lib/ulid.rb
87
80
  - lib/ulid/monotonic_generator.rb
88
81
  - lib/ulid/version.rb
@@ -102,7 +95,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
95
  requirements:
103
96
  - - ">="
104
97
  - !ruby/object:Gem::Version
105
- version: '2.5'
98
+ version: 2.6.0
106
99
  required_rubygems_version: !ruby/object:Gem::Requirement
107
100
  requirements:
108
101
  - - ">="
data/Steepfile DELETED
@@ -1,7 +0,0 @@
1
- target :lib do
2
- signature 'sig'
3
-
4
- check 'lib'
5
-
6
- library 'securerandom'
7
- end