ruby-ulid 0.0.13 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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