ruby-ulid 0.7.0 → 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 +63 -95
- data/lib/ulid/crockford_base32.rb +12 -12
- data/lib/ulid/errors.rb +1 -0
- data/lib/ulid/monotonic_generator.rb +14 -14
- data/lib/ulid/utils.rb +6 -24
- data/lib/ulid/uuid.rb +61 -31
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +102 -116
- data/sig/ulid.rbs +137 -88
- metadata +7 -7
data/lib/ulid.rb
CHANGED
@@ -9,6 +9,7 @@ require_relative('ulid/version')
|
|
9
9
|
require_relative('ulid/errors')
|
10
10
|
require_relative('ulid/crockford_base32')
|
11
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.
|
@@ -50,17 +49,19 @@ class ULID
|
|
50
49
|
SecureRandom.random_number(MAX_INTEGER)
|
51
50
|
}.freeze
|
52
51
|
|
53
|
-
Utils.
|
52
|
+
Utils.make_sharable_constants(self)
|
54
53
|
|
55
54
|
private_constant(
|
56
55
|
:PATTERN_WITH_CROCKFORD_BASE32_SUBSET,
|
57
56
|
:STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET,
|
58
57
|
:SCANNING_PATTERN,
|
59
58
|
:TIME_FORMAT_IN_INSPECT,
|
60
|
-
:RANDOM_INTEGER_GENERATOR
|
59
|
+
:RANDOM_INTEGER_GENERATOR,
|
60
|
+
:OCTETS_LENGTH,
|
61
|
+
:UUID
|
61
62
|
)
|
62
63
|
|
63
|
-
private_class_method(:new)
|
64
|
+
private_class_method(:new, :allocate)
|
64
65
|
|
65
66
|
# @param [Integer, Time] moment
|
66
67
|
# @param [Integer] entropy
|
@@ -69,12 +70,12 @@ class ULID
|
|
69
70
|
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
70
71
|
def self.generate(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy)
|
71
72
|
milliseconds = Utils.milliseconds_from_moment(moment)
|
72
|
-
|
73
|
+
base32hex = Utils.encode_base32hex(milliseconds:, entropy:)
|
73
74
|
new(
|
74
|
-
milliseconds
|
75
|
-
entropy
|
76
|
-
integer:
|
77
|
-
encoded: CrockfordBase32.
|
75
|
+
milliseconds:,
|
76
|
+
entropy:,
|
77
|
+
integer: Integer(base32hex, 32, exception: true),
|
78
|
+
encoded: CrockfordBase32.from_base32hex(base32hex).freeze
|
78
79
|
)
|
79
80
|
end
|
80
81
|
|
@@ -84,8 +85,8 @@ class ULID
|
|
84
85
|
# @param [Integer] entropy
|
85
86
|
# @return [String]
|
86
87
|
def self.encode(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy)
|
87
|
-
|
88
|
-
CrockfordBase32.
|
88
|
+
base32hex = Utils.encode_base32hex(milliseconds: Utils.milliseconds_from_moment(moment), entropy:)
|
89
|
+
CrockfordBase32.from_base32hex(base32hex)
|
89
90
|
end
|
90
91
|
|
91
92
|
# Short hand of `ULID.generate(moment: time)`
|
@@ -100,13 +101,13 @@ class ULID
|
|
100
101
|
# @param [Time, Integer] moment
|
101
102
|
# @return [ULID]
|
102
103
|
def self.min(moment=0)
|
103
|
-
0.equal?(moment) ? MIN : generate(moment
|
104
|
+
0.equal?(moment) ? MIN : generate(moment:, entropy: 0)
|
104
105
|
end
|
105
106
|
|
106
107
|
# @param [Time, Integer] moment
|
107
108
|
# @return [ULID]
|
108
109
|
def self.max(moment=MAX_MILLISECONDS)
|
109
|
-
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment
|
110
|
+
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment:, entropy: MAX_ENTROPY)
|
110
111
|
end
|
111
112
|
|
112
113
|
# @param [Range<Time>, Range<nil>, Range[ULID], nil] period
|
@@ -181,20 +182,20 @@ class ULID
|
|
181
182
|
raise(OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}") unless integer <= MAX_INTEGER
|
182
183
|
raise(ArgumentError, "integer should not be negative: given: #{integer}") if integer.negative?
|
183
184
|
|
184
|
-
|
185
|
-
|
186
|
-
|
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)
|
187
188
|
|
188
|
-
raise(UnexpectedError) unless
|
189
|
+
raise(UnexpectedError) unless base32hex_timestamp && base32hex_randomness
|
189
190
|
|
190
|
-
milliseconds =
|
191
|
-
entropy =
|
191
|
+
milliseconds = Integer(base32hex_timestamp, 32, exception: true)
|
192
|
+
entropy = Integer(base32hex_randomness, 32, exception: true)
|
192
193
|
|
193
194
|
new(
|
194
|
-
milliseconds
|
195
|
-
entropy
|
196
|
-
integer
|
197
|
-
encoded: CrockfordBase32.
|
195
|
+
milliseconds:,
|
196
|
+
entropy:,
|
197
|
+
integer:,
|
198
|
+
encoded: CrockfordBase32.from_base32hex(base32hex).freeze
|
198
199
|
)
|
199
200
|
end
|
200
201
|
|
@@ -205,12 +206,10 @@ class ULID
|
|
205
206
|
raise(ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`') unless Range === period
|
206
207
|
|
207
208
|
begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
|
208
|
-
new_begin, new_end = false, false
|
209
209
|
|
210
210
|
begin_ulid = (
|
211
211
|
case begin_element
|
212
212
|
when Time
|
213
|
-
new_begin = true
|
214
213
|
min(begin_element)
|
215
214
|
when nil
|
216
215
|
MIN
|
@@ -224,7 +223,6 @@ class ULID
|
|
224
223
|
end_ulid = (
|
225
224
|
case end_element
|
226
225
|
when Time
|
227
|
-
new_end = true
|
228
226
|
exclude_end ? min(end_element) : max(end_element)
|
229
227
|
when nil
|
230
228
|
exclude_end = false
|
@@ -237,9 +235,6 @@ class ULID
|
|
237
235
|
end
|
238
236
|
)
|
239
237
|
|
240
|
-
begin_ulid.freeze if new_begin
|
241
|
-
end_ulid.freeze if new_end
|
242
|
-
|
243
238
|
Range.new(begin_ulid, end_ulid, exclude_end)
|
244
239
|
end
|
245
240
|
|
@@ -283,7 +278,7 @@ class ULID
|
|
283
278
|
# @return [Time]
|
284
279
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
285
280
|
def self.decode_time(string, in: 'UTC')
|
286
|
-
in_for_time_at =
|
281
|
+
in_for_time_at = { in: }.fetch(:in)
|
287
282
|
string = String.try_convert(string)
|
288
283
|
raise(ArgumentError, 'ULID.decode_time takes only strings') unless string
|
289
284
|
|
@@ -304,7 +299,7 @@ class ULID
|
|
304
299
|
raise(ArgumentError, 'ULID.normalize takes only strings') unless string
|
305
300
|
|
306
301
|
# Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
|
307
|
-
parse_variant_format(string).
|
302
|
+
parse_variant_format(string).encode
|
308
303
|
end
|
309
304
|
|
310
305
|
# @param [String, #to_str] string
|
@@ -327,19 +322,6 @@ class ULID
|
|
327
322
|
true
|
328
323
|
end
|
329
324
|
|
330
|
-
# @deprecated Use [.valid_as_variant_format?] or [.normalized?] instead
|
331
|
-
#
|
332
|
-
# Returns `true` if it is normalized string.
|
333
|
-
# Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs.
|
334
|
-
#
|
335
|
-
# @return [Boolean]
|
336
|
-
def self.valid?(string)
|
337
|
-
warn_kwargs = (RUBY_VERSION >= '3.0') ? { category: :deprecated } : {}
|
338
|
-
Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.', **warn_kwargs)
|
339
|
-
string = String.try_convert(string)
|
340
|
-
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
|
341
|
-
end
|
342
|
-
|
343
325
|
# @param [ULID, #to_ulid] object
|
344
326
|
# @return [ULID, nil]
|
345
327
|
# @raise [TypeError] if `object.to_ulid` did not return ULID instance
|
@@ -359,7 +341,36 @@ class ULID
|
|
359
341
|
end
|
360
342
|
end
|
361
343
|
|
362
|
-
|
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))
|
349
|
+
end
|
350
|
+
|
351
|
+
# @param [String, #to_str] uuid
|
352
|
+
# @return [ULID]
|
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))
|
356
|
+
end
|
357
|
+
|
358
|
+
attr_reader(:milliseconds, :entropy, :encoded)
|
359
|
+
protected(:encoded)
|
360
|
+
|
361
|
+
# @param [Integer] milliseconds
|
362
|
+
# @param [Integer] entropy
|
363
|
+
# @param [Integer] integer
|
364
|
+
# @param [String] encoded
|
365
|
+
# @return [void]
|
366
|
+
def initialize(milliseconds:, entropy:, integer:, encoded:)
|
367
|
+
# All arguments check should be done with each constructors, not here
|
368
|
+
@integer = integer
|
369
|
+
@encoded = encoded
|
370
|
+
@milliseconds = milliseconds
|
371
|
+
@entropy = entropy
|
372
|
+
freeze
|
373
|
+
end
|
363
374
|
|
364
375
|
# @return [String]
|
365
376
|
def encode
|
@@ -384,7 +395,7 @@ class ULID
|
|
384
395
|
|
385
396
|
# @return [String]
|
386
397
|
def inspect
|
387
|
-
|
398
|
+
"ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{@encoded})"
|
388
399
|
end
|
389
400
|
|
390
401
|
# @return [Boolean]
|
@@ -417,47 +428,25 @@ class ULID
|
|
417
428
|
end
|
418
429
|
|
419
430
|
# @return [Time]
|
420
|
-
|
421
|
-
|
431
|
+
# @param [String, Integer, nil] in
|
432
|
+
def to_time(in: 'UTC')
|
433
|
+
Time.at(0, @milliseconds, :millisecond, in: { in: }.fetch(:in))
|
422
434
|
end
|
423
435
|
|
424
436
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
425
437
|
def octets
|
426
438
|
digits = @integer.digits(256)
|
427
|
-
(OCTETS_LENGTH - digits.size).
|
428
|
-
digits.push(0)
|
429
|
-
end
|
430
|
-
digits.reverse!
|
431
|
-
end
|
432
|
-
|
433
|
-
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
434
|
-
def timestamp_octets
|
435
|
-
octets.slice(0, TIMESTAMP_OCTETS_LENGTH) || raise(UnexpectedError)
|
436
|
-
end
|
437
|
-
|
438
|
-
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
439
|
-
def randomness_octets
|
440
|
-
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH) || raise(UnexpectedError)
|
439
|
+
digits.fill(0, digits.size, OCTETS_LENGTH - digits.size).reverse
|
441
440
|
end
|
442
441
|
|
443
442
|
# @return [String]
|
444
443
|
def timestamp
|
445
|
-
@
|
444
|
+
@encoded.slice(0, TIMESTAMP_ENCODED_LENGTH) || raise(UnexpectedError)
|
446
445
|
end
|
447
446
|
|
448
447
|
# @return [String]
|
449
448
|
def randomness
|
450
|
-
@
|
451
|
-
end
|
452
|
-
|
453
|
-
# @note Providing for rough operations. The keys and values is not fixed.
|
454
|
-
# @return [Hash{Symbol => Regexp, String}]
|
455
|
-
def patterns
|
456
|
-
named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
457
|
-
{
|
458
|
-
named_captures: named_captures,
|
459
|
-
strict_named_captures: /\A#{named_captures.source}\z/i.freeze
|
460
|
-
}
|
449
|
+
@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH) || raise(UnexpectedError)
|
461
450
|
end
|
462
451
|
|
463
452
|
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
@@ -489,13 +478,6 @@ class ULID
|
|
489
478
|
end
|
490
479
|
end
|
491
480
|
|
492
|
-
# @return [self]
|
493
|
-
def freeze
|
494
|
-
# Need to cache before freezing, because frozen objects can't assign instance variables
|
495
|
-
cache_all_instance_variables
|
496
|
-
super
|
497
|
-
end
|
498
|
-
|
499
481
|
# @return [Integer]
|
500
482
|
def marshal_dump
|
501
483
|
@integer
|
@@ -509,7 +491,7 @@ class ULID
|
|
509
491
|
integer: unmarshaled.to_i,
|
510
492
|
milliseconds: unmarshaled.milliseconds,
|
511
493
|
entropy: unmarshaled.entropy,
|
512
|
-
encoded: unmarshaled.
|
494
|
+
encoded: unmarshaled.encoded
|
513
495
|
)
|
514
496
|
end
|
515
497
|
|
@@ -518,45 +500,49 @@ class ULID
|
|
518
500
|
self
|
519
501
|
end
|
520
502
|
|
521
|
-
#
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
def clone(freeze: true)
|
528
|
-
self
|
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
|
529
509
|
end
|
530
510
|
|
531
|
-
|
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
|
532
525
|
|
533
|
-
|
526
|
+
v4.to_s.freeze
|
527
|
+
end
|
534
528
|
|
535
|
-
# @
|
536
|
-
|
537
|
-
|
538
|
-
# @param [String] encoded
|
539
|
-
# @return [void]
|
540
|
-
def initialize(milliseconds:, entropy:, integer:, encoded:)
|
541
|
-
# All arguments check should be done with each constructors, not here
|
542
|
-
@integer = integer
|
543
|
-
@encoded = encoded
|
544
|
-
@milliseconds = milliseconds
|
545
|
-
@entropy = entropy
|
529
|
+
# @return [ULID]
|
530
|
+
def dup
|
531
|
+
super.freeze
|
546
532
|
end
|
547
533
|
|
548
|
-
# @return [
|
549
|
-
def
|
550
|
-
|
551
|
-
|
552
|
-
|
534
|
+
# @return [ULID]
|
535
|
+
def clone(freeze: true)
|
536
|
+
raise(ArgumentError, 'unfreezing ULID is an unexpected operation') unless freeze == true
|
537
|
+
|
538
|
+
super
|
553
539
|
end
|
554
540
|
|
555
|
-
MIN = parse('00000000000000000000000000')
|
556
|
-
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ')
|
541
|
+
MIN = parse('00000000000000000000000000')
|
542
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ')
|
557
543
|
|
558
|
-
|
559
|
-
|
544
|
+
Ractor.make_shareable(MIN)
|
545
|
+
Ractor.make_shareable(MAX)
|
560
546
|
|
561
547
|
private_constant(:MIN, :MAX)
|
562
548
|
end
|