ruby-ulid 0.0.13 → 0.0.18

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: 9e2d0489b211160618fc0ae6ecd30cbbbbaa411683162def97930ca95a860696
4
- data.tar.gz: 0063c982795d2a42e2f6bac7b236d831753c685b36f4d047e2999dbe7368c6de
3
+ metadata.gz: 8b1641b0894c0140c3a9c6f59fa8abcfca386362ad1aaa92fe196f3bf5f75cf1
4
+ data.tar.gz: 932fa04dceb504330289d4236f9670fecd702989504bdd402873d60c0bb1c0e5
5
5
  SHA512:
6
- metadata.gz: 825dab6b7e01f4e1dee5fcd03031ef27645de6a1b56e8392fd784a55c860268af5b84ae4ac3697cdf3eba630dbae197da28cde4b8fbef043c8d0ff48b587f992
7
- data.tar.gz: f2039e401c77cf15bda81d10587085b7fa167c253a19a66bc0b14ee5fda71162325b7a278f2ebb185d53cbb3fd01563ccb4c6875ffbaebf7fc676a2a24c73767
6
+ metadata.gz: f073c7b240ef96b511c84c6bc5649c483fc97c62a3892f58531b6f36e8235e49dcd865433fe522eec48a3532c98c4deec8713f453a4b2a117157788eabddaf6b
7
+ data.tar.gz: a2486bbefd62d0d019c15044c9d67b6aabc33e370f4ee88239c1e2b33a40738c736fcc9221dd3ce457eabf83a3580be5dc501fef0c359345a453331bdd62d139
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,21 +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
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.0.18'
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,19 +114,21 @@ 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 prefer `sortable` rather than the `randomness`, 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
105
123
  monotonic_generator = ULID::MonotonicGenerator.new
106
- monotonic_ulids = 10000.times.map do
124
+ ulids = 10000.times.map do
107
125
  monotonic_generator.generate
108
126
  end
109
- sample_ulids_by_the_time = monotonic_ulids.uniq(&:to_time)
127
+ sample_ulids_by_the_time = ulids.uniq(&:to_time)
110
128
  sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
111
129
 
112
130
  # In same milliseconds creation, it just increments the end of randomness part
113
- monotonic_ulids.take(5) #=>
131
+ ulids.take(5) #=>
114
132
  # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
115
133
  # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
116
134
  # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
@@ -125,23 +143,50 @@ sample_ulids_by_the_time.take(5) #=>
125
143
  # ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
126
144
  # ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
127
145
 
128
- monotonic_ulids.sort == monotonic_ulids #=> true
146
+ ulids.sort == ulids #=> true
147
+ ```
148
+
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)
129
159
  ```
130
160
 
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]`
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,14 +219,28 @@ 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
 
187
246
  ```ruby
@@ -193,7 +252,10 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
193
252
  ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
194
253
  ```
195
254
 
196
- `ULID#next` and `ULID#succ` returns next(successor) ULID
255
+ `ULID#next` and `ULID#succ` returns next(successor) ULID.
256
+ Especially `ULID#succ` makes it possible `Range[ULID]#each`.
257
+
258
+ NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
197
259
 
198
260
  ```ruby
199
261
  ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
@@ -209,17 +271,106 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
209
271
  ULID.parse('00000000000000000000000000').pred #=> nil
210
272
  ```
211
273
 
212
- UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
274
+ `ULID.sample` returns random ULIDs ignoring the generating time
275
+
276
+ ```ruby
277
+ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
278
+ ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
279
+ ULID.sample(0) #=> []
280
+ ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
281
+ ULID.sample(5)
282
+ #=>
283
+ #[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
284
+ # ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
285
+ # ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
286
+ # ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
287
+ # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
288
+ ```
289
+
290
+ ### UUIDv4 converter for migration use-cases
291
+
292
+ `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
293
+ The imported timestamp is meaningless. So ULID's benefit will lost.
213
294
 
214
295
  ```ruby
215
- ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
216
- #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
296
+ # Currently experimental feature, so needed to load the extension.
297
+ require 'ulid/uuid'
298
+
299
+ # Basically reversible
300
+ ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
301
+ ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
302
+
303
+ uuid_v4s = 10000.times.map { SecureRandom.uuid }
304
+ uuid_v4s.uniq.size == 10000 #=> Probably `true`
305
+
306
+ ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
307
+ ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
308
+
309
+ # NOTE: Some boundary values are not reversible. See below.
310
+
311
+ ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
312
+ ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
313
+
314
+ # These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
315
+ reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
316
+ reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
317
+
318
+ # But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
319
+ ULID.min == reversed_min #=> false
320
+ ULID.max == reversed_max #=> false
217
321
  ```
218
322
 
323
+ ## How to migrate from other gems
324
+
325
+ As far as I know, major prior arts are below
326
+
327
+ ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
328
+
329
+ It is just providing basic `String` generator only.
330
+ So you can replace the code as below
331
+
332
+ ```diff
333
+ -ULID.generate
334
+ +ULID.generate.to_s
335
+ ```
336
+
337
+ 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.
338
+
339
+ 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
340
+ 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
341
+ 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
342
+
343
+ ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
344
+
345
+ It is providing basic generator(except monotonic generator) and parser.
346
+ Major methods can be replaced as below.
347
+
348
+ ```diff
349
+ -ULID.generate
350
+ +ULID.generate.to_s
351
+ -ULID.at(time)
352
+ +ULID.at(time).to_s
353
+ -ULID.time(string)
354
+ +ULID.parse(string).to_time
355
+ -ULID.min_ulid_at(time)
356
+ +ULID.min(moment: time).to_s
357
+ -ULID.max_ulid_at(time)
358
+ +ULID.max(moment: time).to_s
359
+ ```
360
+
361
+ NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
362
+
363
+ 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
364
+ 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
365
+ 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
366
+
219
367
  ## References
220
368
 
221
369
  - [Repository](https://github.com/kachick/ruby-ulid)
222
370
  - [API documents](https://kachick.github.io/ruby-ulid/)
223
371
  - [ulid/spec](https://github.com/ulid/spec)
224
- - [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), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
225
- - 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).
372
+
373
+ ## Note
374
+
375
+ - 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)
376
+ - 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,31 +15,46 @@ 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
+ # `Subset` of Crockford's Base32. Just excluded I, L, O, U, -.
21
+ # refs:
22
+ # * https://www.crockford.com/base32.html
23
+ # * https://github.com/ulid/spec/pull/57
24
+ # * https://github.com/kachick/ruby-ulid/issues/57
20
25
  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
26
+ encoding_chars = encoding_string.chars.map(&:freeze).freeze
24
27
 
25
- TIMESTAMP_PART_LENGTH = 10
26
- RANDOMNESS_PART_LENGTH = 16
27
- ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
+ TIMESTAMP_ENCODED_LENGTH = 10
29
+ RANDOMNESS_ENCODED_LENGTH = 16
30
+ ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
28
31
  TIMESTAMP_OCTETS_LENGTH = 6
29
32
  RANDOMNESS_OCTETS_LENGTH = 10
30
33
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
34
  MAX_MILLISECONDS = 281474976710655
32
35
  MAX_ENTROPY = 1208925819614629174706175
33
36
  MAX_INTEGER = 340282366920938463463374607431768211455
34
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
37
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
35
38
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
39
 
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
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
+ # @return [String]
46
+ def UNDEFINED.to_s
47
+ 'ULID::UNDEFINED'
48
+ end
49
+
50
+ # @return [String]
51
+ def UNDEFINED.inspect
52
+ to_s
53
+ end
54
+ Kernel.instance_method(:freeze).bind(UNDEFINED).call
55
+
56
+ private_class_method :new
57
+
44
58
  # @param [Integer, Time] moment
45
59
  # @param [Integer] entropy
46
60
  # @return [ULID]
@@ -48,30 +62,49 @@ class ULID
48
62
  new milliseconds: milliseconds_from_moment(moment), entropy: entropy
49
63
  end
50
64
 
51
- # @param [Integer, Time] moment
65
+ # Short hand of `ULID.generate(moment: time)`
66
+ # @param [Time] time
52
67
  # @return [ULID]
53
- def self.min(moment: 0)
54
- generate(moment: moment, entropy: 0)
68
+ def self.at(time)
69
+ raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
70
+ new milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy
55
71
  end
56
72
 
57
73
  # @param [Integer, Time] moment
58
74
  # @return [ULID]
59
- def self.max(moment: MAX_MILLISECONDS)
60
- generate(moment: moment, entropy: MAX_ENTROPY)
75
+ def self.min(moment: 0)
76
+ 0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
61
77
  end
62
78
 
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
79
+ # @param [Integer, Time] moment
65
80
  # @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)
81
+ def self.max(moment: MAX_MILLISECONDS)
82
+ MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
83
+ end
84
+
85
+ # @param [Integer] number
86
+ # @return [ULID, Array<ULID>]
87
+ # @raise [ArgumentError] if the given number is lager than ULID spec limits or given negative number
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(number=UNDEFINED)
93
+ if UNDEFINED.equal?(number)
94
+ from_integer(SecureRandom.random_number(MAX_INTEGER))
70
95
  else
71
- Warning.warn(warning)
72
- end
96
+ begin
97
+ int = number.to_int
98
+ rescue
99
+ # Can not use `number.to_s` and `number.inspect` for considering BasicObject here
100
+ raise TypeError, 'accepts no argument or integer only'
101
+ end
73
102
 
74
- MONOTONIC_GENERATOR.generate
103
+ if int > MAX_INTEGER || int.negative?
104
+ raise ArgumentError, "given number is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
105
+ end
106
+ int.times.map { from_integer(SecureRandom.random_number(MAX_INTEGER)) }
107
+ end
75
108
  end
76
109
 
77
110
  # @param [String, #to_str] string
@@ -87,53 +120,39 @@ class ULID
87
120
  self
88
121
  end
89
122
 
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
123
  # @param [Integer, #to_int] integer
106
124
  # @return [ULID]
107
125
  # @raise [OverflowError] if the given integer is larger than the ULID limit
108
126
  # @raise [ArgumentError] if the given integer is negative number
109
- # @todo Need optimized for performance
110
127
  def self.from_integer(integer)
111
128
  integer = integer.to_int
112
129
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
113
130
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
114
131
 
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)
132
+ n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
133
+ n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
134
+ n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
135
+
136
+ milliseconds = n32encoded_timestamp.to_i(32)
137
+ entropy = n32encoded_randomness.to_i(32)
120
138
 
121
- new milliseconds: milliseconds, entropy: entropy
139
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
122
140
  end
123
141
 
124
- # @param [Range<Time>] time_range
142
+ # @param [Range<Time>, Range<nil>] time_range
125
143
  # @return [Range<ULID>]
144
+ # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
126
145
  def self.range(time_range)
127
- raise ArgumentError, 'ULID.range takes only Range[Time]' unless time_range.kind_of?(Range)
146
+ raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
128
147
  begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
129
148
 
130
149
  case begin_time
131
150
  when Time
132
151
  begin_ulid = min(moment: begin_time)
133
152
  when nil
134
- begin_ulid = min
153
+ begin_ulid = MIN
135
154
  else
136
- raise ArgumentError, 'ULID.range takes only Range[Time]'
155
+ raise argument_error_for_range_building(time_range)
137
156
  end
138
157
 
139
158
  case end_time
@@ -145,12 +164,15 @@ class ULID
145
164
  end
146
165
  when nil
147
166
  # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
148
- end_ulid = max
167
+ end_ulid = MAX
149
168
  exclude_end = false
150
169
  else
151
- raise ArgumentError, 'ULID.range takes only Range[Time]'
170
+ raise argument_error_for_range_building(time_range)
152
171
  end
153
172
 
173
+ begin_ulid.freeze
174
+ end_ulid.freeze
175
+
154
176
  Range.new(begin_ulid, end_ulid, exclude_end)
155
177
  end
156
178
 
@@ -164,47 +186,88 @@ class ULID
164
186
  end
165
187
  end
166
188
 
189
+ # @api private
167
190
  # @return [Integer]
168
191
  def self.current_milliseconds
169
192
  milliseconds_from_time(Time.now)
170
193
  end
171
194
 
195
+ # @api private
172
196
  # @param [Time] time
173
197
  # @return [Integer]
174
198
  def self.milliseconds_from_time(time)
175
199
  (time.to_r * 1000).to_i
176
200
  end
177
201
 
202
+ # @api private
178
203
  # @param [Time, Integer] moment
179
204
  # @return [Integer]
180
205
  def self.milliseconds_from_moment(moment)
181
206
  moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
182
207
  end
183
208
 
209
+ # @api private
184
210
  # @return [Integer]
185
211
  def self.reasonable_entropy
186
212
  SecureRandom.random_number(MAX_ENTROPY)
187
213
  end
188
214
 
215
+ n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
216
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
217
+
218
+ n32_char_by_number = {}
219
+ n32_chars.each_with_index do |char, index|
220
+ n32_char_by_number[index] = char
221
+ end
222
+ n32_char_by_number.freeze
223
+
224
+ # Currently supporting only for `subset for actual use-case`
225
+ # See below
226
+ # * https://github.com/ulid/spec/pull/57
227
+ # * https://github.com/kachick/ruby-ulid/issues/57
228
+ # * https://github.com/kachick/ruby-ulid/issues/78
229
+ crockford_base32_mappings = {
230
+ 'J' => 18,
231
+ 'K' => 19,
232
+ 'M' => 20,
233
+ 'N' => 21,
234
+ 'P' => 22,
235
+ 'Q' => 23,
236
+ 'R' => 24,
237
+ 'S' => 25,
238
+ 'T' => 26,
239
+ 'V' => 27,
240
+ 'W' => 28,
241
+ 'X' => 29,
242
+ 'Y' => 30,
243
+ 'Z' => 31
244
+ }.freeze
245
+
246
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR = encoding_chars.each_with_object({}) do |encoding_char, map|
247
+ if n = crockford_base32_mappings[encoding_char]
248
+ char_32 = n32_char_by_number.fetch(n)
249
+ map[encoding_char] = char_32
250
+ end
251
+ end.freeze
252
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
253
+ CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
254
+
255
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
256
+ N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
257
+
189
258
  # @param [String, #to_str] string
190
259
  # @return [ULID]
191
260
  # @raise [ParserError] if the given format is not correct for ULID specs
192
- # @raise [OverflowError] if the given value is larger than the ULID limit
193
261
  def self.parse(string)
194
262
  begin
195
263
  string = string.to_str
196
- unless string.size == ENCODED_ID_LENGTH
197
- raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
198
- end
199
- timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
200
- randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
201
- milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
202
- entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
264
+ raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
203
265
  rescue => err
204
266
  raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
205
267
  end
206
-
207
- new milliseconds: milliseconds, entropy: entropy
268
+
269
+ n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
270
+ from_integer(n32encoded.to_i(32))
208
271
  end
209
272
 
210
273
  # @return [Boolean]
@@ -216,18 +279,6 @@ class ULID
216
279
  true
217
280
  end
218
281
 
219
- # @api private
220
- # @param [Integer] integer
221
- # @param [Integer] length
222
- # @return [Array<Integer>]
223
- def self.octets_from_integer(integer, length:)
224
- digits = integer.digits(256)
225
- (length - digits.size).times do
226
- digits.push 0
227
- end
228
- digits.reverse!
229
- end
230
-
231
282
  # @api private
232
283
  # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
233
284
  # @param [Array<Integer>] reversed_digits
@@ -241,50 +292,73 @@ class ULID
241
292
  num
242
293
  end
243
294
 
295
+ # @api private
296
+ # @param [MonotonicGenerator] generator
297
+ # @return [ULID]
298
+ def self.from_monotonic_generator(generator)
299
+ raise ArgumentError, 'this method provided only for MonotonicGenerator' unless MonotonicGenerator === generator
300
+ new milliseconds: generator.latest_milliseconds, entropy: generator.latest_entropy
301
+ end
302
+
303
+ # @api private
304
+ # @return [ArgumentError]
305
+ private_class_method def self.argument_error_for_range_building(argument)
306
+ ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
307
+ end
308
+
244
309
  attr_reader :milliseconds, :entropy
245
310
 
246
311
  # @api private
247
312
  # @param [Integer] milliseconds
248
313
  # @param [Integer] entropy
314
+ # @param [Integer] integer
249
315
  # @return [void]
250
316
  # @raise [OverflowError] if the given value is larger than the ULID limit
251
317
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
252
- def initialize(milliseconds:, entropy:)
253
- milliseconds = milliseconds.to_int
254
- entropy = entropy.to_int
255
- raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
256
- raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
257
- raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
318
+ def initialize(milliseconds:, entropy:, integer: UNDEFINED)
319
+ if UNDEFINED.equal?(integer)
320
+ milliseconds = milliseconds.to_int
321
+ entropy = entropy.to_int
322
+
323
+ raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
324
+ raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
325
+ raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
326
+ else
327
+ @integer = integer
328
+ end
258
329
 
259
330
  @milliseconds = milliseconds
260
331
  @entropy = entropy
261
332
  end
262
333
 
263
334
  # @return [String]
264
- def to_str
265
- @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
335
+ def to_s
336
+ @string ||= to_i.to_s(32).upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0').freeze
266
337
  end
267
- alias_method :to_s, :to_str
268
338
 
269
339
  # @return [Integer]
270
340
  def to_i
271
- @integer ||= self.class.inverse_of_digits(octets)
341
+ @integer ||= begin
342
+ n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
343
+ n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
344
+ (n32encoded_timestamp + n32encoded_randomness).to_i(32)
345
+ end
272
346
  end
273
347
  alias_method :hash, :to_i
274
348
 
275
349
  # @return [Integer, nil]
276
350
  def <=>(other)
277
- other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
351
+ (ULID === other) ? (to_i <=> other.to_i) : nil
278
352
  end
279
353
 
280
354
  # @return [String]
281
355
  def inspect
282
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
356
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
283
357
  end
284
358
 
285
359
  # @return [Boolean]
286
360
  def eql?(other)
287
- other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
361
+ equal?(other) || (ULID === other && to_i == other.to_i)
288
362
  end
289
363
  alias_method :==, :eql?
290
364
 
@@ -317,37 +391,49 @@ class ULID
317
391
 
318
392
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
319
393
  def octets
320
- @octets ||= (timestamp_octets + randomness_octets).freeze
394
+ @octets ||= octets_from_integer(to_i).freeze
321
395
  end
322
396
 
323
397
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
324
398
  def timestamp_octets
325
- @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
399
+ @timestamp_octets ||= octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
326
400
  end
327
401
 
328
402
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
329
403
  def randomness_octets
330
- @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
404
+ @randomness_octets ||= octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
331
405
  end
332
406
 
333
407
  # @return [String]
334
408
  def timestamp
335
- @timestamp ||= matchdata[:timestamp].freeze
409
+ @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
336
410
  end
337
411
 
338
412
  # @return [String]
339
413
  def randomness
340
- @randomness ||= matchdata[:randomness].freeze
414
+ @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
415
+ end
416
+
417
+ # @note Providing for rough operations. The keys and values is not fixed.
418
+ # @return [Hash{Symbol => Regexp, String}]
419
+ def patterns
420
+ named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
421
+ {
422
+ named_captures: named_captures,
423
+ strict_named_captures: /\A#{named_captures.source}\z/i.freeze
424
+ }
341
425
  end
342
426
 
427
+ # @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
343
428
  # @return [Regexp]
344
429
  def pattern
345
- @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
430
+ patterns.fetch(:named_captures)
346
431
  end
347
432
 
433
+ # @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
348
434
  # @return [Regexp]
349
435
  def strict_pattern
350
- @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
436
+ patterns.fetch(:strict_named_captures)
351
437
  end
352
438
 
353
439
  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
@@ -367,21 +453,32 @@ class ULID
367
453
 
368
454
  # @return [self]
369
455
  def freeze
370
- # Evaluate all caching
371
- inspect
372
- octets
373
- to_i
374
- succ
375
- pred
376
- strict_pattern
456
+ # Need to cache before freezing, because frozen objects can't assign instance variables
457
+ cache_all_instance_variables
377
458
  super
378
459
  end
379
460
 
380
461
  private
381
462
 
382
- # @return [MatchData]
383
- def matchdata
384
- @matchdata ||= STRICT_PATTERN.match(to_str).freeze
463
+ # @param [Integer] integer
464
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
465
+ def octets_from_integer(integer)
466
+ digits = integer.digits(256)
467
+ (OCTETS_LENGTH - digits.size).times do
468
+ digits.push 0
469
+ end
470
+ digits.reverse!
471
+ end
472
+
473
+ # @return [void]
474
+ def cache_all_instance_variables
475
+ inspect
476
+ octets
477
+ to_i
478
+ succ
479
+ pred
480
+ timestamp
481
+ randomness
385
482
  end
386
483
  end
387
484
 
@@ -389,7 +486,8 @@ require_relative 'ulid/version'
389
486
  require_relative 'ulid/monotonic_generator'
390
487
 
391
488
  class ULID
392
- MONOTONIC_GENERATOR = MonotonicGenerator.new
489
+ MIN = parse('00000000000000000000000000').freeze
490
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
393
491
 
394
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
492
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :CROCKFORD_BASE32_CHAR_PATTERN, :N32_CHAR_BY_CROCKFORD_BASE32_CHAR, :CROCKFORD_BASE32_CHAR_BY_N32_CHAR, :N32_CHAR_PATTERN, :UNDEFINED
395
493
  end
@@ -26,7 +26,7 @@ class ULID
26
26
  @latest_entropy += 1
27
27
  end
28
28
 
29
- ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
29
+ ULID.from_monotonic_generator(self)
30
30
  end
31
31
 
32
32
  # @api private
data/lib/ulid/uuid.rb ADDED
@@ -0,0 +1,37 @@
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
+ begin
19
+ uuid = uuid.to_str
20
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
21
+ raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
22
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
23
+ from_integer(normalized.to_i(16))
24
+ rescue => err
25
+ raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
26
+ end
27
+ end
28
+
29
+ # @return [String]
30
+ def to_uuidv4
31
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
32
+ array = octets.pack('C*').unpack('NnnnnN')
33
+ array[2] = (array[2] & 0x0fff) | 0x4000
34
+ array[3] = (array[3] & 0x3fff) | 0x8000
35
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
36
+ end
37
+ 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.13'
5
+ VERSION = '0.0.18'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -1,10 +1,9 @@
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
+ TIMESTAMP_ENCODED_LENGTH: 10
5
+ RANDOMNESS_ENCODED_LENGTH: 16
6
+ ENCODED_LENGTH: 26
8
7
  TIMESTAMP_OCTETS_LENGTH: 6
9
8
  RANDOMNESS_OCTETS_LENGTH: 10
10
9
  OCTETS_LENGTH: 16
@@ -15,9 +14,16 @@ class ULID
15
14
  PATTERN: Regexp
16
15
  STRICT_PATTERN: Regexp
17
16
  UUIDV4_PATTERN: Regexp
18
- MONOTONIC_GENERATOR: MonotonicGenerator
17
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
18
+ CROCKFORD_BASE32_CHAR_PATTERN: Regexp
19
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
20
+ N32_CHAR_PATTERN: Regexp
21
+ MIN: ULID
22
+ MAX: ULID
23
+ UNDEFINED: BasicObject
19
24
  include Comparable
20
25
 
26
+ # The `moment` is a `Time` or `Intger of the milliseconds`
21
27
  type moment = Time | Integer
22
28
 
23
29
  class Error < StandardError
@@ -29,6 +35,9 @@ class ULID
29
35
  class ParserError < Error
30
36
  end
31
37
 
38
+ class SetupError < ScriptError
39
+ end
40
+
32
41
  class MonotonicGenerator
33
42
  attr_accessor latest_milliseconds: Integer
34
43
  attr_accessor latest_entropy: Integer
@@ -54,16 +63,13 @@ class ULID
54
63
  @inspect: String?
55
64
  @time: Time?
56
65
  @next: ULID?
57
- @pattern: Regexp?
58
- @strict_pattern: Regexp?
59
- @matchdata: MatchData?
60
66
 
61
67
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
62
- def self.monotonic_generate: -> ULID
68
+ def self.at: (Time time) -> ULID
63
69
  def self.current_milliseconds: -> Integer
64
70
  def self.milliseconds_from_time: (Time time) -> Integer
65
71
  def self.milliseconds_from_moment: (moment moment) -> Integer
66
- def self.range: (Range[Time] time_range) -> Range[ULID]
72
+ def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
67
73
  def self.floor: (Time time) -> Time
68
74
  def self.reasonable_entropy: -> Integer
69
75
  def self.parse: (String string) -> ULID
@@ -71,20 +77,22 @@ class ULID
71
77
  def self.from_integer: (Integer integer) -> ULID
72
78
  def self.min: (?moment: moment) -> ULID
73
79
  def self.max: (?moment: moment) -> ULID
80
+ def self.sample: -> ULID
81
+ | (Integer number) -> Array[ULID]
74
82
  def self.valid?: (untyped string) -> bool
75
83
  def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
76
84
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
77
- def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
85
+ def self.octets_from_integer: (Integer integer) -> octets
78
86
  def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
87
+ def self.from_monotonic_generator: (MonotonicGenerator generator) -> ULID
79
88
  attr_reader milliseconds: Integer
80
89
  attr_reader entropy: Integer
81
- def initialize: (milliseconds: Integer, entropy: Integer) -> void
82
- def to_str: -> String
83
- alias to_s to_str
90
+ def initialize: (milliseconds: Integer, entropy: Integer, ?integer: Integer) -> void
91
+ def to_s: -> String
84
92
  def to_i: -> Integer
85
93
  alias hash to_i
86
94
  def <=>: (ULID other) -> Integer
87
- | (untyped other) -> Integer?
95
+ | (untyped other) -> nil
88
96
  def inspect: -> String
89
97
  def eql?: (untyped other) -> bool
90
98
  alias == eql?
@@ -92,16 +100,19 @@ class ULID
92
100
  def to_time: -> Time
93
101
  def timestamp: -> String
94
102
  def randomness: -> String
103
+ def patterns: -> Hash[Symbol, Regexp | String]
95
104
  def pattern: -> Regexp
96
105
  def strict_pattern: -> Regexp
97
106
  def octets: -> octets
98
107
  def timestamp_octets: -> timestamp_octets
99
108
  def randomness_octets: -> randomness_octets
109
+ def to_uuidv4: -> String
100
110
  def next: -> ULID?
101
111
  alias succ next
102
112
  def pred: -> ULID?
103
113
  def freeze: -> self
104
114
 
105
115
  private
106
- def matchdata: -> MatchData
116
+ def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
117
+ def cache_all_instance_variables: -> void
107
118
  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.13
4
+ version: 0.0.18
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-03 00:00:00.000000000 Z
11
+ date: 2021-05-07 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: []
@@ -96,9 +76,9 @@ extra_rdoc_files: []
96
76
  files:
97
77
  - LICENSE
98
78
  - README.md
99
- - Steepfile
100
79
  - lib/ulid.rb
101
80
  - lib/ulid/monotonic_generator.rb
81
+ - lib/ulid/uuid.rb
102
82
  - lib/ulid/version.rb
103
83
  - sig/ulid.rbs
104
84
  homepage: https://github.com/kachick/ruby-ulid
@@ -123,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
103
  - !ruby/object:Gem::Version
124
104
  version: '0'
125
105
  requirements: []
126
- rubygems_version: 3.1.4
106
+ rubygems_version: 3.2.15
127
107
  signing_key:
128
108
  specification_version: 4
129
109
  summary: A handy ULID library
data/Steepfile DELETED
@@ -1,7 +0,0 @@
1
- target :lib do
2
- signature 'sig'
3
-
4
- check 'lib'
5
-
6
- library 'securerandom'
7
- end