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.
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.freeze
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.freeze
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.freeze
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.make_sharable_constantans(self)
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
- base32_encoded = Utils.encode_base32(milliseconds: milliseconds, entropy: entropy)
73
+ base32hex = Utils.encode_base32hex(milliseconds:, entropy:)
73
74
  new(
74
- milliseconds: milliseconds,
75
- entropy: entropy,
76
- integer: base32_encoded.to_i(32),
77
- encoded: CrockfordBase32.from_base32(base32_encoded).freeze
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
- base32_encoded = Utils.encode_base32(milliseconds: Utils.milliseconds_from_moment(moment), entropy: entropy)
88
- CrockfordBase32.from_base32(base32_encoded)
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: moment, entropy: 0)
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: moment, entropy: MAX_ENTROPY)
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
- base32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
185
- base32encoded_timestamp = base32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
186
- base32encoded_randomness = base32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
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 base32encoded_timestamp && base32encoded_randomness
189
+ raise(UnexpectedError) unless base32hex_timestamp && base32hex_randomness
189
190
 
190
- milliseconds = base32encoded_timestamp.to_i(32)
191
- entropy = base32encoded_randomness.to_i(32)
191
+ milliseconds = Integer(base32hex_timestamp, 32, exception: true)
192
+ entropy = Integer(base32hex_randomness, 32, exception: true)
192
193
 
193
194
  new(
194
- milliseconds: milliseconds,
195
- entropy: entropy,
196
- integer: integer,
197
- encoded: CrockfordBase32.from_base32(base32encoded).freeze
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 = binding.local_variable_get(:in) # Needed because `in` is a reserved word.
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).to_s
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
- attr_reader(:milliseconds, :entropy)
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
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{@encoded})".freeze
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
- def to_time
421
- @time ||= Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
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).times do
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
- @timestamp ||= (@encoded.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
444
+ @encoded.slice(0, TIMESTAMP_ENCODED_LENGTH) || raise(UnexpectedError)
446
445
  end
447
446
 
448
447
  # @return [String]
449
448
  def randomness
450
- @randomness ||= (@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
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.to_s
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
- # @return [self]
522
- def dup
523
- self
524
- end
525
-
526
- # @return [self]
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
- undef_method(:instance_variable_set)
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
- private
526
+ v4.to_s.freeze
527
+ end
534
528
 
535
- # @param [Integer] milliseconds
536
- # @param [Integer] entropy
537
- # @param [Integer] integer
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 [void]
549
- def cache_all_instance_variables
550
- inspect
551
- timestamp
552
- randomness
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').freeze
556
- MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
541
+ MIN = parse('00000000000000000000000000')
542
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ')
557
543
 
558
- Utils.make_sharable_value(MIN)
559
- Utils.make_sharable_value(MAX)
544
+ Ractor.make_shareable(MIN)
545
+ Ractor.make_shareable(MAX)
560
546
 
561
547
  private_constant(:MIN, :MAX)
562
548
  end