ruby-ulid 0.6.1 → 0.8.0
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 +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
|