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