ruby-ulid 0.0.15 → 0.1.0
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 +4 -4
- data/README.md +186 -24
- data/lib/ulid.rb +211 -175
- data/lib/ulid/crockford_base32.rb +68 -0
- data/lib/ulid/monotonic_generator.rb +9 -7
- data/lib/ulid/uuid.rb +38 -0
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +42 -34
- metadata +8 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0aff39d0f8044f373d8dcdea149f26501e0118dfc0876f060b1b757a4d2a49e
|
4
|
+
data.tar.gz: 02df0ea7a5fe5f8dabd1179988cfe4c2574debf3ad7200d11445eef932dd7443
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab036ed40e4f2740de9d37e44d2c6d4d233fa0071f95383accb2334c4d1f85b30b24eea902e0e29c34fefc8749b8caf5d1f9df6c52f93fe2e76c89aa0912d11e
|
7
|
+
data.tar.gz: d8ac11f63e5b301ed8b1eacd0d1211fd4b3a0bd79ccae8fe87050833cf305a7345da1dc136b95ecccfcafc6110cf9a281afb0f15db1fe2e01984bf320f82b7e5
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
-
|
3
|
+
## Overview
|
4
4
|
|
5
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.
|
@@ -33,16 +33,26 @@ Instead, herein is proposed ULID:
|
|
33
33
|
- No special characters (URL safe)
|
34
34
|
- Monotonic sort order (correctly detects and handles the same millisecond)
|
35
35
|
|
36
|
-
##
|
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
|
-
|
46
|
+
Should be installed!
|
43
47
|
```
|
44
48
|
|
45
|
-
|
49
|
+
Add this line to your application/library's `Gemfile` is needed in basic use-case
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
gem 'ruby-ulid', '>= 0.1.0', '< 0.2.0'
|
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
|
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.
|
102
|
+
ULID.at(time + n)
|
87
103
|
end
|
88
104
|
ulids.sort == ulids #=> true
|
89
105
|
```
|
@@ -98,7 +114,9 @@ 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
|
-
|
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
|
@@ -128,20 +146,47 @@ sample_ulids_by_the_time.take(5) #=>
|
|
128
146
|
ulids.sort == ulids #=> true
|
129
147
|
```
|
130
148
|
|
131
|
-
|
132
|
-
|
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(
|
141
|
-
ulids.
|
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,26 +219,45 @@ EOD
|
|
174
219
|
|
175
220
|
ULID.scan(json).to_a
|
176
221
|
#=>
|
177
|
-
[ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
|
246
|
+
It can take `Time` instance as an optional argument. Then returns min/max ID that has limit of randomness part in the time.
|
247
|
+
|
187
248
|
```ruby
|
188
249
|
ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
|
189
250
|
ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
|
190
251
|
|
191
252
|
time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
|
192
|
-
ULID.min(
|
193
|
-
ULID.max(
|
253
|
+
ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
|
254
|
+
ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
194
255
|
```
|
195
256
|
|
196
|
-
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
257
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID.
|
258
|
+
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
259
|
+
|
260
|
+
NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
|
197
261
|
|
198
262
|
```ruby
|
199
263
|
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
|
@@ -209,12 +273,66 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
|
|
209
273
|
ULID.parse('00000000000000000000000000').pred #=> nil
|
210
274
|
```
|
211
275
|
|
276
|
+
`ULID.sample` returns random ULIDs.
|
277
|
+
|
278
|
+
Basically ignores generating time.
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
282
|
+
ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
|
283
|
+
ULID.sample(0) #=> []
|
284
|
+
ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
|
285
|
+
ULID.sample(5)
|
286
|
+
#=>
|
287
|
+
#[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
|
288
|
+
# ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
|
289
|
+
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
|
290
|
+
# ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
|
291
|
+
# ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
|
292
|
+
```
|
293
|
+
|
294
|
+
You can specify a range object for the timestamp restriction, see also `ULID.range`.
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
298
|
+
ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
|
299
|
+
ulids = ULID.sample(1000, period: ulid1..ulid2)
|
300
|
+
ulids.uniq.size #=> 1000
|
301
|
+
ulids.take(10)
|
302
|
+
#=>
|
303
|
+
#[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
|
304
|
+
# ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
|
305
|
+
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
|
306
|
+
# ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
|
307
|
+
# ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
|
308
|
+
# ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
|
309
|
+
# ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
|
310
|
+
# ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
|
311
|
+
# ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
|
312
|
+
# ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
|
313
|
+
ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
|
314
|
+
#=>
|
315
|
+
# [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
|
316
|
+
# ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
|
317
|
+
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
|
318
|
+
# ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
|
319
|
+
# ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
|
320
|
+
# ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
|
321
|
+
# ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
|
322
|
+
# ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
|
323
|
+
# ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
|
324
|
+
# ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
|
325
|
+
```
|
326
|
+
|
212
327
|
### UUIDv4 converter for migration use-cases
|
213
328
|
|
214
329
|
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
215
|
-
The imported timestamp is meaningless. So ULID's benefit will lost
|
330
|
+
The imported timestamp is meaningless. So ULID's benefit will lost.
|
216
331
|
|
217
332
|
```ruby
|
333
|
+
# Currently experimental feature, so needed to load the extension.
|
334
|
+
require 'ulid/uuid'
|
335
|
+
|
218
336
|
# Basically reversible
|
219
337
|
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
220
338
|
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
@@ -239,6 +357,50 @@ ULID.min == reversed_min #=> false
|
|
239
357
|
ULID.max == reversed_max #=> false
|
240
358
|
```
|
241
359
|
|
360
|
+
## How to migrate from other gems
|
361
|
+
|
362
|
+
As far as I know, major prior arts are below
|
363
|
+
|
364
|
+
### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
|
365
|
+
|
366
|
+
It is just providing basic `String` generator only.
|
367
|
+
So you can replace the code as below
|
368
|
+
|
369
|
+
```diff
|
370
|
+
-ULID.generate
|
371
|
+
+ULID.generate.to_s
|
372
|
+
```
|
373
|
+
|
374
|
+
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.
|
375
|
+
|
376
|
+
1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
|
377
|
+
1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
|
378
|
+
1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
|
379
|
+
|
380
|
+
### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
|
381
|
+
|
382
|
+
It is providing basic generator(except monotonic generator) and parser.
|
383
|
+
Major methods can be replaced as below.
|
384
|
+
|
385
|
+
```diff
|
386
|
+
-ULID.generate
|
387
|
+
+ULID.generate.to_s
|
388
|
+
-ULID.at(time)
|
389
|
+
+ULID.at(time).to_s
|
390
|
+
-ULID.time(string)
|
391
|
+
+ULID.parse(string).to_time
|
392
|
+
-ULID.min_ulid_at(time)
|
393
|
+
+ULID.min(time).to_s
|
394
|
+
-ULID.max_ulid_at(time)
|
395
|
+
+ULID.max(time).to_s
|
396
|
+
```
|
397
|
+
|
398
|
+
NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
|
399
|
+
|
400
|
+
1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
|
401
|
+
1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
|
402
|
+
1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
|
403
|
+
|
242
404
|
## References
|
243
405
|
|
244
406
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
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,61 +16,113 @@ class ULID
|
|
17
16
|
class OverflowError < Error; end
|
18
17
|
class ParserError < Error; end
|
19
18
|
|
20
|
-
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
#
|
38
|
-
|
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]
|
47
53
|
def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
|
48
|
-
|
54
|
+
from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
|
49
55
|
end
|
50
56
|
|
51
|
-
#
|
57
|
+
# Short hand of `ULID.generate(moment: time)`
|
58
|
+
# @param [Time] time
|
52
59
|
# @return [ULID]
|
53
|
-
def self.
|
60
|
+
def self.at(time)
|
61
|
+
raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
|
62
|
+
from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [Time, Integer] moment
|
66
|
+
# @return [ULID]
|
67
|
+
def self.min(moment=0)
|
54
68
|
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
55
69
|
end
|
56
70
|
|
57
|
-
# @param [
|
71
|
+
# @param [Time, Integer] moment
|
58
72
|
# @return [ULID]
|
59
|
-
def self.max(moment
|
73
|
+
def self.max(moment=MAX_MILLISECONDS)
|
60
74
|
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
61
75
|
end
|
62
76
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
104
|
+
RANDOM_INTEGER_GENERATOR
|
72
105
|
end
|
73
106
|
|
74
|
-
|
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"
|
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,77 +130,68 @@ class ULID
|
|
79
130
|
# @yieldparam [ULID] ulid
|
80
131
|
# @yieldreturn [self]
|
81
132
|
def self.scan(string)
|
82
|
-
string = string
|
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(
|
85
|
-
yield parse(
|
136
|
+
string.scan(SCANNING_PATTERN) do |matched|
|
137
|
+
yield parse(matched)
|
86
138
|
end
|
87
139
|
self
|
88
140
|
end
|
89
141
|
|
90
|
-
# @param [
|
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
|
-
# @param [Integer, #to_int] integer
|
142
|
+
# @param [Integer] 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
|
+
raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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>, Range<nil
|
161
|
+
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
125
162
|
# @return [Range<ULID>]
|
126
|
-
# @raise [ArgumentError] if the given
|
127
|
-
def self.range(
|
128
|
-
raise
|
129
|
-
|
163
|
+
# @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
|
164
|
+
def self.range(period)
|
165
|
+
raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' 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
|
130
168
|
|
131
|
-
case
|
169
|
+
case begin_element
|
132
170
|
when Time
|
133
|
-
begin_ulid = min(
|
171
|
+
begin_ulid = min(begin_element)
|
134
172
|
when nil
|
135
|
-
begin_ulid =
|
173
|
+
begin_ulid = MIN
|
174
|
+
when self
|
175
|
+
begin_ulid = begin_element
|
136
176
|
else
|
137
|
-
raise
|
177
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
|
138
178
|
end
|
139
179
|
|
140
|
-
case
|
180
|
+
case end_element
|
141
181
|
when Time
|
142
182
|
if exclude_end
|
143
|
-
end_ulid = min(
|
183
|
+
end_ulid = min(end_element)
|
144
184
|
else
|
145
|
-
end_ulid = max(
|
185
|
+
end_ulid = max(end_element)
|
146
186
|
end
|
147
187
|
when nil
|
148
188
|
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
149
|
-
end_ulid =
|
189
|
+
end_ulid = MAX
|
150
190
|
exclude_end = false
|
191
|
+
when self
|
192
|
+
end_ulid = end_element
|
151
193
|
else
|
152
|
-
raise
|
194
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
|
153
195
|
end
|
154
196
|
|
155
197
|
begin_ulid.freeze
|
@@ -161,6 +203,8 @@ class ULID
|
|
161
203
|
# @param [Time] time
|
162
204
|
# @return [Time]
|
163
205
|
def self.floor(time)
|
206
|
+
raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
|
207
|
+
|
164
208
|
if RUBY_VERSION >= '2.7'
|
165
209
|
time.floor(3)
|
166
210
|
else
|
@@ -168,23 +212,34 @@ class ULID
|
|
168
212
|
end
|
169
213
|
end
|
170
214
|
|
215
|
+
# @api private
|
171
216
|
# @return [Integer]
|
172
217
|
def self.current_milliseconds
|
173
218
|
milliseconds_from_time(Time.now)
|
174
219
|
end
|
175
220
|
|
221
|
+
# @api private
|
176
222
|
# @param [Time] time
|
177
223
|
# @return [Integer]
|
178
|
-
def self.milliseconds_from_time(time)
|
224
|
+
private_class_method def self.milliseconds_from_time(time)
|
179
225
|
(time.to_r * 1000).to_i
|
180
226
|
end
|
181
227
|
|
228
|
+
# @api private
|
182
229
|
# @param [Time, Integer] moment
|
183
230
|
# @return [Integer]
|
184
231
|
def self.milliseconds_from_moment(moment)
|
185
|
-
|
232
|
+
case moment
|
233
|
+
when Integer
|
234
|
+
moment
|
235
|
+
when Time
|
236
|
+
milliseconds_from_time(moment)
|
237
|
+
else
|
238
|
+
raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
|
239
|
+
end
|
186
240
|
end
|
187
241
|
|
242
|
+
# @api private
|
188
243
|
# @return [Integer]
|
189
244
|
def self.reasonable_entropy
|
190
245
|
SecureRandom.random_number(MAX_ENTROPY)
|
@@ -193,61 +248,41 @@ class ULID
|
|
193
248
|
# @param [String, #to_str] string
|
194
249
|
# @return [ULID]
|
195
250
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
196
|
-
# @raise [OverflowError] if the given value is larger than the ULID limit
|
197
251
|
def self.parse(string)
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
|
204
|
-
randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
205
|
-
milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
|
206
|
-
entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
|
207
|
-
rescue => err
|
208
|
-
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
252
|
+
string = String.try_convert(string)
|
253
|
+
raise ArgumentError, 'ULID.parse takes only strings' unless string
|
254
|
+
|
255
|
+
unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
|
256
|
+
raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
|
209
257
|
end
|
210
|
-
|
211
|
-
|
258
|
+
|
259
|
+
from_integer(CrockfordBase32.decode(string))
|
212
260
|
end
|
213
261
|
|
262
|
+
# @param [String, #to_str] string
|
214
263
|
# @return [Boolean]
|
215
264
|
def self.valid?(string)
|
216
|
-
|
217
|
-
|
218
|
-
false
|
219
|
-
else
|
220
|
-
true
|
265
|
+
string = String.try_convert(string)
|
266
|
+
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
|
221
267
|
end
|
222
268
|
|
223
269
|
# @api private
|
224
|
-
# @param [Integer]
|
225
|
-
# @param [Integer]
|
226
|
-
# @return [
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
270
|
+
# @param [Integer] milliseconds
|
271
|
+
# @param [Integer] entropy
|
272
|
+
# @return [ULID]
|
273
|
+
# @raise [OverflowError] if the given value is larger than the ULID limit
|
274
|
+
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
275
|
+
def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
|
276
|
+
raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
|
277
|
+
raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
|
278
|
+
raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
|
279
|
+
raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
|
234
280
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
# @return [Integer]
|
239
|
-
def self.inverse_of_digits(reversed_digits)
|
240
|
-
base = 256
|
241
|
-
num = 0
|
242
|
-
reversed_digits.each do |digit|
|
243
|
-
num = (num * base) + digit
|
244
|
-
end
|
245
|
-
num
|
246
|
-
end
|
281
|
+
n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
282
|
+
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
283
|
+
integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)
|
247
284
|
|
248
|
-
|
249
|
-
private_class_method def self.argument_error_for_range_building(argument)
|
250
|
-
ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
|
285
|
+
new milliseconds: milliseconds, entropy: entropy, integer: integer
|
251
286
|
end
|
252
287
|
|
253
288
|
attr_reader :milliseconds, :entropy
|
@@ -255,34 +290,29 @@ class ULID
|
|
255
290
|
# @api private
|
256
291
|
# @param [Integer] milliseconds
|
257
292
|
# @param [Integer] entropy
|
293
|
+
# @param [Integer] integer
|
258
294
|
# @return [void]
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
milliseconds = milliseconds.to_int
|
263
|
-
entropy = entropy.to_int
|
264
|
-
raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
|
265
|
-
raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
|
266
|
-
raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
|
267
|
-
|
295
|
+
def initialize(milliseconds:, entropy:, integer:)
|
296
|
+
# All arguments check should be done with each constructors, not here
|
297
|
+
@integer = integer
|
268
298
|
@milliseconds = milliseconds
|
269
299
|
@entropy = entropy
|
270
300
|
end
|
271
301
|
|
272
302
|
# @return [String]
|
273
303
|
def to_s
|
274
|
-
@string ||=
|
304
|
+
@string ||= CrockfordBase32.encode(@integer).freeze
|
275
305
|
end
|
276
306
|
|
277
307
|
# @return [Integer]
|
278
308
|
def to_i
|
279
|
-
@integer
|
309
|
+
@integer
|
280
310
|
end
|
281
311
|
alias_method :hash, :to_i
|
282
312
|
|
283
313
|
# @return [Integer, nil]
|
284
314
|
def <=>(other)
|
285
|
-
|
315
|
+
(ULID === other) ? (@integer <=> other.to_i) : nil
|
286
316
|
end
|
287
317
|
|
288
318
|
# @return [String]
|
@@ -292,7 +322,7 @@ class ULID
|
|
292
322
|
|
293
323
|
# @return [Boolean]
|
294
324
|
def eql?(other)
|
295
|
-
|
325
|
+
equal?(other) || (ULID === other && @integer == other.to_i)
|
296
326
|
end
|
297
327
|
alias_method :==, :eql?
|
298
328
|
|
@@ -300,13 +330,9 @@ class ULID
|
|
300
330
|
def ===(other)
|
301
331
|
case other
|
302
332
|
when ULID
|
303
|
-
|
333
|
+
@integer == other.to_i
|
304
334
|
when String
|
305
|
-
|
306
|
-
self == self.class.parse(other)
|
307
|
-
rescue Exception
|
308
|
-
false
|
309
|
-
end
|
335
|
+
to_s == other.upcase
|
310
336
|
else
|
311
337
|
false
|
312
338
|
end
|
@@ -325,62 +351,69 @@ class ULID
|
|
325
351
|
|
326
352
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
327
353
|
def octets
|
328
|
-
|
354
|
+
digits = @integer.digits(256)
|
355
|
+
(OCTETS_LENGTH - digits.size).times do
|
356
|
+
digits.push 0
|
357
|
+
end
|
358
|
+
digits.reverse!
|
329
359
|
end
|
330
360
|
|
331
361
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
332
362
|
def timestamp_octets
|
333
|
-
|
363
|
+
octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
|
334
364
|
end
|
335
365
|
|
336
366
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
337
367
|
def randomness_octets
|
338
|
-
|
368
|
+
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
|
339
369
|
end
|
340
370
|
|
341
371
|
# @return [String]
|
342
372
|
def timestamp
|
343
|
-
@timestamp ||=
|
373
|
+
@timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
|
344
374
|
end
|
345
375
|
|
346
376
|
# @return [String]
|
347
377
|
def randomness
|
348
|
-
@randomness ||=
|
378
|
+
@randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
|
349
379
|
end
|
350
380
|
|
351
|
-
# @
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
381
|
+
# @note Providing for rough operations. The keys and values is not fixed.
|
382
|
+
# @return [Hash{Symbol => Regexp, String}]
|
383
|
+
def patterns
|
384
|
+
named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
385
|
+
{
|
386
|
+
named_captures: named_captures,
|
387
|
+
strict_named_captures: /\A#{named_captures.source}\z/i.freeze
|
388
|
+
}
|
359
389
|
end
|
360
390
|
|
361
391
|
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
362
|
-
def
|
363
|
-
|
364
|
-
|
365
|
-
|
392
|
+
def succ
|
393
|
+
succ_int = @integer.succ
|
394
|
+
if succ_int >= MAX_INTEGER
|
395
|
+
if succ_int == MAX_INTEGER
|
396
|
+
MAX
|
397
|
+
else
|
398
|
+
nil
|
399
|
+
end
|
400
|
+
else
|
401
|
+
ULID.from_integer(succ_int)
|
402
|
+
end
|
366
403
|
end
|
367
|
-
alias_method :
|
404
|
+
alias_method :next, :succ
|
368
405
|
|
369
406
|
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
370
407
|
def pred
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
array = octets.pack('C*').unpack('NnnnnN')
|
381
|
-
array[2] = (array[2] & 0x0fff) | 0x4000
|
382
|
-
array[3] = (array[3] & 0x3fff) | 0x8000
|
383
|
-
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
408
|
+
pred_int = @integer.pred
|
409
|
+
if pred_int <= 0
|
410
|
+
if pred_int == 0
|
411
|
+
MIN
|
412
|
+
else
|
413
|
+
nil
|
414
|
+
end
|
415
|
+
else
|
416
|
+
ULID.from_integer(pred_int)
|
384
417
|
end
|
385
418
|
end
|
386
419
|
|
@@ -391,32 +424,35 @@ class ULID
|
|
391
424
|
super
|
392
425
|
end
|
393
426
|
|
394
|
-
|
427
|
+
# @return [self]
|
428
|
+
def dup
|
429
|
+
self
|
430
|
+
end
|
395
431
|
|
396
|
-
# @return [
|
397
|
-
def
|
398
|
-
|
432
|
+
# @return [self]
|
433
|
+
def clone(freeze: true)
|
434
|
+
self
|
399
435
|
end
|
400
436
|
|
437
|
+
undef_method :instance_variable_set
|
438
|
+
|
439
|
+
private
|
440
|
+
|
401
441
|
# @return [void]
|
402
442
|
def cache_all_instance_variables
|
403
443
|
inspect
|
404
|
-
|
405
|
-
|
406
|
-
succ
|
407
|
-
pred
|
408
|
-
strict_pattern
|
409
|
-
to_uuidv4
|
444
|
+
timestamp
|
445
|
+
randomness
|
410
446
|
end
|
411
447
|
end
|
412
448
|
|
413
449
|
require_relative 'ulid/version'
|
450
|
+
require_relative 'ulid/crockford_base32'
|
414
451
|
require_relative 'ulid/monotonic_generator'
|
415
452
|
|
416
453
|
class ULID
|
417
454
|
MIN = parse('00000000000000000000000000').freeze
|
418
455
|
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
419
|
-
MONOTONIC_GENERATOR = MonotonicGenerator.new
|
420
456
|
|
421
|
-
private_constant :
|
457
|
+
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
|
422
458
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
4
|
+
|
5
|
+
class ULID
|
6
|
+
# Currently supporting only for `subset` for actual use-case`
|
7
|
+
# Original decoding spec allows other characters.
|
8
|
+
# But I think ULID should allow `subset` of Crockford's Base32.
|
9
|
+
# See below
|
10
|
+
# * https://github.com/ulid/spec/pull/57
|
11
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
12
|
+
# * https://github.com/kachick/ruby-ulid/issues/78
|
13
|
+
module CrockfordBase32
|
14
|
+
class SetupError < ScriptError; end
|
15
|
+
|
16
|
+
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
17
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
18
|
+
|
19
|
+
n32_char_by_number = {}
|
20
|
+
n32_chars.each_with_index do |char, index|
|
21
|
+
n32_char_by_number[index] = char
|
22
|
+
end
|
23
|
+
n32_char_by_number.freeze
|
24
|
+
|
25
|
+
crockford_base32_mappings = {
|
26
|
+
'J' => 18,
|
27
|
+
'K' => 19,
|
28
|
+
'M' => 20,
|
29
|
+
'N' => 21,
|
30
|
+
'P' => 22,
|
31
|
+
'Q' => 23,
|
32
|
+
'R' => 24,
|
33
|
+
'S' => 25,
|
34
|
+
'T' => 26,
|
35
|
+
'V' => 27,
|
36
|
+
'W' => 28,
|
37
|
+
'X' => 29,
|
38
|
+
'Y' => 30,
|
39
|
+
'Z' => 31
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR = CROCKFORD_BASE32_ENCODING_STRING.chars.map(&:freeze).each_with_object({}) do |encoding_char, map|
|
43
|
+
if n = crockford_base32_mappings[encoding_char]
|
44
|
+
char_32 = n32_char_by_number.fetch(n)
|
45
|
+
map[encoding_char] = char_32
|
46
|
+
end
|
47
|
+
end.freeze
|
48
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
|
49
|
+
CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
|
50
|
+
|
51
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
|
52
|
+
N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
|
53
|
+
|
54
|
+
# @param [String] string
|
55
|
+
# @return [Integer]
|
56
|
+
def self.decode(string)
|
57
|
+
n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
|
58
|
+
n32encoded.to_i(32)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Integer] integer
|
62
|
+
# @return [String]
|
63
|
+
def self.encode(integer)
|
64
|
+
n32encoded = integer.to_s(32)
|
65
|
+
n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -8,6 +8,7 @@ class ULID
|
|
8
8
|
attr_accessor :latest_milliseconds, :latest_entropy
|
9
9
|
|
10
10
|
def initialize
|
11
|
+
@mutex = Thread::Mutex.new
|
11
12
|
reset
|
12
13
|
end
|
13
14
|
|
@@ -19,14 +20,15 @@ class ULID
|
|
19
20
|
milliseconds = ULID.milliseconds_from_moment(moment)
|
20
21
|
raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
|
21
22
|
|
22
|
-
|
23
|
-
@latest_milliseconds
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
@mutex.synchronize do
|
24
|
+
if @latest_milliseconds < milliseconds
|
25
|
+
@latest_milliseconds = milliseconds
|
26
|
+
@latest_entropy = ULID.reasonable_entropy
|
27
|
+
else
|
28
|
+
@latest_entropy += 1
|
29
|
+
end
|
30
|
+
ULID.from_milliseconds_and_entropy(milliseconds: @latest_milliseconds, entropy: @latest_entropy)
|
27
31
|
end
|
28
|
-
|
29
|
-
ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
|
30
32
|
end
|
31
33
|
|
32
34
|
# @api private
|
data/lib/ulid/uuid.rb
ADDED
@@ -0,0 +1,38 @@
|
|
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
|
+
uuid = String.try_convert(uuid)
|
19
|
+
raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid
|
20
|
+
|
21
|
+
prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
|
22
|
+
unless UUIDV4_PATTERN.match?(prefix_trimmed)
|
23
|
+
raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
|
24
|
+
end
|
25
|
+
|
26
|
+
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
27
|
+
from_integer(normalized.to_i(16))
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String]
|
31
|
+
def to_uuidv4
|
32
|
+
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
33
|
+
array = octets.pack('C*').unpack('NnnnnN')
|
34
|
+
array[2] = (array[2] & 0x0fff) | 0x4000
|
35
|
+
array[3] = (array[3] & 0x3fff) | 0x8000
|
36
|
+
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
37
|
+
end
|
38
|
+
end
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Classes
|
2
2
|
class ULID
|
3
3
|
VERSION: String
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
CROCKFORD_BASE32_ENCODING_STRING: String
|
5
|
+
TIMESTAMP_ENCODED_LENGTH: 10
|
6
|
+
RANDOMNESS_ENCODED_LENGTH: 16
|
7
|
+
ENCODED_LENGTH: 26
|
8
8
|
TIMESTAMP_OCTETS_LENGTH: 6
|
9
9
|
RANDOMNESS_OCTETS_LENGTH: 10
|
10
10
|
OCTETS_LENGTH: 16
|
@@ -12,10 +12,11 @@ class ULID
|
|
12
12
|
MAX_ENTROPY: 1208925819614629174706175
|
13
13
|
MAX_INTEGER: 340282366920938463463374607431768211455
|
14
14
|
TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
|
15
|
-
|
16
|
-
|
15
|
+
RANDOM_INTEGER_GENERATOR: ^() -> Integer
|
16
|
+
PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
|
17
|
+
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
|
18
|
+
SCANNING_PATTERN: Regexp
|
17
19
|
UUIDV4_PATTERN: Regexp
|
18
|
-
MONOTONIC_GENERATOR: MonotonicGenerator
|
19
20
|
MIN: ULID
|
20
21
|
MAX: ULID
|
21
22
|
include Comparable
|
@@ -32,6 +33,19 @@ class ULID
|
|
32
33
|
class ParserError < Error
|
33
34
|
end
|
34
35
|
|
36
|
+
module CrockfordBase32
|
37
|
+
class SetupError < ScriptError
|
38
|
+
end
|
39
|
+
|
40
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
|
41
|
+
CROCKFORD_BASE32_CHAR_PATTERN: Regexp
|
42
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
|
43
|
+
N32_CHAR_PATTERN: Regexp
|
44
|
+
|
45
|
+
def self.encode: (Integer integer) -> String
|
46
|
+
def self.decode: (String string) -> Integer
|
47
|
+
end
|
48
|
+
|
35
49
|
class MonotonicGenerator
|
36
50
|
attr_accessor latest_milliseconds: Integer
|
37
51
|
attr_accessor latest_entropy: Integer
|
@@ -44,50 +58,43 @@ class ULID
|
|
44
58
|
type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
45
59
|
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
|
46
60
|
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
61
|
+
type period = Range[Time] | Range[nil] | Range[ULID]
|
47
62
|
|
48
63
|
@milliseconds: Integer
|
49
64
|
@entropy: Integer
|
50
65
|
@string: String?
|
51
|
-
@integer: Integer
|
52
|
-
@octets: octets?
|
53
|
-
@timestamp_octets: timestamp_octets?
|
54
|
-
@randomness_octets: randomness_octets?
|
66
|
+
@integer: Integer
|
55
67
|
@timestamp: String?
|
56
68
|
@randomness: String?
|
57
69
|
@inspect: String?
|
58
70
|
@time: Time?
|
59
|
-
@next: ULID?
|
60
|
-
@pattern: Regexp?
|
61
|
-
@strict_pattern: Regexp?
|
62
|
-
@uuidv4: String?
|
63
|
-
@matchdata: MatchData?
|
64
71
|
|
65
|
-
def self.generate: (?moment: moment, ?entropy: Integer) ->
|
66
|
-
def self.
|
72
|
+
def self.generate: (?moment: moment, ?entropy: Integer) -> self
|
73
|
+
def self.at: (Time time) -> self
|
67
74
|
def self.current_milliseconds: -> Integer
|
68
|
-
def self.milliseconds_from_time: (Time time) -> Integer
|
69
75
|
def self.milliseconds_from_moment: (moment moment) -> Integer
|
70
|
-
def self.range: (
|
76
|
+
def self.range: (period period) -> Range[ULID]
|
71
77
|
def self.floor: (Time time) -> Time
|
72
78
|
def self.reasonable_entropy: -> Integer
|
73
|
-
def self.parse: (String string) ->
|
79
|
+
def self.parse: (String string) -> self
|
74
80
|
def self.from_uuidv4: (String uuid) -> ULID
|
75
|
-
def self.from_integer: (Integer integer) ->
|
76
|
-
def self.min: (?moment
|
77
|
-
def self.max: (?moment
|
81
|
+
def self.from_integer: (Integer integer) -> self
|
82
|
+
def self.min: (?moment moment) -> ULID
|
83
|
+
def self.max: (?moment moment) -> ULID
|
84
|
+
def self.sample: (?period: period) -> self
|
85
|
+
| (Integer number, ?period: period) -> Array[self]
|
78
86
|
def self.valid?: (untyped string) -> bool
|
79
|
-
def self.scan: (String string) -> Enumerator[
|
80
|
-
| (String string) { (
|
81
|
-
def self.
|
82
|
-
def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
|
87
|
+
def self.scan: (String string) -> Enumerator[self, singleton(ULID)]
|
88
|
+
| (String string) { (self ulid) -> void } -> singleton(ULID)
|
89
|
+
def self.from_milliseconds_and_entropy: (milliseconds: Integer, entropy: Integer) -> self
|
83
90
|
attr_reader milliseconds: Integer
|
84
91
|
attr_reader entropy: Integer
|
85
|
-
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
92
|
+
def initialize: (milliseconds: Integer, entropy: Integer, integer: Integer) -> void
|
86
93
|
def to_s: -> String
|
87
94
|
def to_i: -> Integer
|
88
95
|
alias hash to_i
|
89
96
|
def <=>: (ULID other) -> Integer
|
90
|
-
| (untyped other) ->
|
97
|
+
| (untyped other) -> nil
|
91
98
|
def inspect: -> String
|
92
99
|
def eql?: (untyped other) -> bool
|
93
100
|
alias == eql?
|
@@ -95,8 +102,7 @@ class ULID
|
|
95
102
|
def to_time: -> Time
|
96
103
|
def timestamp: -> String
|
97
104
|
def randomness: -> String
|
98
|
-
def
|
99
|
-
def strict_pattern: -> Regexp
|
105
|
+
def patterns: -> Hash[Symbol, Regexp | String]
|
100
106
|
def octets: -> octets
|
101
107
|
def timestamp_octets: -> timestamp_octets
|
102
108
|
def randomness_octets: -> randomness_octets
|
@@ -105,9 +111,11 @@ class ULID
|
|
105
111
|
alias succ next
|
106
112
|
def pred: -> ULID?
|
107
113
|
def freeze: -> self
|
114
|
+
def dup: -> self
|
115
|
+
# Same as https://github.com/ruby/rbs/blob/4fb4c33b2325d1a73d79ff7aaeb49f21cec1e0e5/core/object.rbs#L79
|
116
|
+
def clone: (?freeze: bool) -> self
|
108
117
|
|
109
118
|
private
|
110
|
-
def self.
|
111
|
-
def matchdata: -> MatchData
|
119
|
+
def self.milliseconds_from_time: (Time time) -> Integer
|
112
120
|
def cache_all_instance_variables: -> void
|
113
121
|
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
|
4
|
+
version: 0.1.0
|
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-
|
11
|
+
date: 2021-05-09 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:
|
88
|
-
|
89
|
-
|
90
|
-
|
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: []
|
@@ -97,7 +77,9 @@ files:
|
|
97
77
|
- LICENSE
|
98
78
|
- README.md
|
99
79
|
- lib/ulid.rb
|
80
|
+
- lib/ulid/crockford_base32.rb
|
100
81
|
- lib/ulid/monotonic_generator.rb
|
82
|
+
- lib/ulid/uuid.rb
|
101
83
|
- lib/ulid/version.rb
|
102
84
|
- sig/ulid.rbs
|
103
85
|
homepage: https://github.com/kachick/ruby-ulid
|