ruby-ulid 0.0.13 → 0.0.18
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 +184 -33
- data/lib/ulid.rb +205 -107
- data/lib/ulid/monotonic_generator.rb +1 -1
- data/lib/ulid/uuid.rb +37 -0
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +27 -16
- metadata +8 -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: 8b1641b0894c0140c3a9c6f59fa8abcfca386362ad1aaa92fe196f3bf5f75cf1
|
4
|
+
data.tar.gz: 932fa04dceb504330289d4236f9670fecd702989504bdd402873d60c0bb1c0e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f073c7b240ef96b511c84c6bc5649c483fc97c62a3892f58531b6f36e8235e49dcd865433fe522eec48a3532c98c4deec8713f453a4b2a117157788eabddaf6b
|
7
|
+
data.tar.gz: a2486bbefd62d0d019c15044c9d67b6aabc33e370f4ee88239c1e2b33a40738c736fcc9221dd3ce457eabf83a3580be5dc501fef0c359345a453331bdd62d139
|
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.18'
|
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
|
147
|
+
```
|
148
|
+
|
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)
|
129
159
|
```
|
130
160
|
|
131
|
-
When
|
132
|
-
So this gem provides `ULID.range` to generate `Range[ULID]` from
|
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)]
|
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
|
|
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,106 @@ 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 ignoring the generating time
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
278
|
+
ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
|
279
|
+
ULID.sample(0) #=> []
|
280
|
+
ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
|
281
|
+
ULID.sample(5)
|
282
|
+
#=>
|
283
|
+
#[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
|
284
|
+
# ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
|
285
|
+
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0),
|
286
|
+
# ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
|
287
|
+
# ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
|
288
|
+
```
|
289
|
+
|
290
|
+
### UUIDv4 converter for migration use-cases
|
291
|
+
|
292
|
+
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
293
|
+
The imported timestamp is meaningless. So ULID's benefit will lost.
|
213
294
|
|
214
295
|
```ruby
|
215
|
-
|
216
|
-
|
296
|
+
# Currently experimental feature, so needed to load the extension.
|
297
|
+
require 'ulid/uuid'
|
298
|
+
|
299
|
+
# Basically reversible
|
300
|
+
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
301
|
+
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
302
|
+
|
303
|
+
uuid_v4s = 10000.times.map { SecureRandom.uuid }
|
304
|
+
uuid_v4s.uniq.size == 10000 #=> Probably `true`
|
305
|
+
|
306
|
+
ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
|
307
|
+
ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
|
308
|
+
|
309
|
+
# NOTE: Some boundary values are not reversible. See below.
|
310
|
+
|
311
|
+
ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
|
312
|
+
ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
313
|
+
|
314
|
+
# These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
|
315
|
+
reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
|
316
|
+
reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
|
317
|
+
|
318
|
+
# But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
|
319
|
+
ULID.min == reversed_min #=> false
|
320
|
+
ULID.max == reversed_max #=> false
|
217
321
|
```
|
218
322
|
|
323
|
+
## How to migrate from other gems
|
324
|
+
|
325
|
+
As far as I know, major prior arts are below
|
326
|
+
|
327
|
+
### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
|
328
|
+
|
329
|
+
It is just providing basic `String` generator only.
|
330
|
+
So you can replace the code as below
|
331
|
+
|
332
|
+
```diff
|
333
|
+
-ULID.generate
|
334
|
+
+ULID.generate.to_s
|
335
|
+
```
|
336
|
+
|
337
|
+
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.
|
338
|
+
|
339
|
+
1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
|
340
|
+
1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
|
341
|
+
1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
|
342
|
+
|
343
|
+
### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
|
344
|
+
|
345
|
+
It is providing basic generator(except monotonic generator) and parser.
|
346
|
+
Major methods can be replaced as below.
|
347
|
+
|
348
|
+
```diff
|
349
|
+
-ULID.generate
|
350
|
+
+ULID.generate.to_s
|
351
|
+
-ULID.at(time)
|
352
|
+
+ULID.at(time).to_s
|
353
|
+
-ULID.time(string)
|
354
|
+
+ULID.parse(string).to_time
|
355
|
+
-ULID.min_ulid_at(time)
|
356
|
+
+ULID.min(moment: time).to_s
|
357
|
+
-ULID.max_ulid_at(time)
|
358
|
+
+ULID.max(moment: time).to_s
|
359
|
+
```
|
360
|
+
|
361
|
+
NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
|
362
|
+
|
363
|
+
1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
|
364
|
+
1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
|
365
|
+
1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
|
366
|
+
|
219
367
|
## References
|
220
368
|
|
221
369
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
222
370
|
- [API documents](https://kachick.github.io/ruby-ulid/)
|
223
371
|
- [ulid/spec](https://github.com/ulid/spec)
|
224
|
-
|
225
|
-
|
372
|
+
|
373
|
+
## Note
|
374
|
+
|
375
|
+
- 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)
|
376
|
+
- 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
|
@@ -16,31 +15,46 @@ class ULID
|
|
16
15
|
class Error < StandardError; end
|
17
16
|
class OverflowError < Error; end
|
18
17
|
class ParserError < Error; end
|
18
|
+
class SetupError < ScriptError; end
|
19
19
|
|
20
|
+
# `Subset` of Crockford's Base32. Just excluded I, L, O, U, -.
|
21
|
+
# refs:
|
22
|
+
# * https://www.crockford.com/base32.html
|
23
|
+
# * https://github.com/ulid/spec/pull/57
|
24
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
20
25
|
encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
21
|
-
|
22
|
-
# @see https://www.crockford.com/base32.html
|
23
|
-
ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
|
26
|
+
encoding_chars = encoding_string.chars.map(&:freeze).freeze
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
TIMESTAMP_ENCODED_LENGTH = 10
|
29
|
+
RANDOMNESS_ENCODED_LENGTH = 16
|
30
|
+
ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
|
28
31
|
TIMESTAMP_OCTETS_LENGTH = 6
|
29
32
|
RANDOMNESS_OCTETS_LENGTH = 10
|
30
33
|
OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
|
31
34
|
MAX_MILLISECONDS = 281474976710655
|
32
35
|
MAX_ENTROPY = 1208925819614629174706175
|
33
36
|
MAX_INTEGER = 340282366920938463463374607431768211455
|
34
|
-
PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{
|
37
|
+
PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
|
35
38
|
STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
|
36
39
|
|
37
|
-
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
38
|
-
UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i
|
39
|
-
|
40
40
|
# Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
|
41
41
|
# @see https://bugs.ruby-lang.org/issues/15958
|
42
42
|
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
43
43
|
|
44
|
+
UNDEFINED = BasicObject.new
|
45
|
+
# @return [String]
|
46
|
+
def UNDEFINED.to_s
|
47
|
+
'ULID::UNDEFINED'
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String]
|
51
|
+
def UNDEFINED.inspect
|
52
|
+
to_s
|
53
|
+
end
|
54
|
+
Kernel.instance_method(:freeze).bind(UNDEFINED).call
|
55
|
+
|
56
|
+
private_class_method :new
|
57
|
+
|
44
58
|
# @param [Integer, Time] moment
|
45
59
|
# @param [Integer] entropy
|
46
60
|
# @return [ULID]
|
@@ -48,30 +62,49 @@ class ULID
|
|
48
62
|
new milliseconds: milliseconds_from_moment(moment), entropy: entropy
|
49
63
|
end
|
50
64
|
|
51
|
-
#
|
65
|
+
# Short hand of `ULID.generate(moment: time)`
|
66
|
+
# @param [Time] time
|
52
67
|
# @return [ULID]
|
53
|
-
def self.
|
54
|
-
|
68
|
+
def self.at(time)
|
69
|
+
raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
|
70
|
+
new milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy
|
55
71
|
end
|
56
72
|
|
57
73
|
# @param [Integer, Time] moment
|
58
74
|
# @return [ULID]
|
59
|
-
def self.
|
60
|
-
generate(moment: moment, entropy:
|
75
|
+
def self.min(moment: 0)
|
76
|
+
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
61
77
|
end
|
62
78
|
|
63
|
-
# @
|
64
|
-
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
79
|
+
# @param [Integer, Time] moment
|
65
80
|
# @return [ULID]
|
66
|
-
def self.
|
67
|
-
|
68
|
-
|
69
|
-
|
81
|
+
def self.max(moment: MAX_MILLISECONDS)
|
82
|
+
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [Integer] number
|
86
|
+
# @return [ULID, Array<ULID>]
|
87
|
+
# @raise [ArgumentError] if the given number is lager than ULID spec limits or given negative number
|
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(number=UNDEFINED)
|
93
|
+
if UNDEFINED.equal?(number)
|
94
|
+
from_integer(SecureRandom.random_number(MAX_INTEGER))
|
70
95
|
else
|
71
|
-
|
72
|
-
|
96
|
+
begin
|
97
|
+
int = number.to_int
|
98
|
+
rescue
|
99
|
+
# Can not use `number.to_s` and `number.inspect` for considering BasicObject here
|
100
|
+
raise TypeError, 'accepts no argument or integer only'
|
101
|
+
end
|
73
102
|
|
74
|
-
|
103
|
+
if int > MAX_INTEGER || int.negative?
|
104
|
+
raise ArgumentError, "given number is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
|
105
|
+
end
|
106
|
+
int.times.map { from_integer(SecureRandom.random_number(MAX_INTEGER)) }
|
107
|
+
end
|
75
108
|
end
|
76
109
|
|
77
110
|
# @param [String, #to_str] string
|
@@ -87,53 +120,39 @@ class ULID
|
|
87
120
|
self
|
88
121
|
end
|
89
122
|
|
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
123
|
# @param [Integer, #to_int] integer
|
106
124
|
# @return [ULID]
|
107
125
|
# @raise [OverflowError] if the given integer is larger than the ULID limit
|
108
126
|
# @raise [ArgumentError] if the given integer is negative number
|
109
|
-
# @todo Need optimized for performance
|
110
127
|
def self.from_integer(integer)
|
111
128
|
integer = integer.to_int
|
112
129
|
raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
|
113
130
|
raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
|
114
131
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
132
|
+
n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
|
133
|
+
n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
|
134
|
+
n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
|
135
|
+
|
136
|
+
milliseconds = n32encoded_timestamp.to_i(32)
|
137
|
+
entropy = n32encoded_randomness.to_i(32)
|
120
138
|
|
121
|
-
new milliseconds: milliseconds, entropy: entropy
|
139
|
+
new milliseconds: milliseconds, entropy: entropy, integer: integer
|
122
140
|
end
|
123
141
|
|
124
|
-
# @param [Range<Time>] time_range
|
142
|
+
# @param [Range<Time>, Range<nil>] time_range
|
125
143
|
# @return [Range<ULID>]
|
144
|
+
# @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
|
126
145
|
def self.range(time_range)
|
127
|
-
raise
|
146
|
+
raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
|
128
147
|
begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
|
129
148
|
|
130
149
|
case begin_time
|
131
150
|
when Time
|
132
151
|
begin_ulid = min(moment: begin_time)
|
133
152
|
when nil
|
134
|
-
begin_ulid =
|
153
|
+
begin_ulid = MIN
|
135
154
|
else
|
136
|
-
raise
|
155
|
+
raise argument_error_for_range_building(time_range)
|
137
156
|
end
|
138
157
|
|
139
158
|
case end_time
|
@@ -145,12 +164,15 @@ class ULID
|
|
145
164
|
end
|
146
165
|
when nil
|
147
166
|
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
148
|
-
end_ulid =
|
167
|
+
end_ulid = MAX
|
149
168
|
exclude_end = false
|
150
169
|
else
|
151
|
-
raise
|
170
|
+
raise argument_error_for_range_building(time_range)
|
152
171
|
end
|
153
172
|
|
173
|
+
begin_ulid.freeze
|
174
|
+
end_ulid.freeze
|
175
|
+
|
154
176
|
Range.new(begin_ulid, end_ulid, exclude_end)
|
155
177
|
end
|
156
178
|
|
@@ -164,47 +186,88 @@ class ULID
|
|
164
186
|
end
|
165
187
|
end
|
166
188
|
|
189
|
+
# @api private
|
167
190
|
# @return [Integer]
|
168
191
|
def self.current_milliseconds
|
169
192
|
milliseconds_from_time(Time.now)
|
170
193
|
end
|
171
194
|
|
195
|
+
# @api private
|
172
196
|
# @param [Time] time
|
173
197
|
# @return [Integer]
|
174
198
|
def self.milliseconds_from_time(time)
|
175
199
|
(time.to_r * 1000).to_i
|
176
200
|
end
|
177
201
|
|
202
|
+
# @api private
|
178
203
|
# @param [Time, Integer] moment
|
179
204
|
# @return [Integer]
|
180
205
|
def self.milliseconds_from_moment(moment)
|
181
206
|
moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
|
182
207
|
end
|
183
208
|
|
209
|
+
# @api private
|
184
210
|
# @return [Integer]
|
185
211
|
def self.reasonable_entropy
|
186
212
|
SecureRandom.random_number(MAX_ENTROPY)
|
187
213
|
end
|
188
214
|
|
215
|
+
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
216
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
217
|
+
|
218
|
+
n32_char_by_number = {}
|
219
|
+
n32_chars.each_with_index do |char, index|
|
220
|
+
n32_char_by_number[index] = char
|
221
|
+
end
|
222
|
+
n32_char_by_number.freeze
|
223
|
+
|
224
|
+
# Currently supporting only for `subset for actual use-case`
|
225
|
+
# See below
|
226
|
+
# * https://github.com/ulid/spec/pull/57
|
227
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
228
|
+
# * https://github.com/kachick/ruby-ulid/issues/78
|
229
|
+
crockford_base32_mappings = {
|
230
|
+
'J' => 18,
|
231
|
+
'K' => 19,
|
232
|
+
'M' => 20,
|
233
|
+
'N' => 21,
|
234
|
+
'P' => 22,
|
235
|
+
'Q' => 23,
|
236
|
+
'R' => 24,
|
237
|
+
'S' => 25,
|
238
|
+
'T' => 26,
|
239
|
+
'V' => 27,
|
240
|
+
'W' => 28,
|
241
|
+
'X' => 29,
|
242
|
+
'Y' => 30,
|
243
|
+
'Z' => 31
|
244
|
+
}.freeze
|
245
|
+
|
246
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR = encoding_chars.each_with_object({}) do |encoding_char, map|
|
247
|
+
if n = crockford_base32_mappings[encoding_char]
|
248
|
+
char_32 = n32_char_by_number.fetch(n)
|
249
|
+
map[encoding_char] = char_32
|
250
|
+
end
|
251
|
+
end.freeze
|
252
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
|
253
|
+
CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
|
254
|
+
|
255
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
|
256
|
+
N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
|
257
|
+
|
189
258
|
# @param [String, #to_str] string
|
190
259
|
# @return [ULID]
|
191
260
|
# @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
261
|
def self.parse(string)
|
194
262
|
begin
|
195
263
|
string = string.to_str
|
196
|
-
|
197
|
-
raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
|
198
|
-
end
|
199
|
-
timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
|
200
|
-
randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
201
|
-
milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
|
202
|
-
entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
|
264
|
+
raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
|
203
265
|
rescue => err
|
204
266
|
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
205
267
|
end
|
206
|
-
|
207
|
-
|
268
|
+
|
269
|
+
n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
|
270
|
+
from_integer(n32encoded.to_i(32))
|
208
271
|
end
|
209
272
|
|
210
273
|
# @return [Boolean]
|
@@ -216,18 +279,6 @@ class ULID
|
|
216
279
|
true
|
217
280
|
end
|
218
281
|
|
219
|
-
# @api private
|
220
|
-
# @param [Integer] integer
|
221
|
-
# @param [Integer] length
|
222
|
-
# @return [Array<Integer>]
|
223
|
-
def self.octets_from_integer(integer, length:)
|
224
|
-
digits = integer.digits(256)
|
225
|
-
(length - digits.size).times do
|
226
|
-
digits.push 0
|
227
|
-
end
|
228
|
-
digits.reverse!
|
229
|
-
end
|
230
|
-
|
231
282
|
# @api private
|
232
283
|
# @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
|
233
284
|
# @param [Array<Integer>] reversed_digits
|
@@ -241,50 +292,73 @@ class ULID
|
|
241
292
|
num
|
242
293
|
end
|
243
294
|
|
295
|
+
# @api private
|
296
|
+
# @param [MonotonicGenerator] generator
|
297
|
+
# @return [ULID]
|
298
|
+
def self.from_monotonic_generator(generator)
|
299
|
+
raise ArgumentError, 'this method provided only for MonotonicGenerator' unless MonotonicGenerator === generator
|
300
|
+
new milliseconds: generator.latest_milliseconds, entropy: generator.latest_entropy
|
301
|
+
end
|
302
|
+
|
303
|
+
# @api private
|
304
|
+
# @return [ArgumentError]
|
305
|
+
private_class_method def self.argument_error_for_range_building(argument)
|
306
|
+
ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
|
307
|
+
end
|
308
|
+
|
244
309
|
attr_reader :milliseconds, :entropy
|
245
310
|
|
246
311
|
# @api private
|
247
312
|
# @param [Integer] milliseconds
|
248
313
|
# @param [Integer] entropy
|
314
|
+
# @param [Integer] integer
|
249
315
|
# @return [void]
|
250
316
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
251
317
|
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
252
|
-
def initialize(milliseconds:, entropy:)
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
318
|
+
def initialize(milliseconds:, entropy:, integer: UNDEFINED)
|
319
|
+
if UNDEFINED.equal?(integer)
|
320
|
+
milliseconds = milliseconds.to_int
|
321
|
+
entropy = entropy.to_int
|
322
|
+
|
323
|
+
raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
|
324
|
+
raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
|
325
|
+
raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
|
326
|
+
else
|
327
|
+
@integer = integer
|
328
|
+
end
|
258
329
|
|
259
330
|
@milliseconds = milliseconds
|
260
331
|
@entropy = entropy
|
261
332
|
end
|
262
333
|
|
263
334
|
# @return [String]
|
264
|
-
def
|
265
|
-
@string ||=
|
335
|
+
def to_s
|
336
|
+
@string ||= to_i.to_s(32).upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0').freeze
|
266
337
|
end
|
267
|
-
alias_method :to_s, :to_str
|
268
338
|
|
269
339
|
# @return [Integer]
|
270
340
|
def to_i
|
271
|
-
@integer ||=
|
341
|
+
@integer ||= begin
|
342
|
+
n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
343
|
+
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
344
|
+
(n32encoded_timestamp + n32encoded_randomness).to_i(32)
|
345
|
+
end
|
272
346
|
end
|
273
347
|
alias_method :hash, :to_i
|
274
348
|
|
275
349
|
# @return [Integer, nil]
|
276
350
|
def <=>(other)
|
277
|
-
|
351
|
+
(ULID === other) ? (to_i <=> other.to_i) : nil
|
278
352
|
end
|
279
353
|
|
280
354
|
# @return [String]
|
281
355
|
def inspect
|
282
|
-
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{
|
356
|
+
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
|
283
357
|
end
|
284
358
|
|
285
359
|
# @return [Boolean]
|
286
360
|
def eql?(other)
|
287
|
-
|
361
|
+
equal?(other) || (ULID === other && to_i == other.to_i)
|
288
362
|
end
|
289
363
|
alias_method :==, :eql?
|
290
364
|
|
@@ -317,37 +391,49 @@ class ULID
|
|
317
391
|
|
318
392
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
319
393
|
def octets
|
320
|
-
@octets ||= (
|
394
|
+
@octets ||= octets_from_integer(to_i).freeze
|
321
395
|
end
|
322
396
|
|
323
397
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
324
398
|
def timestamp_octets
|
325
|
-
@timestamp_octets ||=
|
399
|
+
@timestamp_octets ||= octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
|
326
400
|
end
|
327
401
|
|
328
402
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
329
403
|
def randomness_octets
|
330
|
-
@randomness_octets ||=
|
404
|
+
@randomness_octets ||= octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
|
331
405
|
end
|
332
406
|
|
333
407
|
# @return [String]
|
334
408
|
def timestamp
|
335
|
-
@timestamp ||=
|
409
|
+
@timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
|
336
410
|
end
|
337
411
|
|
338
412
|
# @return [String]
|
339
413
|
def randomness
|
340
|
-
@randomness ||=
|
414
|
+
@randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
|
415
|
+
end
|
416
|
+
|
417
|
+
# @note Providing for rough operations. The keys and values is not fixed.
|
418
|
+
# @return [Hash{Symbol => Regexp, String}]
|
419
|
+
def patterns
|
420
|
+
named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
421
|
+
{
|
422
|
+
named_captures: named_captures,
|
423
|
+
strict_named_captures: /\A#{named_captures.source}\z/i.freeze
|
424
|
+
}
|
341
425
|
end
|
342
426
|
|
427
|
+
# @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
|
343
428
|
# @return [Regexp]
|
344
429
|
def pattern
|
345
|
-
|
430
|
+
patterns.fetch(:named_captures)
|
346
431
|
end
|
347
432
|
|
433
|
+
# @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
|
348
434
|
# @return [Regexp]
|
349
435
|
def strict_pattern
|
350
|
-
|
436
|
+
patterns.fetch(:strict_named_captures)
|
351
437
|
end
|
352
438
|
|
353
439
|
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
@@ -367,21 +453,32 @@ class ULID
|
|
367
453
|
|
368
454
|
# @return [self]
|
369
455
|
def freeze
|
370
|
-
#
|
371
|
-
|
372
|
-
octets
|
373
|
-
to_i
|
374
|
-
succ
|
375
|
-
pred
|
376
|
-
strict_pattern
|
456
|
+
# Need to cache before freezing, because frozen objects can't assign instance variables
|
457
|
+
cache_all_instance_variables
|
377
458
|
super
|
378
459
|
end
|
379
460
|
|
380
461
|
private
|
381
462
|
|
382
|
-
# @
|
383
|
-
|
384
|
-
|
463
|
+
# @param [Integer] integer
|
464
|
+
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
465
|
+
def octets_from_integer(integer)
|
466
|
+
digits = integer.digits(256)
|
467
|
+
(OCTETS_LENGTH - digits.size).times do
|
468
|
+
digits.push 0
|
469
|
+
end
|
470
|
+
digits.reverse!
|
471
|
+
end
|
472
|
+
|
473
|
+
# @return [void]
|
474
|
+
def cache_all_instance_variables
|
475
|
+
inspect
|
476
|
+
octets
|
477
|
+
to_i
|
478
|
+
succ
|
479
|
+
pred
|
480
|
+
timestamp
|
481
|
+
randomness
|
385
482
|
end
|
386
483
|
end
|
387
484
|
|
@@ -389,7 +486,8 @@ require_relative 'ulid/version'
|
|
389
486
|
require_relative 'ulid/monotonic_generator'
|
390
487
|
|
391
488
|
class ULID
|
392
|
-
|
489
|
+
MIN = parse('00000000000000000000000000').freeze
|
490
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
393
491
|
|
394
|
-
private_constant :
|
492
|
+
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :CROCKFORD_BASE32_CHAR_PATTERN, :N32_CHAR_BY_CROCKFORD_BASE32_CHAR, :CROCKFORD_BASE32_CHAR_BY_N32_CHAR, :N32_CHAR_PATTERN, :UNDEFINED
|
395
493
|
end
|
data/lib/ulid/uuid.rb
ADDED
@@ -0,0 +1,37 @@
|
|
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
|
+
begin
|
19
|
+
uuid = uuid.to_str
|
20
|
+
prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
|
21
|
+
raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
|
22
|
+
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
23
|
+
from_integer(normalized.to_i(16))
|
24
|
+
rescue => err
|
25
|
+
raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
def to_uuidv4
|
31
|
+
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
32
|
+
array = octets.pack('C*').unpack('NnnnnN')
|
33
|
+
array[2] = (array[2] & 0x0fff) | 0x4000
|
34
|
+
array[3] = (array[3] & 0x3fff) | 0x8000
|
35
|
+
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
36
|
+
end
|
37
|
+
end
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
# Classes
|
2
2
|
class ULID
|
3
3
|
VERSION: String
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
ENCODED_ID_LENGTH: 26
|
4
|
+
TIMESTAMP_ENCODED_LENGTH: 10
|
5
|
+
RANDOMNESS_ENCODED_LENGTH: 16
|
6
|
+
ENCODED_LENGTH: 26
|
8
7
|
TIMESTAMP_OCTETS_LENGTH: 6
|
9
8
|
RANDOMNESS_OCTETS_LENGTH: 10
|
10
9
|
OCTETS_LENGTH: 16
|
@@ -15,9 +14,16 @@ class ULID
|
|
15
14
|
PATTERN: Regexp
|
16
15
|
STRICT_PATTERN: Regexp
|
17
16
|
UUIDV4_PATTERN: Regexp
|
18
|
-
|
17
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
|
18
|
+
CROCKFORD_BASE32_CHAR_PATTERN: Regexp
|
19
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
|
20
|
+
N32_CHAR_PATTERN: Regexp
|
21
|
+
MIN: ULID
|
22
|
+
MAX: ULID
|
23
|
+
UNDEFINED: BasicObject
|
19
24
|
include Comparable
|
20
25
|
|
26
|
+
# The `moment` is a `Time` or `Intger of the milliseconds`
|
21
27
|
type moment = Time | Integer
|
22
28
|
|
23
29
|
class Error < StandardError
|
@@ -29,6 +35,9 @@ class ULID
|
|
29
35
|
class ParserError < Error
|
30
36
|
end
|
31
37
|
|
38
|
+
class SetupError < ScriptError
|
39
|
+
end
|
40
|
+
|
32
41
|
class MonotonicGenerator
|
33
42
|
attr_accessor latest_milliseconds: Integer
|
34
43
|
attr_accessor latest_entropy: Integer
|
@@ -54,16 +63,13 @@ class ULID
|
|
54
63
|
@inspect: String?
|
55
64
|
@time: Time?
|
56
65
|
@next: ULID?
|
57
|
-
@pattern: Regexp?
|
58
|
-
@strict_pattern: Regexp?
|
59
|
-
@matchdata: MatchData?
|
60
66
|
|
61
67
|
def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
|
62
|
-
def self.
|
68
|
+
def self.at: (Time time) -> ULID
|
63
69
|
def self.current_milliseconds: -> Integer
|
64
70
|
def self.milliseconds_from_time: (Time time) -> Integer
|
65
71
|
def self.milliseconds_from_moment: (moment moment) -> Integer
|
66
|
-
def self.range: (Range[Time] time_range) -> Range[ULID]
|
72
|
+
def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
|
67
73
|
def self.floor: (Time time) -> Time
|
68
74
|
def self.reasonable_entropy: -> Integer
|
69
75
|
def self.parse: (String string) -> ULID
|
@@ -71,20 +77,22 @@ class ULID
|
|
71
77
|
def self.from_integer: (Integer integer) -> ULID
|
72
78
|
def self.min: (?moment: moment) -> ULID
|
73
79
|
def self.max: (?moment: moment) -> ULID
|
80
|
+
def self.sample: -> ULID
|
81
|
+
| (Integer number) -> Array[ULID]
|
74
82
|
def self.valid?: (untyped string) -> bool
|
75
83
|
def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
|
76
84
|
| (String string) { (ULID ulid) -> void } -> singleton(ULID)
|
77
|
-
def self.octets_from_integer: (Integer integer
|
85
|
+
def self.octets_from_integer: (Integer integer) -> octets
|
78
86
|
def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
|
87
|
+
def self.from_monotonic_generator: (MonotonicGenerator generator) -> ULID
|
79
88
|
attr_reader milliseconds: Integer
|
80
89
|
attr_reader entropy: Integer
|
81
|
-
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
82
|
-
def
|
83
|
-
alias to_s to_str
|
90
|
+
def initialize: (milliseconds: Integer, entropy: Integer, ?integer: Integer) -> void
|
91
|
+
def to_s: -> String
|
84
92
|
def to_i: -> Integer
|
85
93
|
alias hash to_i
|
86
94
|
def <=>: (ULID other) -> Integer
|
87
|
-
| (untyped other) ->
|
95
|
+
| (untyped other) -> nil
|
88
96
|
def inspect: -> String
|
89
97
|
def eql?: (untyped other) -> bool
|
90
98
|
alias == eql?
|
@@ -92,16 +100,19 @@ class ULID
|
|
92
100
|
def to_time: -> Time
|
93
101
|
def timestamp: -> String
|
94
102
|
def randomness: -> String
|
103
|
+
def patterns: -> Hash[Symbol, Regexp | String]
|
95
104
|
def pattern: -> Regexp
|
96
105
|
def strict_pattern: -> Regexp
|
97
106
|
def octets: -> octets
|
98
107
|
def timestamp_octets: -> timestamp_octets
|
99
108
|
def randomness_octets: -> randomness_octets
|
109
|
+
def to_uuidv4: -> String
|
100
110
|
def next: -> ULID?
|
101
111
|
alias succ next
|
102
112
|
def pred: -> ULID?
|
103
113
|
def freeze: -> self
|
104
114
|
|
105
115
|
private
|
106
|
-
def
|
116
|
+
def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
|
117
|
+
def cache_all_instance_variables: -> void
|
107
118
|
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.0.18
|
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-07 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: []
|
@@ -96,9 +76,9 @@ extra_rdoc_files: []
|
|
96
76
|
files:
|
97
77
|
- LICENSE
|
98
78
|
- README.md
|
99
|
-
- Steepfile
|
100
79
|
- lib/ulid.rb
|
101
80
|
- lib/ulid/monotonic_generator.rb
|
81
|
+
- lib/ulid/uuid.rb
|
102
82
|
- lib/ulid/version.rb
|
103
83
|
- sig/ulid.rbs
|
104
84
|
homepage: https://github.com/kachick/ruby-ulid
|
@@ -123,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
103
|
- !ruby/object:Gem::Version
|
124
104
|
version: '0'
|
125
105
|
requirements: []
|
126
|
-
rubygems_version: 3.
|
106
|
+
rubygems_version: 3.2.15
|
127
107
|
signing_key:
|
128
108
|
specification_version: 4
|
129
109
|
summary: A handy ULID library
|