ruby-ulid 0.0.14 → 0.0.19

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: fa89ecbb2a09940666b57e690d43a985872bb85f40b2b1175c79a762d79c8e45
4
- data.tar.gz: 2404fe5e1efa77899262fbf8979529fc474e44ffa0a8572ee3cf71eb1caf941a
3
+ metadata.gz: 646d2a4b433ffbe28d4801483d3f1c43cfb5839bf5f80e759447304a1c8dad52
4
+ data.tar.gz: 1e4ddc37266eb09f3635ba5509e66e6ff93e15fad3e6574fb3b50e8ad9322b10
5
5
  SHA512:
6
- metadata.gz: 1a86224846b27985d641bf485f952583c73dafc87a0ca8f65744cfdfa35371a6f5cfedb4246e7839da6e51fdcfd53632f20057c399038f3399f2986a9bf20085
7
- data.tar.gz: 24daff089a2b0564a2e7a93c34fef4d44420d1e0bbc6a054c5bcdfac02986c66204515bc085a7dc6499257692682217a888d8a04e04a7ae8de36e884cc182965
6
+ metadata.gz: 9a564dbad8c3c88b729353826c599e618cd8963806759ce8a6bf041a95aef60044c8f75873560129bcef7550eec226c3bf04bcfec339c4b07fb72c3372342811
7
+ data.tar.gz: 187c475f9332cb69827e2664193faa7f001cb14a5709b3c28600286ac9e6c1fb3176e14e3a7ae3a658d232af6157a3dea83ad992cee75db966ff80213c3a0e52
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.19'
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
129
147
  ```
130
148
 
131
- When filtering ULIDs by `Time`, we should consider to handle the precision.
132
- So this gem provides `ULID.range` to generate `Range[ULID]` from given `Range[Time]`
149
+ ### Filtering IDs with `Time`
150
+
151
+ `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
152
+
153
+ ```ruby
154
+ include_end = ulid1..ulid2
155
+ exclude_end = ulid1...ulid2
156
+
157
+ ulids.grep(one_of_the_above)
158
+ ulids.grep_v(one_of_the_above)
159
+ ```
160
+
161
+ When want to filter ULIDs with `Time`, we should consider to handle the precision.
162
+ So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
133
163
 
134
164
  ```ruby
135
165
  # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
136
166
  include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
137
167
  exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
138
168
 
169
+ # Below patterns are acceptable
170
+ pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
171
+ until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
172
+ until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
173
+ until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
174
+
139
175
  # So you can use the generated range objects as below
140
- ulids.grep(include_end)
141
- ulids.grep(exclude_end)
176
+ ulids.grep(one_of_the_above)
177
+ ulids.grep_v(one_of_the_above)
142
178
  #=> I hope the results should be actually you want!
143
179
  ```
144
180
 
181
+ If you want to manually handle the Time objects, `ULID.floor` returns new `Time` with truncating excess precisions in ULID spec.
182
+
183
+ ```ruby
184
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
185
+ ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
186
+ ```
187
+
188
+ ### Scanner for string (e.g. `JSON`)
189
+
145
190
  For rough operations, `ULID.scan` might be useful.
146
191
 
147
192
  ```ruby
@@ -174,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)]
183
228
  ```
184
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
+ }
240
+ ```
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,141 @@ 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.
275
+
276
+ Basically ignores generating time.
213
277
 
214
278
  ```ruby
215
- ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
216
- #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
279
+ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
280
+ ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
281
+ ULID.sample(0) #=> []
282
+ ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
283
+ ULID.sample(5)
284
+ #=>
285
+ #[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
286
+ # ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
287
+ # ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
288
+ # ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
289
+ # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
217
290
  ```
218
291
 
292
+ You can specify a range object for the timestamp restriction, see also `ULID.range`.
293
+
294
+ ```ruby
295
+ ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
296
+ ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
297
+ ulids = ULID.sample(1000, period: ulid1..ulid2)
298
+ ulids.uniq.size #=> 1000
299
+ ulids.take(10)
300
+ #=>
301
+ #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
302
+ # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
303
+ # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
304
+ # ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
305
+ # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
306
+ # ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
307
+ # ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
308
+ # ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
309
+ # ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
310
+ # ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
311
+ ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
312
+ #=>
313
+ # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
314
+ # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
315
+ # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
316
+ # ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
317
+ # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
318
+ # ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
319
+ # ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
320
+ # ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
321
+ # ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
322
+ # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
323
+ ```
324
+
325
+ ### UUIDv4 converter for migration use-cases
326
+
327
+ `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
328
+ The imported timestamp is meaningless. So ULID's benefit will lost.
329
+
330
+ ```ruby
331
+ # Currently experimental feature, so needed to load the extension.
332
+ require 'ulid/uuid'
333
+
334
+ # Basically reversible
335
+ ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
336
+ ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
337
+
338
+ uuid_v4s = 10000.times.map { SecureRandom.uuid }
339
+ uuid_v4s.uniq.size == 10000 #=> Probably `true`
340
+
341
+ ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
342
+ ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
343
+
344
+ # NOTE: Some boundary values are not reversible. See below.
345
+
346
+ ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
347
+ ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
348
+
349
+ # These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
350
+ reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
351
+ reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
352
+
353
+ # But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
354
+ ULID.min == reversed_min #=> false
355
+ ULID.max == reversed_max #=> false
356
+ ```
357
+
358
+ ## How to migrate from other gems
359
+
360
+ As far as I know, major prior arts are below
361
+
362
+ ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
363
+
364
+ It is just providing basic `String` generator only.
365
+ So you can replace the code as below
366
+
367
+ ```diff
368
+ -ULID.generate
369
+ +ULID.generate.to_s
370
+ ```
371
+
372
+ 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.
373
+
374
+ 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
375
+ 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
376
+ 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
377
+
378
+ ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
379
+
380
+ It is providing basic generator(except monotonic generator) and parser.
381
+ Major methods can be replaced as below.
382
+
383
+ ```diff
384
+ -ULID.generate
385
+ +ULID.generate.to_s
386
+ -ULID.at(time)
387
+ +ULID.at(time).to_s
388
+ -ULID.time(string)
389
+ +ULID.parse(string).to_time
390
+ -ULID.min_ulid_at(time)
391
+ +ULID.min(moment: time).to_s
392
+ -ULID.max_ulid_at(time)
393
+ +ULID.max(moment: time).to_s
394
+ ```
395
+
396
+ NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
397
+
398
+ 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
399
+ 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
400
+ 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
401
+
219
402
  ## References
220
403
 
221
404
  - [Repository](https://github.com/kachick/ruby-ulid)
222
405
  - [API documents](https://kachick.github.io/ruby-ulid/)
223
406
  - [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).
407
+
408
+ ## Note
409
+
410
+ - 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)
411
+ - 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
@@ -17,30 +16,37 @@ class ULID
17
16
  class OverflowError < Error; end
18
17
  class ParserError < Error; end
19
18
 
20
- encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
- # Crockford's Base32. Excluded I, L, O, U.
22
- # @see https://www.crockford.com/base32.html
23
- ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
19
+ # Excluded I, L, O, U, -.
20
+ # This is the encoding patterns.
21
+ # The decoding issue is written in ULID::CrockfordBase32
22
+ CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
24
23
 
25
- TIMESTAMP_PART_LENGTH = 10
26
- RANDOMNESS_PART_LENGTH = 16
27
- ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
24
+ TIMESTAMP_ENCODED_LENGTH = 10
25
+ RANDOMNESS_ENCODED_LENGTH = 16
26
+ ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
28
27
  TIMESTAMP_OCTETS_LENGTH = 6
29
28
  RANDOMNESS_OCTETS_LENGTH = 10
30
29
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
30
  MAX_MILLISECONDS = 281474976710655
32
31
  MAX_ENTROPY = 1208925819614629174706175
33
32
  MAX_INTEGER = 340282366920938463463374607431768211455
34
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
35
- STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
33
 
37
- # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
- UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i
34
+ # @see https://github.com/ulid/spec/pull/57
35
+ # Currently not used as a constant, but kept as a reference for now.
36
+ PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
37
+
38
+ STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
39
+
40
+ # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
41
+ # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
42
+ SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
39
43
 
40
44
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
45
  # @see https://bugs.ruby-lang.org/issues/15958
42
46
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
43
47
 
48
+ private_class_method :new
49
+
44
50
  # @param [Integer, Time] moment
45
51
  # @param [Integer] entropy
46
52
  # @return [ULID]
@@ -48,30 +54,75 @@ class ULID
48
54
  new milliseconds: milliseconds_from_moment(moment), entropy: entropy
49
55
  end
50
56
 
51
- # @param [Integer, Time] moment
57
+ # Short hand of `ULID.generate(moment: time)`
58
+ # @param [Time] time
52
59
  # @return [ULID]
53
- def self.min(moment: 0)
54
- generate(moment: moment, entropy: 0)
60
+ def self.at(time)
61
+ raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
62
+ new milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy
55
63
  end
56
64
 
57
65
  # @param [Integer, Time] moment
58
66
  # @return [ULID]
59
- def self.max(moment: MAX_MILLISECONDS)
60
- generate(moment: moment, entropy: MAX_ENTROPY)
67
+ def self.min(moment: 0)
68
+ 0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
61
69
  end
62
70
 
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
71
+ # @param [Integer, Time] moment
65
72
  # @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)
73
+ def self.max(moment: MAX_MILLISECONDS)
74
+ MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
75
+ end
76
+
77
+ RANDOM_INTEGER_GENERATOR = -> {
78
+ SecureRandom.random_number(MAX_INTEGER)
79
+ }
80
+
81
+ # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
82
+ # @overload sample(number, period: nil)
83
+ # @param [Integer] number
84
+ # @return [Array<ULID>]
85
+ # @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
86
+ # @overload sample(period: nil)
87
+ # @return [ULID]
88
+ # @note Major difference of `Array#sample` interface is below
89
+ # * Do not ensure the uniqueness
90
+ # * Do not take random generator for the arguments
91
+ # * Raising error instead of truncating elements for the given number
92
+ def self.sample(*args, period: nil)
93
+ int_generator = if period
94
+ ulid_range = range(period)
95
+ min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
96
+
97
+ possibilities = (max - min) + (exclude_end ? 0 : 1)
98
+ raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
99
+
100
+ -> {
101
+ SecureRandom.random_number(possibilities) + min
102
+ }
70
103
  else
71
- Warning.warn(warning)
104
+ RANDOM_INTEGER_GENERATOR
72
105
  end
73
106
 
74
- MONOTONIC_GENERATOR.generate
107
+ case args.size
108
+ when 0
109
+ from_integer(int_generator.call)
110
+ when 1
111
+ number = args.first
112
+ raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
113
+
114
+ if number > MAX_INTEGER || number.negative?
115
+ raise ArgumentError, "given number #{number} is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
116
+ end
117
+
118
+ if period && (number > possibilities)
119
+ raise ArgumentError, "given number #{number} is larger than given possibilities #{possibilities}"
120
+ end
121
+
122
+ Array.new(number) { from_integer(int_generator.call) }
123
+ else
124
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
125
+ end
75
126
  end
76
127
 
77
128
  # @param [String, #to_str] string
@@ -79,84 +130,81 @@ class ULID
79
130
  # @yieldparam [ULID] ulid
80
131
  # @yieldreturn [self]
81
132
  def self.scan(string)
82
- string = string.to_str
133
+ string = String.try_convert(string)
134
+ raise ArgumentError, 'ULID.scan takes only strings' unless string
83
135
  return to_enum(__callee__, string) unless block_given?
84
- string.scan(PATTERN) do |pair|
85
- yield parse(pair.join)
136
+ string.scan(SCANNING_PATTERN) do |matched|
137
+ yield parse(matched)
86
138
  end
87
139
  self
88
140
  end
89
141
 
90
- # @param [String, #to_str] uuid
91
- # @return [ULID]
92
- # @raise [ParserError] if the given format is not correct for UUIDv4 specs
93
- def self.from_uuidv4(uuid)
94
- begin
95
- uuid = uuid.to_str
96
- prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
97
- raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
98
- normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
99
- from_integer(normalized.to_i(16))
100
- rescue => err
101
- raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
102
- end
103
- end
104
-
105
142
  # @param [Integer, #to_int] integer
106
143
  # @return [ULID]
107
144
  # @raise [OverflowError] if the given integer is larger than the ULID limit
108
145
  # @raise [ArgumentError] if the given integer is negative number
109
- # @todo Need optimized for performance
110
146
  def self.from_integer(integer)
111
147
  integer = integer.to_int
112
148
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
113
149
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
114
150
 
115
- octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
116
- time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
117
- randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
118
- milliseconds = inverse_of_digits(time_octets)
119
- entropy = inverse_of_digits(randomness_octets)
151
+ n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
152
+ n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
153
+ n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
154
+
155
+ milliseconds = n32encoded_timestamp.to_i(32)
156
+ entropy = n32encoded_randomness.to_i(32)
120
157
 
121
- new milliseconds: milliseconds, entropy: entropy
158
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
122
159
  end
123
160
 
124
- # @param [Range<Time>] time_range
161
+ # @param [Range<Time>, Range<nil>, Range[ULID]] period
125
162
  # @return [Range<ULID>]
126
- def self.range(time_range)
127
- raise ArgumentError, 'ULID.range takes only Range[Time]' unless time_range.kind_of?(Range)
128
- begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
163
+ # @raise [ArgumentError] if the given period is not a `Range[Time]` or `Range[nil]`
164
+ def self.range(period)
165
+ raise ArgumentError, 'ULID.range takes only `Range[Time]` or `Range[nil]`' unless Range === period
166
+ begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
167
+ return period if self === begin_element && self === end_element
129
168
 
130
- case begin_time
169
+ case begin_element
131
170
  when Time
132
- begin_ulid = min(moment: begin_time)
171
+ begin_ulid = min(moment: begin_element)
133
172
  when nil
134
- begin_ulid = min
173
+ begin_ulid = MIN
174
+ when self
175
+ begin_ulid = begin_element
135
176
  else
136
- raise ArgumentError, 'ULID.range takes only Range[Time]'
177
+ raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
137
178
  end
138
179
 
139
- case end_time
180
+ case end_element
140
181
  when Time
141
182
  if exclude_end
142
- end_ulid = min(moment: end_time)
183
+ end_ulid = min(moment: end_element)
143
184
  else
144
- end_ulid = max(moment: end_time)
185
+ end_ulid = max(moment: end_element)
145
186
  end
146
187
  when nil
147
188
  # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
148
- end_ulid = max
189
+ end_ulid = MAX
149
190
  exclude_end = false
191
+ when self
192
+ end_ulid = end_element
150
193
  else
151
- raise ArgumentError, 'ULID.range takes only Range[Time]'
194
+ raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
152
195
  end
153
196
 
197
+ begin_ulid.freeze
198
+ end_ulid.freeze
199
+
154
200
  Range.new(begin_ulid, end_ulid, exclude_end)
155
201
  end
156
202
 
157
203
  # @param [Time] time
158
204
  # @return [Time]
159
205
  def self.floor(time)
206
+ raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
207
+
160
208
  if RUBY_VERSION >= '2.7'
161
209
  time.floor(3)
162
210
  else
@@ -164,6 +212,7 @@ class ULID
164
212
  end
165
213
  end
166
214
 
215
+ # @api private
167
216
  # @return [Integer]
168
217
  def self.current_milliseconds
169
218
  milliseconds_from_time(Time.now)
@@ -171,16 +220,25 @@ class ULID
171
220
 
172
221
  # @param [Time] time
173
222
  # @return [Integer]
174
- def self.milliseconds_from_time(time)
223
+ private_class_method def self.milliseconds_from_time(time)
175
224
  (time.to_r * 1000).to_i
176
225
  end
177
226
 
227
+ # @api private
178
228
  # @param [Time, Integer] moment
179
229
  # @return [Integer]
180
230
  def self.milliseconds_from_moment(moment)
181
- moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
231
+ case moment
232
+ when Integer
233
+ moment
234
+ when Time
235
+ milliseconds_from_time(moment)
236
+ else
237
+ raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
238
+ end
182
239
  end
183
240
 
241
+ # @api private
184
242
  # @return [Integer]
185
243
  def self.reasonable_entropy
186
244
  SecureRandom.random_number(MAX_ENTROPY)
@@ -189,56 +247,30 @@ class ULID
189
247
  # @param [String, #to_str] string
190
248
  # @return [ULID]
191
249
  # @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
250
  def self.parse(string)
194
- begin
195
- 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)
203
- rescue => err
204
- raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
251
+ string = String.try_convert(string)
252
+ raise ArgumentError, 'ULID.parse takes only strings' unless string
253
+
254
+ unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
255
+ raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
205
256
  end
206
-
207
- new milliseconds: milliseconds, entropy: entropy
257
+
258
+ from_integer(CrockfordBase32.decode(string))
208
259
  end
209
260
 
261
+ # @param [String, #to_str] string
210
262
  # @return [Boolean]
211
263
  def self.valid?(string)
212
- parse(string)
213
- rescue Exception
214
- false
215
- else
216
- true
264
+ string = String.try_convert(string)
265
+ string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
217
266
  end
218
267
 
219
268
  # @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
- # @api private
232
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
233
- # @param [Array<Integer>] reversed_digits
234
- # @return [Integer]
235
- def self.inverse_of_digits(reversed_digits)
236
- base = 256
237
- num = 0
238
- reversed_digits.each do |digit|
239
- num = (num * base) + digit
240
- end
241
- num
269
+ # @param [MonotonicGenerator] generator
270
+ # @return [ULID]
271
+ def self.from_monotonic_generator(generator)
272
+ raise ArgumentError, 'this method provided only for MonotonicGenerator' unless MonotonicGenerator === generator
273
+ new milliseconds: generator.latest_milliseconds, entropy: generator.latest_entropy
242
274
  end
243
275
 
244
276
  attr_reader :milliseconds, :entropy
@@ -246,15 +278,21 @@ class ULID
246
278
  # @api private
247
279
  # @param [Integer] milliseconds
248
280
  # @param [Integer] entropy
281
+ # @param [Integer] integer
249
282
  # @return [void]
250
283
  # @raise [OverflowError] if the given value is larger than the ULID limit
251
284
  # @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?
285
+ def initialize(milliseconds:, entropy:, integer: nil)
286
+ if integer
287
+ @integer = integer
288
+ else
289
+ milliseconds = milliseconds.to_int
290
+ entropy = entropy.to_int
291
+
292
+ raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
293
+ raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
294
+ raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
295
+ end
258
296
 
259
297
  @milliseconds = milliseconds
260
298
  @entropy = entropy
@@ -262,18 +300,22 @@ class ULID
262
300
 
263
301
  # @return [String]
264
302
  def to_s
265
- @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
303
+ @string ||= CrockfordBase32.encode(to_i).freeze
266
304
  end
267
305
 
268
306
  # @return [Integer]
269
307
  def to_i
270
- @integer ||= self.class.inverse_of_digits(octets)
308
+ @integer ||= begin
309
+ n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
310
+ n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
311
+ (n32encoded_timestamp + n32encoded_randomness).to_i(32)
312
+ end
271
313
  end
272
314
  alias_method :hash, :to_i
273
315
 
274
316
  # @return [Integer, nil]
275
317
  def <=>(other)
276
- other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
318
+ (ULID === other) ? (to_i <=> other.to_i) : nil
277
319
  end
278
320
 
279
321
  # @return [String]
@@ -283,7 +325,7 @@ class ULID
283
325
 
284
326
  # @return [Boolean]
285
327
  def eql?(other)
286
- other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
328
+ equal?(other) || (ULID === other && to_i == other.to_i)
287
329
  end
288
330
  alias_method :==, :eql?
289
331
 
@@ -291,13 +333,9 @@ class ULID
291
333
  def ===(other)
292
334
  case other
293
335
  when ULID
294
- self == other
336
+ to_i == other.to_i
295
337
  when String
296
- begin
297
- self == self.class.parse(other)
298
- rescue Exception
299
- false
300
- end
338
+ to_s == other.upcase
301
339
  else
302
340
  false
303
341
  end
@@ -316,79 +354,96 @@ class ULID
316
354
 
317
355
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
318
356
  def octets
319
- @octets ||= (timestamp_octets + randomness_octets).freeze
357
+ digits = to_i.digits(256)
358
+ (OCTETS_LENGTH - digits.size).times do
359
+ digits.push 0
360
+ end
361
+ digits.reverse!
320
362
  end
321
363
 
322
364
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
323
365
  def timestamp_octets
324
- @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
366
+ octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
325
367
  end
326
368
 
327
369
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
328
370
  def randomness_octets
329
- @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
371
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
330
372
  end
331
373
 
332
374
  # @return [String]
333
375
  def timestamp
334
- @timestamp ||= matchdata[:timestamp].freeze
376
+ @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
335
377
  end
336
378
 
337
379
  # @return [String]
338
380
  def randomness
339
- @randomness ||= matchdata[:randomness].freeze
340
- end
341
-
342
- # @return [Regexp]
343
- def pattern
344
- @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
381
+ @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
345
382
  end
346
383
 
347
- # @return [Regexp]
348
- def strict_pattern
349
- @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
384
+ # @note Providing for rough operations. The keys and values is not fixed.
385
+ # @return [Hash{Symbol => Regexp, String}]
386
+ def patterns
387
+ named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
388
+ {
389
+ named_captures: named_captures,
390
+ strict_named_captures: /\A#{named_captures.source}\z/i.freeze
391
+ }
350
392
  end
351
393
 
352
394
  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
353
- def next
354
- next_int = to_i.next
355
- return nil if next_int > MAX_INTEGER
356
- @next ||= self.class.from_integer(next_int)
395
+ def succ
396
+ succ_int = to_i.succ
397
+ if succ_int >= MAX_INTEGER
398
+ if succ_int == MAX_INTEGER
399
+ MAX
400
+ else
401
+ nil
402
+ end
403
+ else
404
+ ULID.from_integer(succ_int)
405
+ end
357
406
  end
358
- alias_method :succ, :next
407
+ alias_method :next, :succ
359
408
 
360
409
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
361
410
  def pred
362
- pre_int = to_i.pred
363
- return nil if pre_int.negative?
364
- @pred ||= self.class.from_integer(pre_int)
411
+ pred_int = to_i.pred
412
+ if pred_int <= 0
413
+ if pred_int == 0
414
+ MIN
415
+ else
416
+ nil
417
+ end
418
+ else
419
+ ULID.from_integer(pred_int)
420
+ end
365
421
  end
366
422
 
367
423
  # @return [self]
368
424
  def freeze
369
- # Evaluate all caching
370
- inspect
371
- octets
372
- to_i
373
- succ
374
- pred
375
- strict_pattern
425
+ # Need to cache before freezing, because frozen objects can't assign instance variables
426
+ cache_all_instance_variables
376
427
  super
377
428
  end
378
429
 
379
430
  private
380
431
 
381
- # @return [MatchData]
382
- def matchdata
383
- @matchdata ||= STRICT_PATTERN.match(to_s).freeze
432
+ # @return [void]
433
+ def cache_all_instance_variables
434
+ inspect
435
+ timestamp
436
+ randomness
384
437
  end
385
438
  end
386
439
 
387
440
  require_relative 'ulid/version'
441
+ require_relative 'ulid/crockford_base32'
388
442
  require_relative 'ulid/monotonic_generator'
389
443
 
390
444
  class ULID
391
- MONOTONIC_GENERATOR = MonotonicGenerator.new
445
+ MIN = parse('00000000000000000000000000').freeze
446
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
392
447
 
393
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
448
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
394
449
  end