ruby-ulid 0.6.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -106
- data/lib/ulid/crockford_base32.rb +17 -16
- data/lib/ulid/errors.rb +1 -0
- data/lib/ulid/monotonic_generator.rb +23 -16
- data/lib/ulid/utils.rb +99 -0
- data/lib/ulid/uuid.rb +62 -30
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +115 -194
- data/sig/ulid.rbs +162 -117
- metadata +8 -8
- data/lib/ulid/ractor_unshareable_constants.rb +0 -12
data/lib/ulid/uuid.rb
CHANGED
@@ -4,40 +4,72 @@
|
|
4
4
|
|
5
5
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# * https://github.com/kachick/ruby-ulid/issues/76
|
7
|
+
require_relative('errors')
|
8
|
+
require_relative('utils')
|
9
|
+
|
11
10
|
class ULID
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
11
|
+
module UUID
|
12
|
+
BASE_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\z/i
|
13
|
+
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
14
|
+
V4_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
|
15
|
+
|
16
|
+
def self.parse_any_to_int(uuidish)
|
17
|
+
encoded = String.try_convert(uuidish)
|
18
|
+
raise(ArgumentError, 'should pass a string for UUID parser') unless encoded
|
19
|
+
|
20
|
+
prefix_trimmed = encoded.delete_prefix('urn:uuid:')
|
21
|
+
unless BASE_PATTERN.match?(prefix_trimmed)
|
22
|
+
raise(ParserError, "given `#{encoded}` does not match to `#{BASE_PATTERN.inspect}`")
|
23
|
+
end
|
24
|
+
|
25
|
+
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
26
|
+
Integer(normalized, 16, exception: true)
|
26
27
|
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def self.parse_v4_to_int(uuid)
|
30
|
+
encoded = String.try_convert(uuid)
|
31
|
+
raise(ArgumentError, 'should pass a string for UUID parser') unless encoded
|
32
|
+
|
33
|
+
prefix_trimmed = encoded.delete_prefix('urn:uuid:')
|
34
|
+
unless V4_PATTERN.match?(prefix_trimmed)
|
35
|
+
raise(ParserError, "given `#{encoded}` does not match to `#{V4_PATTERN.inspect}`")
|
36
|
+
end
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
+
parse_any_to_int(encoded)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @see https://www.rfc-editor.org/rfc/rfc4122#section-4.1.2
|
42
|
+
# @todo Replace to Data class after dropped Ruby 3.1
|
43
|
+
# @note Using `Fields = Struct.new` syntax made https://github.com/kachick/ruby-ulid/issues/233 again. So use class syntax instead
|
44
|
+
class Fields < Struct.new(:time_low, :time_mid, :time_hi_and_version, :clock_seq_hi_and_res, :clk_seq_low, :node, keyword_init: true)
|
45
|
+
def self.raw_from_octets(octets)
|
46
|
+
case octets.pack('C*').unpack('NnnnnN')
|
47
|
+
in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node]
|
48
|
+
new(time_low:, time_mid:, time_hi_and_version:, clock_seq_hi_and_res:, clk_seq_low:, node:).freeze
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.forced_v4_from_octets(octets)
|
53
|
+
case octets.pack('C*').unpack('NnnnnN')
|
54
|
+
in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node]
|
55
|
+
new(
|
56
|
+
time_low:,
|
57
|
+
time_mid:,
|
58
|
+
time_hi_and_version: (time_hi_and_version & 0x0fff) | 0x4000,
|
59
|
+
clock_seq_hi_and_res: (clock_seq_hi_and_res & 0x3fff) | 0x8000,
|
60
|
+
clk_seq_low:,
|
61
|
+
node:
|
62
|
+
).freeze
|
63
|
+
end
|
64
|
+
end
|
38
65
|
|
39
|
-
|
40
|
-
|
41
|
-
|
66
|
+
def to_s
|
67
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % values
|
68
|
+
end
|
69
|
+
end
|
42
70
|
end
|
71
|
+
|
72
|
+
Ractor.make_shareable(UUID)
|
73
|
+
|
74
|
+
private_constant(:UUID)
|
43
75
|
end
|
data/lib/ulid/version.rb
CHANGED
data/lib/ulid.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
-
# shareable_constant_value: experimental_everything
|
4
3
|
|
5
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
5
|
|
@@ -9,6 +8,8 @@ require('securerandom')
|
|
9
8
|
require_relative('ulid/version')
|
10
9
|
require_relative('ulid/errors')
|
11
10
|
require_relative('ulid/crockford_base32')
|
11
|
+
require_relative('ulid/utils')
|
12
|
+
require_relative('ulid/uuid')
|
12
13
|
require_relative('ulid/monotonic_generator')
|
13
14
|
|
14
15
|
# @see https://github.com/ulid/spec
|
@@ -23,8 +24,6 @@ class ULID
|
|
23
24
|
RANDOMNESS_ENCODED_LENGTH = 16
|
24
25
|
ENCODED_LENGTH = 26
|
25
26
|
|
26
|
-
TIMESTAMP_OCTETS_LENGTH = 6
|
27
|
-
RANDOMNESS_OCTETS_LENGTH = 10
|
28
27
|
OCTETS_LENGTH = 16
|
29
28
|
|
30
29
|
MAX_MILLISECONDS = 281474976710655
|
@@ -33,12 +32,12 @@ class ULID
|
|
33
32
|
|
34
33
|
# @see https://github.com/ulid/spec/pull/57
|
35
34
|
# Currently not used as a constant, but kept as a reference for now.
|
36
|
-
PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i
|
35
|
+
PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i
|
37
36
|
|
38
|
-
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i
|
37
|
+
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i
|
39
38
|
|
40
39
|
# Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
|
41
|
-
SCANNING_PATTERN = /\b[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}\b/i
|
40
|
+
SCANNING_PATTERN = /\b[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}\b/i
|
42
41
|
|
43
42
|
# Similar as Time#inspect since Ruby 2.7, however it is NOT same.
|
44
43
|
# Time#inspect trancates needless digits. Keeping full milliseconds with "%3N" will fit for ULID.
|
@@ -46,13 +45,38 @@ class ULID
|
|
46
45
|
# @see https://github.com/ruby/ruby/blob/744d17ff6c33b09334508e8110007ea2a82252f5/time.c#L4026-L4078
|
47
46
|
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
48
47
|
|
49
|
-
|
48
|
+
RANDOM_INTEGER_GENERATOR = -> {
|
49
|
+
SecureRandom.random_number(MAX_INTEGER)
|
50
|
+
}.freeze
|
51
|
+
|
52
|
+
Utils.make_sharable_constants(self)
|
53
|
+
|
54
|
+
private_constant(
|
55
|
+
:PATTERN_WITH_CROCKFORD_BASE32_SUBSET,
|
56
|
+
:STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET,
|
57
|
+
:SCANNING_PATTERN,
|
58
|
+
:TIME_FORMAT_IN_INSPECT,
|
59
|
+
:RANDOM_INTEGER_GENERATOR,
|
60
|
+
:OCTETS_LENGTH,
|
61
|
+
:UUID
|
62
|
+
)
|
63
|
+
|
64
|
+
private_class_method(:new, :allocate)
|
50
65
|
|
51
66
|
# @param [Integer, Time] moment
|
52
67
|
# @param [Integer] entropy
|
53
68
|
# @return [ULID]
|
54
|
-
|
55
|
-
|
69
|
+
# @raise [OverflowError] if the given value is larger than the ULID limit
|
70
|
+
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
71
|
+
def self.generate(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy)
|
72
|
+
milliseconds = Utils.milliseconds_from_moment(moment)
|
73
|
+
base32hex = Utils.encode_base32hex(milliseconds:, entropy:)
|
74
|
+
new(
|
75
|
+
milliseconds:,
|
76
|
+
entropy:,
|
77
|
+
integer: Integer(base32hex, 32, exception: true),
|
78
|
+
encoded: CrockfordBase32.from_base32hex(base32hex).freeze
|
79
|
+
)
|
56
80
|
end
|
57
81
|
|
58
82
|
# Almost same as [.generate] except directly returning String without needless object creation
|
@@ -60,9 +84,9 @@ class ULID
|
|
60
84
|
# @param [Integer, Time] moment
|
61
85
|
# @param [Integer] entropy
|
62
86
|
# @return [String]
|
63
|
-
def self.encode(moment: current_milliseconds, entropy: reasonable_entropy)
|
64
|
-
|
65
|
-
CrockfordBase32.
|
87
|
+
def self.encode(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy)
|
88
|
+
base32hex = Utils.encode_base32hex(milliseconds: Utils.milliseconds_from_moment(moment), entropy:)
|
89
|
+
CrockfordBase32.from_base32hex(base32hex)
|
66
90
|
end
|
67
91
|
|
68
92
|
# Short hand of `ULID.generate(moment: time)`
|
@@ -71,25 +95,21 @@ class ULID
|
|
71
95
|
def self.at(time)
|
72
96
|
raise(ArgumentError, 'ULID.at takes only `Time` instance') unless Time === time
|
73
97
|
|
74
|
-
|
98
|
+
generate(moment: time)
|
75
99
|
end
|
76
100
|
|
77
101
|
# @param [Time, Integer] moment
|
78
102
|
# @return [ULID]
|
79
103
|
def self.min(moment=0)
|
80
|
-
0.equal?(moment) ? MIN : generate(moment
|
104
|
+
0.equal?(moment) ? MIN : generate(moment:, entropy: 0)
|
81
105
|
end
|
82
106
|
|
83
107
|
# @param [Time, Integer] moment
|
84
108
|
# @return [ULID]
|
85
109
|
def self.max(moment=MAX_MILLISECONDS)
|
86
|
-
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment
|
110
|
+
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment:, entropy: MAX_ENTROPY)
|
87
111
|
end
|
88
112
|
|
89
|
-
RANDOM_INTEGER_GENERATOR = -> {
|
90
|
-
SecureRandom.random_number(MAX_INTEGER)
|
91
|
-
}.freeze
|
92
|
-
|
93
113
|
# @param [Range<Time>, Range<nil>, Range[ULID], nil] period
|
94
114
|
# @overload sample(number, period: nil)
|
95
115
|
# @param [Integer] number
|
@@ -162,20 +182,20 @@ class ULID
|
|
162
182
|
raise(OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}") unless integer <= MAX_INTEGER
|
163
183
|
raise(ArgumentError, "integer should not be negative: given: #{integer}") if integer.negative?
|
164
184
|
|
165
|
-
|
166
|
-
|
167
|
-
|
185
|
+
base32hex = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
|
186
|
+
base32hex_timestamp = base32hex.slice(0, TIMESTAMP_ENCODED_LENGTH)
|
187
|
+
base32hex_randomness = base32hex.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
|
168
188
|
|
169
|
-
raise(UnexpectedError) unless
|
189
|
+
raise(UnexpectedError) unless base32hex_timestamp && base32hex_randomness
|
170
190
|
|
171
|
-
milliseconds =
|
172
|
-
entropy =
|
191
|
+
milliseconds = Integer(base32hex_timestamp, 32, exception: true)
|
192
|
+
entropy = Integer(base32hex_randomness, 32, exception: true)
|
173
193
|
|
174
194
|
new(
|
175
|
-
milliseconds
|
176
|
-
entropy
|
177
|
-
integer
|
178
|
-
encoded: CrockfordBase32.
|
195
|
+
milliseconds:,
|
196
|
+
entropy:,
|
197
|
+
integer:,
|
198
|
+
encoded: CrockfordBase32.from_base32hex(base32hex).freeze
|
179
199
|
)
|
180
200
|
end
|
181
201
|
|
@@ -186,12 +206,10 @@ class ULID
|
|
186
206
|
raise(ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`') unless Range === period
|
187
207
|
|
188
208
|
begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
|
189
|
-
new_begin, new_end = false, false
|
190
209
|
|
191
210
|
begin_ulid = (
|
192
211
|
case begin_element
|
193
212
|
when Time
|
194
|
-
new_begin = true
|
195
213
|
min(begin_element)
|
196
214
|
when nil
|
197
215
|
MIN
|
@@ -205,7 +223,6 @@ class ULID
|
|
205
223
|
end_ulid = (
|
206
224
|
case end_element
|
207
225
|
when Time
|
208
|
-
new_end = true
|
209
226
|
exclude_end ? min(end_element) : max(end_element)
|
210
227
|
when nil
|
211
228
|
exclude_end = false
|
@@ -218,9 +235,6 @@ class ULID
|
|
218
235
|
end
|
219
236
|
)
|
220
237
|
|
221
|
-
begin_ulid.freeze if new_begin
|
222
|
-
end_ulid.freeze if new_end
|
223
|
-
|
224
238
|
Range.new(begin_ulid, end_ulid, exclude_end)
|
225
239
|
end
|
226
240
|
|
@@ -232,49 +246,6 @@ class ULID
|
|
232
246
|
time.floor(3)
|
233
247
|
end
|
234
248
|
|
235
|
-
# @api private
|
236
|
-
# @return [Integer]
|
237
|
-
def self.current_milliseconds
|
238
|
-
milliseconds_from_time(Time.now)
|
239
|
-
end
|
240
|
-
|
241
|
-
# @api private
|
242
|
-
# @param [Time] time
|
243
|
-
# @return [Integer]
|
244
|
-
private_class_method def self.milliseconds_from_time(time)
|
245
|
-
(time.to_r * 1000).to_i
|
246
|
-
end
|
247
|
-
|
248
|
-
# @api private
|
249
|
-
# @param [Time, Integer] moment
|
250
|
-
# @return [Integer]
|
251
|
-
def self.milliseconds_from_moment(moment)
|
252
|
-
case moment
|
253
|
-
when Integer
|
254
|
-
moment
|
255
|
-
when Time
|
256
|
-
milliseconds_from_time(moment)
|
257
|
-
else
|
258
|
-
raise(ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`')
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
# @return [Integer]
|
263
|
-
private_class_method def self.reasonable_entropy
|
264
|
-
SecureRandom.random_number(MAX_ENTROPY)
|
265
|
-
end
|
266
|
-
|
267
|
-
private_class_method def self.encode_n32(milliseconds:, entropy:)
|
268
|
-
raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
|
269
|
-
raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
|
270
|
-
raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
|
271
|
-
raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
|
272
|
-
|
273
|
-
n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
274
|
-
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
275
|
-
"#{n32encoded_timestamp}#{n32encoded_randomness}"
|
276
|
-
end
|
277
|
-
|
278
249
|
# @param [String, #to_str] string
|
279
250
|
# @return [ULID]
|
280
251
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
@@ -303,10 +274,11 @@ class ULID
|
|
303
274
|
# Almost same as `ULID.parse(string).to_time` except directly returning Time instance without needless object creation
|
304
275
|
#
|
305
276
|
# @param [String, #to_str] string
|
277
|
+
# @param [String, Integer, nil] in
|
306
278
|
# @return [Time]
|
307
279
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
308
280
|
def self.decode_time(string, in: 'UTC')
|
309
|
-
in_for_time_at =
|
281
|
+
in_for_time_at = { in: }.fetch(:in)
|
310
282
|
string = String.try_convert(string)
|
311
283
|
raise(ArgumentError, 'ULID.decode_time takes only strings') unless string
|
312
284
|
|
@@ -327,7 +299,7 @@ class ULID
|
|
327
299
|
raise(ArgumentError, 'ULID.normalize takes only strings') unless string
|
328
300
|
|
329
301
|
# Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
|
330
|
-
parse_variant_format(string).
|
302
|
+
parse_variant_format(string).encode
|
331
303
|
end
|
332
304
|
|
333
305
|
# @param [String, #to_str] string
|
@@ -350,19 +322,6 @@ class ULID
|
|
350
322
|
true
|
351
323
|
end
|
352
324
|
|
353
|
-
# @deprecated Use [.valid_as_variant_format?] or [.normalized?] instead
|
354
|
-
#
|
355
|
-
# Returns `true` if it is normalized string.
|
356
|
-
# Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs.
|
357
|
-
#
|
358
|
-
# @return [Boolean]
|
359
|
-
def self.valid?(string)
|
360
|
-
warn_kwargs = (RUBY_VERSION >= '3.0') ? { category: :deprecated } : {}
|
361
|
-
Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.', **warn_kwargs)
|
362
|
-
string = String.try_convert(string)
|
363
|
-
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
|
364
|
-
end
|
365
|
-
|
366
325
|
# @param [ULID, #to_ulid] object
|
367
326
|
# @return [ULID, nil]
|
368
327
|
# @raise [TypeError] if `object.to_ulid` did not return ULID instance
|
@@ -375,61 +334,30 @@ class ULID
|
|
375
334
|
if ULID === converted
|
376
335
|
converted
|
377
336
|
else
|
378
|
-
object_class_name = safe_get_class_name(object)
|
379
|
-
converted_class_name = safe_get_class_name(converted)
|
337
|
+
object_class_name = Utils.safe_get_class_name(object)
|
338
|
+
converted_class_name = Utils.safe_get_class_name(converted)
|
380
339
|
raise(TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})")
|
381
340
|
end
|
382
341
|
end
|
383
342
|
end
|
384
343
|
|
385
|
-
# @param [
|
386
|
-
# @return [
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
# This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
|
391
|
-
# ref: https://twitter.com/_kachick/status/1400064896759304196
|
392
|
-
klass = (
|
393
|
-
begin
|
394
|
-
object.class
|
395
|
-
rescue NoMethodError
|
396
|
-
# steep can't correctly handle singleton class assign. See https://github.com/soutaro/steep/pull/586 for further detail
|
397
|
-
# So this annotation is hack for the type infer.
|
398
|
-
# @type var object: BasicObject
|
399
|
-
# @type var singleton_class: untyped
|
400
|
-
singleton_class = class << object; self; end
|
401
|
-
(Class === singleton_class) ? singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) } : fallback
|
402
|
-
end
|
403
|
-
)
|
404
|
-
|
405
|
-
begin
|
406
|
-
name = String.try_convert(klass.name)
|
407
|
-
rescue Exception
|
408
|
-
fallback
|
409
|
-
else
|
410
|
-
name || fallback
|
411
|
-
end
|
344
|
+
# @param [String, #to_str] uuidish
|
345
|
+
# @return [ULID]
|
346
|
+
# @raise [ParserError] if the given format is not correct for UUID`ish` format
|
347
|
+
def self.from_uuidish(uuidish)
|
348
|
+
from_integer(UUID.parse_any_to_int(uuidish))
|
412
349
|
end
|
413
350
|
|
414
|
-
# @
|
415
|
-
# @param [Integer] milliseconds
|
416
|
-
# @param [Integer] entropy
|
351
|
+
# @param [String, #to_str] uuid
|
417
352
|
# @return [ULID]
|
418
|
-
# @raise [
|
419
|
-
|
420
|
-
|
421
|
-
n32_encoded = encode_n32(milliseconds: milliseconds, entropy: entropy)
|
422
|
-
new(
|
423
|
-
milliseconds: milliseconds,
|
424
|
-
entropy: entropy,
|
425
|
-
integer: n32_encoded.to_i(32),
|
426
|
-
encoded: CrockfordBase32.from_n32(n32_encoded).upcase.freeze
|
427
|
-
)
|
353
|
+
# @raise [ParserError] if the given format is not correct for UUIDv4 specs
|
354
|
+
def self.from_uuidv4(uuid)
|
355
|
+
from_integer(UUID.parse_v4_to_int(uuid))
|
428
356
|
end
|
429
357
|
|
430
|
-
attr_reader(:milliseconds, :entropy)
|
358
|
+
attr_reader(:milliseconds, :entropy, :encoded)
|
359
|
+
protected(:encoded)
|
431
360
|
|
432
|
-
# @api private
|
433
361
|
# @param [Integer] milliseconds
|
434
362
|
# @param [Integer] entropy
|
435
363
|
# @param [Integer] integer
|
@@ -441,6 +369,7 @@ class ULID
|
|
441
369
|
@encoded = encoded
|
442
370
|
@milliseconds = milliseconds
|
443
371
|
@entropy = entropy
|
372
|
+
freeze
|
444
373
|
end
|
445
374
|
|
446
375
|
# @return [String]
|
@@ -466,7 +395,7 @@ class ULID
|
|
466
395
|
|
467
396
|
# @return [String]
|
468
397
|
def inspect
|
469
|
-
|
398
|
+
"ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{@encoded})"
|
470
399
|
end
|
471
400
|
|
472
401
|
# @return [Boolean]
|
@@ -499,47 +428,25 @@ class ULID
|
|
499
428
|
end
|
500
429
|
|
501
430
|
# @return [Time]
|
502
|
-
|
503
|
-
|
431
|
+
# @param [String, Integer, nil] in
|
432
|
+
def to_time(in: 'UTC')
|
433
|
+
Time.at(0, @milliseconds, :millisecond, in: { in: }.fetch(:in))
|
504
434
|
end
|
505
435
|
|
506
436
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
507
437
|
def octets
|
508
438
|
digits = @integer.digits(256)
|
509
|
-
(OCTETS_LENGTH - digits.size).
|
510
|
-
digits.push(0)
|
511
|
-
end
|
512
|
-
digits.reverse!
|
513
|
-
end
|
514
|
-
|
515
|
-
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
516
|
-
def timestamp_octets
|
517
|
-
octets.slice(0, TIMESTAMP_OCTETS_LENGTH) || raise(UnexpectedError)
|
518
|
-
end
|
519
|
-
|
520
|
-
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
521
|
-
def randomness_octets
|
522
|
-
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH) || raise(UnexpectedError)
|
439
|
+
digits.fill(0, digits.size, OCTETS_LENGTH - digits.size).reverse
|
523
440
|
end
|
524
441
|
|
525
442
|
# @return [String]
|
526
443
|
def timestamp
|
527
|
-
@
|
444
|
+
@encoded.slice(0, TIMESTAMP_ENCODED_LENGTH) || raise(UnexpectedError)
|
528
445
|
end
|
529
446
|
|
530
447
|
# @return [String]
|
531
448
|
def randomness
|
532
|
-
@
|
533
|
-
end
|
534
|
-
|
535
|
-
# @note Providing for rough operations. The keys and values is not fixed.
|
536
|
-
# @return [Hash{Symbol => Regexp, String}]
|
537
|
-
def patterns
|
538
|
-
named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
539
|
-
{
|
540
|
-
named_captures: named_captures,
|
541
|
-
strict_named_captures: /\A#{named_captures.source}\z/i.freeze
|
542
|
-
}
|
449
|
+
@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH) || raise(UnexpectedError)
|
543
450
|
end
|
544
451
|
|
545
452
|
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
@@ -571,25 +478,21 @@ class ULID
|
|
571
478
|
end
|
572
479
|
end
|
573
480
|
|
574
|
-
# @return [self]
|
575
|
-
def freeze
|
576
|
-
# Need to cache before freezing, because frozen objects can't assign instance variables
|
577
|
-
cache_all_instance_variables
|
578
|
-
super
|
579
|
-
end
|
580
|
-
|
581
|
-
# @api private
|
582
481
|
# @return [Integer]
|
583
482
|
def marshal_dump
|
584
483
|
@integer
|
585
484
|
end
|
586
485
|
|
587
|
-
# @api private
|
588
486
|
# @param [Integer] integer
|
589
487
|
# @return [void]
|
590
488
|
def marshal_load(integer)
|
591
489
|
unmarshaled = ULID.from_integer(integer)
|
592
|
-
initialize(
|
490
|
+
initialize(
|
491
|
+
integer: unmarshaled.to_i,
|
492
|
+
milliseconds: unmarshaled.milliseconds,
|
493
|
+
entropy: unmarshaled.entropy,
|
494
|
+
encoded: unmarshaled.encoded
|
495
|
+
)
|
593
496
|
end
|
594
497
|
|
595
498
|
# @return [self]
|
@@ -597,31 +500,49 @@ class ULID
|
|
597
500
|
self
|
598
501
|
end
|
599
502
|
|
600
|
-
#
|
601
|
-
|
602
|
-
|
503
|
+
# Generate a UUID-like string that does not set the version and variants field.
|
504
|
+
# It means wrong in UUIDv4 spec, but reversible
|
505
|
+
#
|
506
|
+
# @return [String]
|
507
|
+
def to_uuidish
|
508
|
+
UUID::Fields.raw_from_octets(octets).to_s.freeze
|
603
509
|
end
|
604
510
|
|
605
|
-
#
|
606
|
-
|
607
|
-
|
511
|
+
# Generate a UUIDv4-like string that sets the version and variants field.
|
512
|
+
# It may conform to the UUID specification, but it is irreversible with the source ULID and may conflict with some other ULIDs.
|
513
|
+
# You can specify `force` keyword argument to turn off the irreversible check
|
514
|
+
#
|
515
|
+
# @raise [IrreversibleUUIDError] if the converted UUID cannot be reversible with the replacing above 2 fields
|
516
|
+
# @see https://github.com/kachick/ruby-ulid/issues/76
|
517
|
+
# @param [bool] force
|
518
|
+
# @return [String]
|
519
|
+
def to_uuidv4(force: false)
|
520
|
+
v4 = UUID::Fields.forced_v4_from_octets(octets)
|
521
|
+
unless force
|
522
|
+
uuidish = UUID::Fields.raw_from_octets(octets)
|
523
|
+
raise(IrreversibleUUIDError) unless uuidish == v4
|
524
|
+
end
|
525
|
+
|
526
|
+
v4.to_s.freeze
|
608
527
|
end
|
609
528
|
|
610
|
-
|
529
|
+
# @return [ULID]
|
530
|
+
def dup
|
531
|
+
super.freeze
|
532
|
+
end
|
611
533
|
|
612
|
-
|
534
|
+
# @return [ULID]
|
535
|
+
def clone(freeze: true)
|
536
|
+
raise(ArgumentError, 'unfreezing ULID is an unexpected operation') unless freeze == true
|
613
537
|
|
614
|
-
|
615
|
-
def cache_all_instance_variables
|
616
|
-
inspect
|
617
|
-
timestamp
|
618
|
-
randomness
|
538
|
+
super
|
619
539
|
end
|
620
|
-
end
|
621
540
|
|
622
|
-
|
541
|
+
MIN = parse('00000000000000000000000000')
|
542
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ')
|
623
543
|
|
624
|
-
|
625
|
-
|
626
|
-
|
544
|
+
Ractor.make_shareable(MIN)
|
545
|
+
Ractor.make_shareable(MAX)
|
546
|
+
|
547
|
+
private_constant(:MIN, :MAX)
|
627
548
|
end
|