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.
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