ruby-ulid 0.0.17 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29a9e3cd7ec91b93c24691976fef74ea5ab0cbc8f4fa237f449fa9322e42c6d2
4
- data.tar.gz: 9a3c0e12e7d2fe8b6057662ebf6a392c749f7254ee41c4e77221a2e9f45ff6ac
3
+ metadata.gz: 8b1641b0894c0140c3a9c6f59fa8abcfca386362ad1aaa92fe196f3bf5f75cf1
4
+ data.tar.gz: 932fa04dceb504330289d4236f9670fecd702989504bdd402873d60c0bb1c0e5
5
5
  SHA512:
6
- metadata.gz: 53a12c330c32f76f13e651cef96ce29a6a442cf3dc7742faf2409a0ee17c7dfdcee1031c17696cd51d7bd3cf5b22b2df34b8be5a203cd1d4fd5df079eb357b82
7
- data.tar.gz: f6c8f8850272353bbd32cc15d437d0b021c13d9bc93c5c988255d6c50fe21ff41aef5b719261ca95b00c129024fe325e2d3c8683c70f9b55e89a09a261fff31f
6
+ metadata.gz: f073c7b240ef96b511c84c6bc5649c483fc97c62a3892f58531b6f36e8235e49dcd865433fe522eec48a3532c98c4deec8713f453a4b2a117157788eabddaf6b
7
+ data.tar.gz: a2486bbefd62d0d019c15044c9d67b6aabc33e370f4ee88239c1e2b33a40738c736fcc9221dd3ce457eabf83a3580be5dc501fef0c359345a453331bdd62d139
data/README.md CHANGED
@@ -49,7 +49,7 @@ Should be installed!
49
49
  Add this line to your application/library's `Gemfile` is needed in basic use-case
50
50
 
51
51
  ```ruby
52
- gem 'ruby-ulid', '0.0.16'
52
+ gem 'ruby-ulid', '0.0.18'
53
53
  ```
54
54
 
55
55
  ### Generator and Parser
@@ -62,7 +62,11 @@ require 'ulid'
62
62
 
63
63
  ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
64
64
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
65
+ ulid.milliseconds #=> 1619544442826
65
66
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
67
+ ulid.timestamp #=> "01F4A5Y1YA"
68
+ ulid.randomness #=> "QCYAYCTC7GRMJ9AA"
69
+ ulid.to_i #=> 1957909092946624190749577070267409738
66
70
  ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
67
71
  ```
68
72
 
@@ -86,15 +90,16 @@ ulids.uniq(&:to_time).size #=> 1000
86
90
  ulids.sort == ulids #=> true
87
91
  ```
88
92
 
89
- `ULID.generate` can take fixed `Time` instance
93
+ `ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`
90
94
 
91
95
  ```ruby
92
96
  time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
93
97
  ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
94
98
  ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
99
+ ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
95
100
 
96
101
  ulids = 1000.times.map do |n|
97
- ULID.generate(moment: time + n)
102
+ ULID.at(time + n)
98
103
  end
99
104
  ulids.sort == ulids #=> true
100
105
  ```
@@ -173,6 +178,13 @@ ulids.grep_v(one_of_the_above)
173
178
  #=> I hope the results should be actually you want!
174
179
  ```
175
180
 
181
+ If you want to manually handle the Time objects, `ULID.floor` returns new `Time` with truncating excess precisions in ULID spec.
182
+
183
+ ```ruby
184
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
185
+ ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
186
+ ```
187
+
176
188
  ### Scanner for string (e.g. `JSON`)
177
189
 
178
190
  For rough operations, `ULID.scan` might be useful.
@@ -215,6 +227,18 @@ ULID.scan(json).to_a
215
227
  # ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
216
228
  ```
217
229
 
230
+ `ULID#patterns` is a util for text based operations.
231
+ The results and spec are not fixed. Should not be used except snippets/console operation
232
+
233
+ ```ruby
234
+ ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
235
+ #=> returns like a fallowing Hash
236
+ {
237
+ named_captures: /(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)/i,
238
+ strict_named_captures: /\A(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)\z/i
239
+ }
240
+ ```
241
+
218
242
  ### Some methods to help manipulations
219
243
 
220
244
  `ULID.min` and `ULID.max` return termination values for ULID spec.
@@ -266,9 +290,12 @@ ULID.sample(5)
266
290
  ### UUIDv4 converter for migration use-cases
267
291
 
268
292
  `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
269
- The imported timestamp is meaningless. So ULID's benefit will lost
293
+ The imported timestamp is meaningless. So ULID's benefit will lost.
270
294
 
271
295
  ```ruby
296
+ # Currently experimental feature, so needed to load the extension.
297
+ require 'ulid/uuid'
298
+
272
299
  # Basically reversible
273
300
  ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
274
301
  ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
@@ -322,7 +349,7 @@ Major methods can be replaced as below.
322
349
  -ULID.generate
323
350
  +ULID.generate.to_s
324
351
  -ULID.at(time)
325
- +ULID.generate(moment: time).to_s
352
+ +ULID.at(time).to_s
326
353
  -ULID.time(string)
327
354
  +ULID.parse(string).to_time
328
355
  -ULID.min_ulid_at(time)
data/lib/ulid.rb CHANGED
@@ -17,33 +17,44 @@ class ULID
17
17
  class ParserError < Error; end
18
18
  class SetupError < ScriptError; end
19
19
 
20
+ # `Subset` of Crockford's Base32. Just excluded I, L, O, U, -.
21
+ # refs:
22
+ # * https://www.crockford.com/base32.html
23
+ # * https://github.com/ulid/spec/pull/57
24
+ # * https://github.com/kachick/ruby-ulid/issues/57
20
25
  encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
- # Crockford's Base32. Excluded I, L, O, U.
22
- # @see https://www.crockford.com/base32.html
23
- ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
26
+ encoding_chars = encoding_string.chars.map(&:freeze).freeze
24
27
 
25
- TIMESTAMP_PART_LENGTH = 10
26
- RANDOMNESS_PART_LENGTH = 16
27
- ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
+ TIMESTAMP_ENCODED_LENGTH = 10
29
+ RANDOMNESS_ENCODED_LENGTH = 16
30
+ ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
28
31
  TIMESTAMP_OCTETS_LENGTH = 6
29
32
  RANDOMNESS_OCTETS_LENGTH = 10
30
33
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
34
  MAX_MILLISECONDS = 281474976710655
32
35
  MAX_ENTROPY = 1208925819614629174706175
33
36
  MAX_INTEGER = 340282366920938463463374607431768211455
34
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
37
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
35
38
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
39
 
37
- # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
- 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
39
-
40
40
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
41
  # @see https://bugs.ruby-lang.org/issues/15958
42
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
43
43
 
44
44
  UNDEFINED = BasicObject.new
45
+ # @return [String]
46
+ def UNDEFINED.to_s
47
+ 'ULID::UNDEFINED'
48
+ end
49
+
50
+ # @return [String]
51
+ def UNDEFINED.inspect
52
+ to_s
53
+ end
45
54
  Kernel.instance_method(:freeze).bind(UNDEFINED).call
46
55
 
56
+ private_class_method :new
57
+
47
58
  # @param [Integer, Time] moment
48
59
  # @param [Integer] entropy
49
60
  # @return [ULID]
@@ -51,6 +62,14 @@ class ULID
51
62
  new milliseconds: milliseconds_from_moment(moment), entropy: entropy
52
63
  end
53
64
 
65
+ # Short hand of `ULID.generate(moment: time)`
66
+ # @param [Time] time
67
+ # @return [ULID]
68
+ def self.at(time)
69
+ raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
70
+ new milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy
71
+ end
72
+
54
73
  # @param [Integer, Time] moment
55
74
  # @return [ULID]
56
75
  def self.min(moment: 0)
@@ -101,38 +120,23 @@ class ULID
101
120
  self
102
121
  end
103
122
 
104
- # @param [String, #to_str] uuid
105
- # @return [ULID]
106
- # @raise [ParserError] if the given format is not correct for UUIDv4 specs
107
- def self.from_uuidv4(uuid)
108
- begin
109
- uuid = uuid.to_str
110
- prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
111
- raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
112
- normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
113
- from_integer(normalized.to_i(16))
114
- rescue => err
115
- raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
116
- end
117
- end
118
-
119
123
  # @param [Integer, #to_int] integer
120
124
  # @return [ULID]
121
125
  # @raise [OverflowError] if the given integer is larger than the ULID limit
122
126
  # @raise [ArgumentError] if the given integer is negative number
123
- # @todo Need optimized for performance
124
127
  def self.from_integer(integer)
125
128
  integer = integer.to_int
126
129
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
127
130
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
128
131
 
129
- octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
130
- time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
131
- randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
132
- milliseconds = inverse_of_digits(time_octets)
133
- entropy = inverse_of_digits(randomness_octets)
132
+ n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
133
+ n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
134
+ n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
135
+
136
+ milliseconds = n32encoded_timestamp.to_i(32)
137
+ entropy = n32encoded_randomness.to_i(32)
134
138
 
135
- new milliseconds: milliseconds, entropy: entropy
139
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
136
140
  end
137
141
 
138
142
  # @param [Range<Time>, Range<nil>] time_range
@@ -239,7 +243,7 @@ class ULID
239
243
  'Z' => 31
240
244
  }.freeze
241
245
 
242
- N32_CHAR_BY_CROCKFORD_BASE32_CHAR = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
246
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR = encoding_chars.each_with_object({}) do |encoding_char, map|
243
247
  if n = crockford_base32_mappings[encoding_char]
244
248
  char_32 = n32_char_by_number.fetch(n)
245
249
  map[encoding_char] = char_32
@@ -258,21 +262,12 @@ class ULID
258
262
  begin
259
263
  string = string.to_str
260
264
  raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
261
- n32encoded = convert_crockford_base32_to_n32(string.upcase)
262
- timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
263
- randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
264
- milliseconds = timestamp.to_i(32)
265
- entropy = randomness.to_i(32)
266
265
  rescue => err
267
266
  raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
268
267
  end
269
268
 
270
- new milliseconds: milliseconds, entropy: entropy
271
- end
272
-
273
- # @api private
274
- private_class_method def self.convert_crockford_base32_to_n32(string)
275
- string.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
269
+ n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
270
+ from_integer(n32encoded.to_i(32))
276
271
  end
277
272
 
278
273
  # @return [Boolean]
@@ -284,18 +279,6 @@ class ULID
284
279
  true
285
280
  end
286
281
 
287
- # @api private
288
- # @param [Integer] integer
289
- # @param [Integer] length
290
- # @return [Array<Integer>]
291
- def self.octets_from_integer(integer, length:)
292
- digits = integer.digits(256)
293
- (length - digits.size).times do
294
- digits.push 0
295
- end
296
- digits.reverse!
297
- end
298
-
299
282
  # @api private
300
283
  # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
301
284
  # @param [Array<Integer>] reversed_digits
@@ -309,6 +292,15 @@ class ULID
309
292
  num
310
293
  end
311
294
 
295
+ # @api private
296
+ # @param [MonotonicGenerator] generator
297
+ # @return [ULID]
298
+ def self.from_monotonic_generator(generator)
299
+ raise ArgumentError, 'this method provided only for MonotonicGenerator' unless MonotonicGenerator === generator
300
+ new milliseconds: generator.latest_milliseconds, entropy: generator.latest_entropy
301
+ end
302
+
303
+ # @api private
312
304
  # @return [ArgumentError]
313
305
  private_class_method def self.argument_error_for_range_building(argument)
314
306
  ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
@@ -319,15 +311,21 @@ class ULID
319
311
  # @api private
320
312
  # @param [Integer] milliseconds
321
313
  # @param [Integer] entropy
314
+ # @param [Integer] integer
322
315
  # @return [void]
323
316
  # @raise [OverflowError] if the given value is larger than the ULID limit
324
317
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
325
- def initialize(milliseconds:, entropy:)
326
- milliseconds = milliseconds.to_int
327
- entropy = entropy.to_int
328
- raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
329
- raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
330
- raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
318
+ def initialize(milliseconds:, entropy:, integer: UNDEFINED)
319
+ if UNDEFINED.equal?(integer)
320
+ milliseconds = milliseconds.to_int
321
+ entropy = entropy.to_int
322
+
323
+ raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
324
+ raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
325
+ raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
326
+ else
327
+ @integer = integer
328
+ end
331
329
 
332
330
  @milliseconds = milliseconds
333
331
  @entropy = entropy
@@ -335,18 +333,22 @@ class ULID
335
333
 
336
334
  # @return [String]
337
335
  def to_s
338
- @string ||= convert_n32_to_crockford_base32(to_i.to_s(32).rjust(ENCODED_ID_LENGTH, '0').upcase).freeze
336
+ @string ||= to_i.to_s(32).upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0').freeze
339
337
  end
340
338
 
341
339
  # @return [Integer]
342
340
  def to_i
343
- @integer ||= self.class.inverse_of_digits(octets)
341
+ @integer ||= begin
342
+ n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
343
+ n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
344
+ (n32encoded_timestamp + n32encoded_randomness).to_i(32)
345
+ end
344
346
  end
345
347
  alias_method :hash, :to_i
346
348
 
347
349
  # @return [Integer, nil]
348
350
  def <=>(other)
349
- other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
351
+ (ULID === other) ? (to_i <=> other.to_i) : nil
350
352
  end
351
353
 
352
354
  # @return [String]
@@ -356,7 +358,7 @@ class ULID
356
358
 
357
359
  # @return [Boolean]
358
360
  def eql?(other)
359
- other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
361
+ equal?(other) || (ULID === other && to_i == other.to_i)
360
362
  end
361
363
  alias_method :==, :eql?
362
364
 
@@ -389,39 +391,49 @@ class ULID
389
391
 
390
392
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
391
393
  def octets
392
- @octets ||= (timestamp_octets + randomness_octets).freeze
394
+ @octets ||= octets_from_integer(to_i).freeze
393
395
  end
394
396
 
395
397
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
396
398
  def timestamp_octets
397
- @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
399
+ @timestamp_octets ||= octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
398
400
  end
399
401
 
400
402
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
401
403
  def randomness_octets
402
- @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
404
+ @randomness_octets ||= octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
403
405
  end
404
406
 
405
407
  # @return [String]
406
408
  def timestamp
407
- @timestamp ||= matchdata[:timestamp].freeze
409
+ @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
408
410
  end
409
411
 
410
412
  # @return [String]
411
413
  def randomness
412
- @randomness ||= matchdata[:randomness].freeze
414
+ @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
415
+ end
416
+
417
+ # @note Providing for rough operations. The keys and values is not fixed.
418
+ # @return [Hash{Symbol => Regexp, String}]
419
+ def patterns
420
+ named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
421
+ {
422
+ named_captures: named_captures,
423
+ strict_named_captures: /\A#{named_captures.source}\z/i.freeze
424
+ }
413
425
  end
414
426
 
415
- # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
427
+ # @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
416
428
  # @return [Regexp]
417
429
  def pattern
418
- @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
430
+ patterns.fetch(:named_captures)
419
431
  end
420
432
 
421
- # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
433
+ # @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
422
434
  # @return [Regexp]
423
435
  def strict_pattern
424
- @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
436
+ patterns.fetch(:strict_named_captures)
425
437
  end
426
438
 
427
439
  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
@@ -439,17 +451,6 @@ class ULID
439
451
  @pred ||= self.class.from_integer(pre_int)
440
452
  end
441
453
 
442
- # @return [String]
443
- def to_uuidv4
444
- @uuidv4 ||= begin
445
- # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
446
- array = octets.pack('C*').unpack('NnnnnN')
447
- array[2] = (array[2] & 0x0fff) | 0x4000
448
- array[3] = (array[3] & 0x3fff) | 0x8000
449
- ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
450
- end
451
- end
452
-
453
454
  # @return [self]
454
455
  def freeze
455
456
  # Need to cache before freezing, because frozen objects can't assign instance variables
@@ -459,14 +460,14 @@ class ULID
459
460
 
460
461
  private
461
462
 
462
- # @api private
463
- def convert_n32_to_crockford_base32(string)
464
- string.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR)
465
- end
466
-
467
- # @return [MatchData]
468
- def matchdata
469
- @matchdata ||= STRICT_PATTERN.match(to_s).freeze
463
+ # @param [Integer] integer
464
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
465
+ def octets_from_integer(integer)
466
+ digits = integer.digits(256)
467
+ (OCTETS_LENGTH - digits.size).times do
468
+ digits.push 0
469
+ end
470
+ digits.reverse!
470
471
  end
471
472
 
472
473
  # @return [void]
@@ -476,8 +477,8 @@ class ULID
476
477
  to_i
477
478
  succ
478
479
  pred
479
- strict_pattern
480
- to_uuidv4
480
+ timestamp
481
+ randomness
481
482
  end
482
483
  end
483
484
 
@@ -488,5 +489,5 @@ class ULID
488
489
  MIN = parse('00000000000000000000000000').freeze
489
490
  MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
490
491
 
491
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :CROCKFORD_BASE32_CHAR_PATTERN, :N32_CHAR_BY_CROCKFORD_BASE32_CHAR, :CROCKFORD_BASE32_CHAR_BY_N32_CHAR, :N32_CHAR_PATTERN, :UNDEFINED
492
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :CROCKFORD_BASE32_CHAR_PATTERN, :N32_CHAR_BY_CROCKFORD_BASE32_CHAR, :CROCKFORD_BASE32_CHAR_BY_N32_CHAR, :N32_CHAR_PATTERN, :UNDEFINED
492
493
  end
@@ -26,7 +26,7 @@ class ULID
26
26
  @latest_entropy += 1
27
27
  end
28
28
 
29
- ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
29
+ ULID.from_monotonic_generator(self)
30
30
  end
31
31
 
32
32
  # @api private
data/lib/ulid/uuid.rb ADDED
@@ -0,0 +1,37 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ # Extracted features around UUID from some reasons
6
+ # ref:
7
+ # * https://github.com/kachick/ruby-ulid/issues/105
8
+ # * https://github.com/kachick/ruby-ulid/issues/76
9
+ class ULID
10
+ # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
11
+ 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
12
+ private_constant :UUIDV4_PATTERN
13
+
14
+ # @param [String, #to_str] uuid
15
+ # @return [ULID]
16
+ # @raise [ParserError] if the given format is not correct for UUIDv4 specs
17
+ def self.from_uuidv4(uuid)
18
+ begin
19
+ uuid = uuid.to_str
20
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
21
+ raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
22
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
23
+ from_integer(normalized.to_i(16))
24
+ rescue => err
25
+ raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
26
+ end
27
+ end
28
+
29
+ # @return [String]
30
+ def to_uuidv4
31
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
32
+ array = octets.pack('C*').unpack('NnnnnN')
33
+ array[2] = (array[2] & 0x0fff) | 0x4000
34
+ array[3] = (array[3] & 0x3fff) | 0x8000
35
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
36
+ end
37
+ end
data/lib/ulid/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  class ULID
5
- VERSION = '0.0.17'
5
+ VERSION = '0.0.18'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -1,10 +1,9 @@
1
1
  # Classes
2
2
  class ULID
3
3
  VERSION: String
4
- ENCODING_CHARS: Array[String]
5
- TIMESTAMP_PART_LENGTH: 10
6
- RANDOMNESS_PART_LENGTH: 16
7
- ENCODED_ID_LENGTH: 26
4
+ TIMESTAMP_ENCODED_LENGTH: 10
5
+ RANDOMNESS_ENCODED_LENGTH: 16
6
+ ENCODED_LENGTH: 26
8
7
  TIMESTAMP_OCTETS_LENGTH: 6
9
8
  RANDOMNESS_OCTETS_LENGTH: 10
10
9
  OCTETS_LENGTH: 16
@@ -64,12 +63,9 @@ class ULID
64
63
  @inspect: String?
65
64
  @time: Time?
66
65
  @next: ULID?
67
- @pattern: Regexp?
68
- @strict_pattern: Regexp?
69
- @uuidv4: String?
70
- @matchdata: MatchData?
71
66
 
72
67
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
68
+ def self.at: (Time time) -> ULID
73
69
  def self.current_milliseconds: -> Integer
74
70
  def self.milliseconds_from_time: (Time time) -> Integer
75
71
  def self.milliseconds_from_moment: (moment moment) -> Integer
@@ -86,16 +82,17 @@ class ULID
86
82
  def self.valid?: (untyped string) -> bool
87
83
  def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
88
84
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
89
- def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
85
+ def self.octets_from_integer: (Integer integer) -> octets
90
86
  def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
87
+ def self.from_monotonic_generator: (MonotonicGenerator generator) -> ULID
91
88
  attr_reader milliseconds: Integer
92
89
  attr_reader entropy: Integer
93
- def initialize: (milliseconds: Integer, entropy: Integer) -> void
90
+ def initialize: (milliseconds: Integer, entropy: Integer, ?integer: Integer) -> void
94
91
  def to_s: -> String
95
92
  def to_i: -> Integer
96
93
  alias hash to_i
97
94
  def <=>: (ULID other) -> Integer
98
- | (untyped other) -> Integer?
95
+ | (untyped other) -> nil
99
96
  def inspect: -> String
100
97
  def eql?: (untyped other) -> bool
101
98
  alias == eql?
@@ -103,6 +100,7 @@ class ULID
103
100
  def to_time: -> Time
104
101
  def timestamp: -> String
105
102
  def randomness: -> String
103
+ def patterns: -> Hash[Symbol, Regexp | String]
106
104
  def pattern: -> Regexp
107
105
  def strict_pattern: -> Regexp
108
106
  def octets: -> octets
@@ -115,9 +113,6 @@ class ULID
115
113
  def freeze: -> self
116
114
 
117
115
  private
118
- def self.convert_crockford_base32_to_n32: (String) -> String
119
116
  def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
120
- def convert_n32_to_crockford_base32: (String) -> String
121
- def matchdata: -> MatchData
122
117
  def cache_all_instance_variables: -> void
123
118
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.17
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Kamiya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-06 00:00:00.000000000 Z
11
+ date: 2021-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbs
@@ -78,6 +78,7 @@ files:
78
78
  - README.md
79
79
  - lib/ulid.rb
80
80
  - lib/ulid/monotonic_generator.rb
81
+ - lib/ulid/uuid.rb
81
82
  - lib/ulid/version.rb
82
83
  - sig/ulid.rbs
83
84
  homepage: https://github.com/kachick/ruby-ulid