ruby-ulid 0.0.18 → 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 +37 -2
- data/lib/ulid.rb +133 -177
- data/lib/ulid/crockford_base32.rb +68 -0
- data/lib/ulid/monotonic_generator.rb +9 -7
- data/lib/ulid/uuid.rb +9 -8
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +21 -21
- metadata +3 -2
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
@@ -49,7 +49,7 @@ Should be installed!
|
|
49
49
|
Add this line to your application/library's `Gemfile` is needed in basic use-case
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
gem 'ruby-ulid', '0.0.
|
52
|
+
gem 'ruby-ulid', '0.0.19'
|
53
53
|
```
|
54
54
|
|
55
55
|
### Generator and Parser
|
@@ -271,7 +271,9 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
|
|
271
271
|
ULID.parse('00000000000000000000000000').pred #=> nil
|
272
272
|
```
|
273
273
|
|
274
|
-
`ULID.sample` returns random ULIDs
|
274
|
+
`ULID.sample` returns random ULIDs.
|
275
|
+
|
276
|
+
Basically ignores generating time.
|
275
277
|
|
276
278
|
```ruby
|
277
279
|
ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
@@ -287,6 +289,39 @@ ULID.sample(5)
|
|
287
289
|
# ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
|
288
290
|
```
|
289
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
|
+
|
290
325
|
### UUIDv4 converter for migration use-cases
|
291
326
|
|
292
327
|
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
data/lib/ulid.rb
CHANGED
@@ -15,15 +15,11 @@ class ULID
|
|
15
15
|
class Error < StandardError; end
|
16
16
|
class OverflowError < Error; end
|
17
17
|
class ParserError < Error; end
|
18
|
-
class SetupError < ScriptError; end
|
19
18
|
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
# * https://github.com/kachick/ruby-ulid/issues/57
|
25
|
-
encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
26
|
-
encoding_chars = encoding_string.chars.map(&:freeze).freeze
|
19
|
+
# Excluded I, L, O, U, -.
|
20
|
+
# This is the encoding patterns.
|
21
|
+
# The decoding issue is written in ULID::CrockfordBase32
|
22
|
+
CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
27
23
|
|
28
24
|
TIMESTAMP_ENCODED_LENGTH = 10
|
29
25
|
RANDOMNESS_ENCODED_LENGTH = 16
|
@@ -34,25 +30,21 @@ class ULID
|
|
34
30
|
MAX_MILLISECONDS = 281474976710655
|
35
31
|
MAX_ENTROPY = 1208925819614629174706175
|
36
32
|
MAX_INTEGER = 340282366920938463463374607431768211455
|
37
|
-
|
38
|
-
|
33
|
+
|
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
|
|
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
48
|
private_class_method :new
|
57
49
|
|
58
50
|
# @param [Integer, Time] moment
|
@@ -82,28 +74,54 @@ class ULID
|
|
82
74
|
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
83
75
|
end
|
84
76
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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
88
|
# @note Major difference of `Array#sample` interface is below
|
89
89
|
# * Do not ensure the uniqueness
|
90
90
|
# * Do not take random generator for the arguments
|
91
91
|
# * Raising error instead of truncating elements for the given number
|
92
|
-
def self.sample(
|
93
|
-
if
|
94
|
-
|
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
|
+
}
|
95
103
|
else
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
104
|
+
RANDOM_INTEGER_GENERATOR
|
105
|
+
end
|
106
|
+
|
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}"
|
101
116
|
end
|
102
117
|
|
103
|
-
if
|
104
|
-
raise ArgumentError, "given number is larger than
|
118
|
+
if period && (number > possibilities)
|
119
|
+
raise ArgumentError, "given number #{number} is larger than given possibilities #{possibilities}"
|
105
120
|
end
|
106
|
-
|
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)"
|
107
125
|
end
|
108
126
|
end
|
109
127
|
|
@@ -112,10 +130,11 @@ class ULID
|
|
112
130
|
# @yieldparam [ULID] ulid
|
113
131
|
# @yieldreturn [self]
|
114
132
|
def self.scan(string)
|
115
|
-
string = string
|
133
|
+
string = String.try_convert(string)
|
134
|
+
raise ArgumentError, 'ULID.scan takes only strings' unless string
|
116
135
|
return to_enum(__callee__, string) unless block_given?
|
117
|
-
string.scan(
|
118
|
-
yield parse(
|
136
|
+
string.scan(SCANNING_PATTERN) do |matched|
|
137
|
+
yield parse(matched)
|
119
138
|
end
|
120
139
|
self
|
121
140
|
end
|
@@ -139,35 +158,40 @@ class ULID
|
|
139
158
|
new milliseconds: milliseconds, entropy: entropy, integer: integer
|
140
159
|
end
|
141
160
|
|
142
|
-
# @param [Range<Time>, Range<nil
|
161
|
+
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
143
162
|
# @return [Range<ULID>]
|
144
|
-
# @raise [ArgumentError] if the given
|
145
|
-
def self.range(
|
146
|
-
raise
|
147
|
-
|
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
|
148
168
|
|
149
|
-
case
|
169
|
+
case begin_element
|
150
170
|
when Time
|
151
|
-
begin_ulid = min(moment:
|
171
|
+
begin_ulid = min(moment: begin_element)
|
152
172
|
when nil
|
153
173
|
begin_ulid = MIN
|
174
|
+
when self
|
175
|
+
begin_ulid = begin_element
|
154
176
|
else
|
155
|
-
raise
|
177
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
|
156
178
|
end
|
157
179
|
|
158
|
-
case
|
180
|
+
case end_element
|
159
181
|
when Time
|
160
182
|
if exclude_end
|
161
|
-
end_ulid = min(moment:
|
183
|
+
end_ulid = min(moment: end_element)
|
162
184
|
else
|
163
|
-
end_ulid = max(moment:
|
185
|
+
end_ulid = max(moment: end_element)
|
164
186
|
end
|
165
187
|
when nil
|
166
188
|
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
167
189
|
end_ulid = MAX
|
168
190
|
exclude_end = false
|
191
|
+
when self
|
192
|
+
end_ulid = end_element
|
169
193
|
else
|
170
|
-
raise
|
194
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
|
171
195
|
end
|
172
196
|
|
173
197
|
begin_ulid.freeze
|
@@ -179,6 +203,8 @@ class ULID
|
|
179
203
|
# @param [Time] time
|
180
204
|
# @return [Time]
|
181
205
|
def self.floor(time)
|
206
|
+
raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
|
207
|
+
|
182
208
|
if RUBY_VERSION >= '2.7'
|
183
209
|
time.floor(3)
|
184
210
|
else
|
@@ -192,10 +218,9 @@ class ULID
|
|
192
218
|
milliseconds_from_time(Time.now)
|
193
219
|
end
|
194
220
|
|
195
|
-
# @api private
|
196
221
|
# @param [Time] time
|
197
222
|
# @return [Integer]
|
198
|
-
def self.milliseconds_from_time(time)
|
223
|
+
private_class_method def self.milliseconds_from_time(time)
|
199
224
|
(time.to_r * 1000).to_i
|
200
225
|
end
|
201
226
|
|
@@ -203,7 +228,14 @@ class ULID
|
|
203
228
|
# @param [Time, Integer] moment
|
204
229
|
# @return [Integer]
|
205
230
|
def self.milliseconds_from_moment(moment)
|
206
|
-
|
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
|
207
239
|
end
|
208
240
|
|
209
241
|
# @api private
|
@@ -212,84 +244,25 @@ class ULID
|
|
212
244
|
SecureRandom.random_number(MAX_ENTROPY)
|
213
245
|
end
|
214
246
|
|
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
|
-
|
258
247
|
# @param [String, #to_str] string
|
259
248
|
# @return [ULID]
|
260
249
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
261
250
|
def self.parse(string)
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
raise ParserError, "
|
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}`"
|
267
256
|
end
|
268
257
|
|
269
|
-
|
270
|
-
from_integer(n32encoded.to_i(32))
|
258
|
+
from_integer(CrockfordBase32.decode(string))
|
271
259
|
end
|
272
260
|
|
261
|
+
# @param [String, #to_str] string
|
273
262
|
# @return [Boolean]
|
274
263
|
def self.valid?(string)
|
275
|
-
|
276
|
-
|
277
|
-
false
|
278
|
-
else
|
279
|
-
true
|
280
|
-
end
|
281
|
-
|
282
|
-
# @api private
|
283
|
-
# @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
|
284
|
-
# @param [Array<Integer>] reversed_digits
|
285
|
-
# @return [Integer]
|
286
|
-
def self.inverse_of_digits(reversed_digits)
|
287
|
-
base = 256
|
288
|
-
num = 0
|
289
|
-
reversed_digits.each do |digit|
|
290
|
-
num = (num * base) + digit
|
291
|
-
end
|
292
|
-
num
|
264
|
+
string = String.try_convert(string)
|
265
|
+
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
|
293
266
|
end
|
294
267
|
|
295
268
|
# @api private
|
@@ -300,12 +273,6 @@ class ULID
|
|
300
273
|
new milliseconds: generator.latest_milliseconds, entropy: generator.latest_entropy
|
301
274
|
end
|
302
275
|
|
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
|
-
|
309
276
|
attr_reader :milliseconds, :entropy
|
310
277
|
|
311
278
|
# @api private
|
@@ -315,16 +282,16 @@ class ULID
|
|
315
282
|
# @return [void]
|
316
283
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
317
284
|
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
318
|
-
def initialize(milliseconds:, entropy:, integer:
|
319
|
-
if
|
285
|
+
def initialize(milliseconds:, entropy:, integer: nil)
|
286
|
+
if integer
|
287
|
+
@integer = integer
|
288
|
+
else
|
320
289
|
milliseconds = milliseconds.to_int
|
321
290
|
entropy = entropy.to_int
|
322
291
|
|
323
292
|
raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
|
324
293
|
raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
|
325
294
|
raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
|
326
|
-
else
|
327
|
-
@integer = integer
|
328
295
|
end
|
329
296
|
|
330
297
|
@milliseconds = milliseconds
|
@@ -333,7 +300,7 @@ class ULID
|
|
333
300
|
|
334
301
|
# @return [String]
|
335
302
|
def to_s
|
336
|
-
@string ||=
|
303
|
+
@string ||= CrockfordBase32.encode(to_i).freeze
|
337
304
|
end
|
338
305
|
|
339
306
|
# @return [Integer]
|
@@ -366,13 +333,9 @@ class ULID
|
|
366
333
|
def ===(other)
|
367
334
|
case other
|
368
335
|
when ULID
|
369
|
-
|
336
|
+
to_i == other.to_i
|
370
337
|
when String
|
371
|
-
|
372
|
-
self == self.class.parse(other)
|
373
|
-
rescue Exception
|
374
|
-
false
|
375
|
-
end
|
338
|
+
to_s == other.upcase
|
376
339
|
else
|
377
340
|
false
|
378
341
|
end
|
@@ -391,17 +354,21 @@ class ULID
|
|
391
354
|
|
392
355
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
393
356
|
def octets
|
394
|
-
|
357
|
+
digits = to_i.digits(256)
|
358
|
+
(OCTETS_LENGTH - digits.size).times do
|
359
|
+
digits.push 0
|
360
|
+
end
|
361
|
+
digits.reverse!
|
395
362
|
end
|
396
363
|
|
397
364
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
398
365
|
def timestamp_octets
|
399
|
-
|
366
|
+
octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
|
400
367
|
end
|
401
368
|
|
402
369
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
403
370
|
def randomness_octets
|
404
|
-
|
371
|
+
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
|
405
372
|
end
|
406
373
|
|
407
374
|
# @return [String]
|
@@ -424,31 +391,33 @@ class ULID
|
|
424
391
|
}
|
425
392
|
end
|
426
393
|
|
427
|
-
# @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
|
428
|
-
# @return [Regexp]
|
429
|
-
def pattern
|
430
|
-
patterns.fetch(:named_captures)
|
431
|
-
end
|
432
|
-
|
433
|
-
# @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
|
434
|
-
# @return [Regexp]
|
435
|
-
def strict_pattern
|
436
|
-
patterns.fetch(:strict_named_captures)
|
437
|
-
end
|
438
|
-
|
439
394
|
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
440
|
-
def
|
441
|
-
|
442
|
-
|
443
|
-
|
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
|
444
406
|
end
|
445
|
-
alias_method :
|
407
|
+
alias_method :next, :succ
|
446
408
|
|
447
409
|
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
448
410
|
def pred
|
449
|
-
|
450
|
-
|
451
|
-
|
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
|
452
421
|
end
|
453
422
|
|
454
423
|
# @return [self]
|
@@ -460,34 +429,21 @@ class ULID
|
|
460
429
|
|
461
430
|
private
|
462
431
|
|
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
432
|
# @return [void]
|
474
433
|
def cache_all_instance_variables
|
475
434
|
inspect
|
476
|
-
octets
|
477
|
-
to_i
|
478
|
-
succ
|
479
|
-
pred
|
480
435
|
timestamp
|
481
436
|
randomness
|
482
437
|
end
|
483
438
|
end
|
484
439
|
|
485
440
|
require_relative 'ulid/version'
|
441
|
+
require_relative 'ulid/crockford_base32'
|
486
442
|
require_relative 'ulid/monotonic_generator'
|
487
443
|
|
488
444
|
class ULID
|
489
445
|
MIN = parse('00000000000000000000000000').freeze
|
490
446
|
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
491
447
|
|
492
|
-
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :
|
448
|
+
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
|
493
449
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
4
|
+
|
5
|
+
class ULID
|
6
|
+
# Currently supporting only for `subset` for actual use-case`
|
7
|
+
# Original decoding spec allows other characters.
|
8
|
+
# But I think ULID should allow `subset` of Crockford's Base32.
|
9
|
+
# See below
|
10
|
+
# * https://github.com/ulid/spec/pull/57
|
11
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
12
|
+
# * https://github.com/kachick/ruby-ulid/issues/78
|
13
|
+
module CrockfordBase32
|
14
|
+
class SetupError < ScriptError; end
|
15
|
+
|
16
|
+
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
17
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
18
|
+
|
19
|
+
n32_char_by_number = {}
|
20
|
+
n32_chars.each_with_index do |char, index|
|
21
|
+
n32_char_by_number[index] = char
|
22
|
+
end
|
23
|
+
n32_char_by_number.freeze
|
24
|
+
|
25
|
+
crockford_base32_mappings = {
|
26
|
+
'J' => 18,
|
27
|
+
'K' => 19,
|
28
|
+
'M' => 20,
|
29
|
+
'N' => 21,
|
30
|
+
'P' => 22,
|
31
|
+
'Q' => 23,
|
32
|
+
'R' => 24,
|
33
|
+
'S' => 25,
|
34
|
+
'T' => 26,
|
35
|
+
'V' => 27,
|
36
|
+
'W' => 28,
|
37
|
+
'X' => 29,
|
38
|
+
'Y' => 30,
|
39
|
+
'Z' => 31
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR = CROCKFORD_BASE32_ENCODING_STRING.chars.map(&:freeze).each_with_object({}) do |encoding_char, map|
|
43
|
+
if n = crockford_base32_mappings[encoding_char]
|
44
|
+
char_32 = n32_char_by_number.fetch(n)
|
45
|
+
map[encoding_char] = char_32
|
46
|
+
end
|
47
|
+
end.freeze
|
48
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
|
49
|
+
CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
|
50
|
+
|
51
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
|
52
|
+
N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
|
53
|
+
|
54
|
+
# @param [String] string
|
55
|
+
# @return [Integer]
|
56
|
+
def self.decode(string)
|
57
|
+
n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
|
58
|
+
n32encoded.to_i(32)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Integer] integer
|
62
|
+
# @return [String]
|
63
|
+
def self.encode(integer)
|
64
|
+
n32encoded = integer.to_s(32)
|
65
|
+
n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -8,6 +8,7 @@ class ULID
|
|
8
8
|
attr_accessor :latest_milliseconds, :latest_entropy
|
9
9
|
|
10
10
|
def initialize
|
11
|
+
@mutex = Thread::Mutex.new
|
11
12
|
reset
|
12
13
|
end
|
13
14
|
|
@@ -19,14 +20,15 @@ class ULID
|
|
19
20
|
milliseconds = ULID.milliseconds_from_moment(moment)
|
20
21
|
raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
|
21
22
|
|
22
|
-
|
23
|
-
@latest_milliseconds
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
@mutex.synchronize do
|
24
|
+
if @latest_milliseconds < milliseconds
|
25
|
+
@latest_milliseconds = milliseconds
|
26
|
+
@latest_entropy = ULID.reasonable_entropy
|
27
|
+
else
|
28
|
+
@latest_entropy += 1
|
29
|
+
end
|
30
|
+
ULID.from_monotonic_generator(self)
|
27
31
|
end
|
28
|
-
|
29
|
-
ULID.from_monotonic_generator(self)
|
30
32
|
end
|
31
33
|
|
32
34
|
# @api private
|
data/lib/ulid/uuid.rb
CHANGED
@@ -15,15 +15,16 @@ class ULID
|
|
15
15
|
# @return [ULID]
|
16
16
|
# @raise [ParserError] if the given format is not correct for UUIDv4 specs
|
17
17
|
def self.from_uuidv4(uuid)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
rescue => err
|
25
|
-
raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
|
18
|
+
uuid = String.try_convert(uuid)
|
19
|
+
raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid
|
20
|
+
|
21
|
+
prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
|
22
|
+
unless UUIDV4_PATTERN.match?(prefix_trimmed)
|
23
|
+
raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
|
26
24
|
end
|
25
|
+
|
26
|
+
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
27
|
+
from_integer(normalized.to_i(16))
|
27
28
|
end
|
28
29
|
|
29
30
|
# @return [String]
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Classes
|
2
2
|
class ULID
|
3
3
|
VERSION: String
|
4
|
+
CROCKFORD_BASE32_ENCODING_STRING: String
|
4
5
|
TIMESTAMP_ENCODED_LENGTH: 10
|
5
6
|
RANDOMNESS_ENCODED_LENGTH: 16
|
6
7
|
ENCODED_LENGTH: 26
|
@@ -11,16 +12,13 @@ class ULID
|
|
11
12
|
MAX_ENTROPY: 1208925819614629174706175
|
12
13
|
MAX_INTEGER: 340282366920938463463374607431768211455
|
13
14
|
TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
|
14
|
-
|
15
|
-
|
15
|
+
RANDOM_INTEGER_GENERATOR: ^() -> Integer
|
16
|
+
PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
|
17
|
+
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
|
18
|
+
SCANNING_PATTERN: Regexp
|
16
19
|
UUIDV4_PATTERN: Regexp
|
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
20
|
MIN: ULID
|
22
21
|
MAX: ULID
|
23
|
-
UNDEFINED: BasicObject
|
24
22
|
include Comparable
|
25
23
|
|
26
24
|
# The `moment` is a `Time` or `Intger of the milliseconds`
|
@@ -35,7 +33,17 @@ class ULID
|
|
35
33
|
class ParserError < Error
|
36
34
|
end
|
37
35
|
|
38
|
-
|
36
|
+
module CrockfordBase32
|
37
|
+
class SetupError < ScriptError
|
38
|
+
end
|
39
|
+
|
40
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
|
41
|
+
CROCKFORD_BASE32_CHAR_PATTERN: Regexp
|
42
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
|
43
|
+
N32_CHAR_PATTERN: Regexp
|
44
|
+
|
45
|
+
def self.encode: (Integer integer) -> String
|
46
|
+
def self.decode: (String string) -> Integer
|
39
47
|
end
|
40
48
|
|
41
49
|
class MonotonicGenerator
|
@@ -50,26 +58,22 @@ class ULID
|
|
50
58
|
type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
51
59
|
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
|
52
60
|
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
61
|
+
type period = Range[Time] | Range[nil] | Range[ULID]
|
53
62
|
|
54
63
|
@milliseconds: Integer
|
55
64
|
@entropy: Integer
|
56
65
|
@string: String?
|
57
66
|
@integer: Integer?
|
58
|
-
@octets: octets?
|
59
|
-
@timestamp_octets: timestamp_octets?
|
60
|
-
@randomness_octets: randomness_octets?
|
61
67
|
@timestamp: String?
|
62
68
|
@randomness: String?
|
63
69
|
@inspect: String?
|
64
70
|
@time: Time?
|
65
|
-
@next: ULID?
|
66
71
|
|
67
72
|
def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
|
68
73
|
def self.at: (Time time) -> ULID
|
69
74
|
def self.current_milliseconds: -> Integer
|
70
|
-
def self.milliseconds_from_time: (Time time) -> Integer
|
71
75
|
def self.milliseconds_from_moment: (moment moment) -> Integer
|
72
|
-
def self.range: (
|
76
|
+
def self.range: (period period) -> Range[ULID]
|
73
77
|
def self.floor: (Time time) -> Time
|
74
78
|
def self.reasonable_entropy: -> Integer
|
75
79
|
def self.parse: (String string) -> ULID
|
@@ -77,13 +81,11 @@ class ULID
|
|
77
81
|
def self.from_integer: (Integer integer) -> ULID
|
78
82
|
def self.min: (?moment: moment) -> ULID
|
79
83
|
def self.max: (?moment: moment) -> ULID
|
80
|
-
def self.sample: -> ULID
|
81
|
-
| (Integer number) -> Array[ULID]
|
84
|
+
def self.sample: (?period: period) -> ULID
|
85
|
+
| (Integer number, ?period: period) -> Array[ULID]
|
82
86
|
def self.valid?: (untyped string) -> bool
|
83
87
|
def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
|
84
88
|
| (String string) { (ULID ulid) -> void } -> singleton(ULID)
|
85
|
-
def self.octets_from_integer: (Integer integer) -> octets
|
86
|
-
def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
|
87
89
|
def self.from_monotonic_generator: (MonotonicGenerator generator) -> ULID
|
88
90
|
attr_reader milliseconds: Integer
|
89
91
|
attr_reader entropy: Integer
|
@@ -101,8 +103,6 @@ class ULID
|
|
101
103
|
def timestamp: -> String
|
102
104
|
def randomness: -> String
|
103
105
|
def patterns: -> Hash[Symbol, Regexp | String]
|
104
|
-
def pattern: -> Regexp
|
105
|
-
def strict_pattern: -> Regexp
|
106
106
|
def octets: -> octets
|
107
107
|
def timestamp_octets: -> timestamp_octets
|
108
108
|
def randomness_octets: -> randomness_octets
|
@@ -113,6 +113,6 @@ class ULID
|
|
113
113
|
def freeze: -> self
|
114
114
|
|
115
115
|
private
|
116
|
-
def self.
|
116
|
+
def self.milliseconds_from_time: (Time time) -> Integer
|
117
117
|
def cache_all_instance_variables: -> void
|
118
118
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.19
|
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-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rbs
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- LICENSE
|
78
78
|
- README.md
|
79
79
|
- lib/ulid.rb
|
80
|
+
- lib/ulid/crockford_base32.rb
|
80
81
|
- lib/ulid/monotonic_generator.rb
|
81
82
|
- lib/ulid/uuid.rb
|
82
83
|
- lib/ulid/version.rb
|