ruby-ulid 0.0.14 → 0.0.19

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