ruby-ulid 0.0.12 → 0.0.17

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.
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