ruby-ulid 0.0.14 → 0.0.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +219 -33
- data/lib/ulid.rb +210 -155
- 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 +36 -24
- metadata +9 -28
- data/Steepfile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 646d2a4b433ffbe28d4801483d3f1c43cfb5839bf5f80e759447304a1c8dad52
|
4
|
+
data.tar.gz: 1e4ddc37266eb09f3635ba5509e66e6ff93e15fad3e6574fb3b50e8ad9322b10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a564dbad8c3c88b729353826c599e618cd8963806759ce8a6bf041a95aef60044c8f75873560129bcef7550eec226c3bf04bcfec339c4b07fb72c3372342811
|
7
|
+
data.tar.gz: 187c475f9332cb69827e2664193faa7f001cb14a5709b3c28600286ac9e6c1fb3176e14e3a7ae3a658d232af6157a3dea83ad992cee75db966ff80213c3a0e52
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
-
|
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
|
-
##
|
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.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
|
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,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
|
-
|
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
|
-
|
124
|
+
ulids = 10000.times.map do
|
107
125
|
monotonic_generator.generate
|
108
126
|
end
|
109
|
-
sample_ulids_by_the_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
|
-
|
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
|
-
|
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,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
|
-
|
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)]
|
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
|
-
|
274
|
+
`ULID.sample` returns random ULIDs.
|
275
|
+
|
276
|
+
Basically ignores generating time.
|
213
277
|
|
214
278
|
```ruby
|
215
|
-
ULID.
|
216
|
-
#=> ULID(
|
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
|
-
|
225
|
-
|
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
|
-
|
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]
|
@@ -48,30 +54,75 @@ class ULID
|
|
48
54
|
new 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.
|
54
|
-
|
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.
|
60
|
-
generate(moment: moment, entropy:
|
67
|
+
def self.min(moment: 0)
|
68
|
+
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
61
69
|
end
|
62
70
|
|
63
|
-
# @
|
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.
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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: #{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
|
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 [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
|
-
|
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
|
161
|
+
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
125
162
|
# @return [Range<ULID>]
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
169
|
+
case begin_element
|
131
170
|
when Time
|
132
|
-
begin_ulid = min(moment:
|
171
|
+
begin_ulid = min(moment: begin_element)
|
133
172
|
when nil
|
134
|
-
begin_ulid =
|
173
|
+
begin_ulid = MIN
|
174
|
+
when self
|
175
|
+
begin_ulid = begin_element
|
135
176
|
else
|
136
|
-
raise ArgumentError,
|
177
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
|
137
178
|
end
|
138
179
|
|
139
|
-
case
|
180
|
+
case end_element
|
140
181
|
when Time
|
141
182
|
if exclude_end
|
142
|
-
end_ulid = min(moment:
|
183
|
+
end_ulid = min(moment: end_element)
|
143
184
|
else
|
144
|
-
end_ulid = max(moment:
|
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 =
|
189
|
+
end_ulid = MAX
|
149
190
|
exclude_end = false
|
191
|
+
when self
|
192
|
+
end_ulid = end_element
|
150
193
|
else
|
151
|
-
raise ArgumentError,
|
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
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
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
|
-
|
213
|
-
|
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 [
|
221
|
-
# @
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
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 ||=
|
303
|
+
@string ||= CrockfordBase32.encode(to_i).freeze
|
266
304
|
end
|
267
305
|
|
268
306
|
# @return [Integer]
|
269
307
|
def to_i
|
270
|
-
@integer ||=
|
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
|
-
|
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
|
-
|
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
|
-
|
336
|
+
to_i == other.to_i
|
295
337
|
when String
|
296
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
371
|
+
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
|
330
372
|
end
|
331
373
|
|
332
374
|
# @return [String]
|
333
375
|
def timestamp
|
334
|
-
@timestamp ||=
|
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 ||=
|
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
|
-
# @
|
348
|
-
|
349
|
-
|
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
|
354
|
-
|
355
|
-
|
356
|
-
|
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 :
|
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
|
-
|
363
|
-
|
364
|
-
|
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
|
-
#
|
370
|
-
|
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 [
|
382
|
-
def
|
383
|
-
|
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
|
-
|
445
|
+
MIN = parse('00000000000000000000000000').freeze
|
446
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
392
447
|
|
393
|
-
private_constant :
|
448
|
+
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
|
394
449
|
end
|