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.
data/lib/ulid/uuid.rb CHANGED
@@ -4,40 +4,72 @@
4
4
 
5
5
  # Copyright (C) 2021 Kenichi Kamiya
6
6
 
7
- # Extracted features around UUID from some reasons
8
- # ref:
9
- # * https://github.com/kachick/ruby-ulid/issues/105
10
- # * https://github.com/kachick/ruby-ulid/issues/76
7
+ require_relative('errors')
8
+ require_relative('utils')
9
+
11
10
  class ULID
12
- # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
13
- UUIDV4_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.freeze
14
- private_constant(:UUIDV4_PATTERN)
15
-
16
- # @param [String, #to_str] uuid
17
- # @return [ULID]
18
- # @raise [ParserError] if the given format is not correct for UUIDv4 specs
19
- def self.from_uuidv4(uuid)
20
- uuid = String.try_convert(uuid)
21
- raise(ArgumentError, 'ULID.from_uuidv4 takes only strings') unless uuid
22
-
23
- prefix_trimmed = uuid.delete_prefix('urn:uuid:')
24
- unless UUIDV4_PATTERN.match?(prefix_trimmed)
25
- raise(ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`")
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
- normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
29
- from_integer(normalized.to_i(16))
30
- end
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
- # @return [String]
33
- def to_uuidv4
34
- # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
35
- array = octets.pack('C*').unpack('NnnnnN')
36
- ref2, ref3 = array[2], array[3]
37
- raise unless Integer === ref2 && Integer === ref3
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
- array[2] = (ref2 & 0x0fff) | 0x4000
40
- array[3] = (ref3 & 0x3fff) | 0x8000
41
- ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
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
@@ -3,5 +3,5 @@
3
3
  # shareable_constant_value: literal
4
4
 
5
5
  class ULID
6
- VERSION = '0.6.1'
6
+ VERSION = '0.8.0'
7
7
  end
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.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.
@@ -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
- private_class_method(:new)
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
- def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
55
- from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
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
- n32_encoded = encode_n32(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
65
- CrockfordBase32.from_n32(n32_encoded)
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
- from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
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: moment, entropy: 0)
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: moment, entropy: MAX_ENTROPY)
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
- n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
166
- n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
167
- n32encoded_randomness = n32encoded.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)
168
188
 
169
- raise(UnexpectedError) unless n32encoded_timestamp && n32encoded_randomness
189
+ raise(UnexpectedError) unless base32hex_timestamp && base32hex_randomness
170
190
 
171
- milliseconds = n32encoded_timestamp.to_i(32)
172
- entropy = n32encoded_randomness.to_i(32)
191
+ milliseconds = Integer(base32hex_timestamp, 32, exception: true)
192
+ entropy = Integer(base32hex_randomness, 32, exception: true)
173
193
 
174
194
  new(
175
- milliseconds: milliseconds,
176
- entropy: entropy,
177
- integer: integer,
178
- encoded: CrockfordBase32.from_n32(n32encoded).freeze
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 = binding.local_variable_get(:in) # Needed because `in` is a reserved word.
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).to_s
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 [BasicObject] object
386
- # @return [String]
387
- private_class_method def self.safe_get_class_name(object)
388
- fallback = 'UnknownObject'
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
- # @api private
415
- # @param [Integer] milliseconds
416
- # @param [Integer] entropy
351
+ # @param [String, #to_str] uuid
417
352
  # @return [ULID]
418
- # @raise [OverflowError] if the given value is larger than the ULID limit
419
- # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
420
- def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
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
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{@encoded})".freeze
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
- def to_time
503
- @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))
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).times do
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
- @timestamp ||= (@encoded.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
444
+ @encoded.slice(0, TIMESTAMP_ENCODED_LENGTH) || raise(UnexpectedError)
528
445
  end
529
446
 
530
447
  # @return [String]
531
448
  def randomness
532
- @randomness ||= (@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
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(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy, encoded: unmarshaled.to_s)
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
- # @return [self]
601
- def dup
602
- 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
603
509
  end
604
510
 
605
- # @return [self]
606
- def clone(freeze: true)
607
- self
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
- undef_method(:instance_variable_set)
529
+ # @return [ULID]
530
+ def dup
531
+ super.freeze
532
+ end
611
533
 
612
- private
534
+ # @return [ULID]
535
+ def clone(freeze: true)
536
+ raise(ArgumentError, 'unfreezing ULID is an unexpected operation') unless freeze == true
613
537
 
614
- # @return [void]
615
- def cache_all_instance_variables
616
- inspect
617
- timestamp
618
- randomness
538
+ super
619
539
  end
620
- end
621
540
 
622
- require_relative('ulid/ractor_unshareable_constants')
541
+ MIN = parse('00000000000000000000000000')
542
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ')
623
543
 
624
- class ULID
625
- # Do not write as `ULID.private_constant` for avoiding YARD warnings `[warn]: in YARD::Handlers::Ruby::PrivateConstantHandler: Undocumentable private constants:`
626
- private_constant(:TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR)
544
+ Ractor.make_shareable(MIN)
545
+ Ractor.make_shareable(MAX)
546
+
547
+ private_constant(:MIN, :MAX)
627
548
  end