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