ruby-ulid 0.0.8 → 0.0.9

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ulid.rb +110 -31
  3. data/lib/ulid/version.rb +1 -1
  4. data/sig/ulid.rbs +33 -13
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1179379859a437f04ff904c941372fffc4aa3027d550ae50c67f744e4899d9a
4
- data.tar.gz: 71781ee8481373bb7229f10f88a1703b2653c11a4f18cbc7b50eb4adbec58908
3
+ metadata.gz: cc7674472c14c213d6fe01c6994520b33bc82ab164b538948a4f1b532d0d88f2
4
+ data.tar.gz: 85987c59ca6f119d40caaf69cc830a8e1b5cc2f9ea3250c315be2f9d8b7faa09
5
5
  SHA512:
6
- metadata.gz: 6d48e489221851290a486ffe3a39847196e900bb54751fa7d0c808f7580c8838d1a6382b203da1b90ad4f74dd9e998239e0a6c996b06d99881c39489ff607697
7
- data.tar.gz: fbef689db966901800b24e18820ee6744aeeaeb2476f14349b74d634ed1e5d359a4bff4abb3eb0eee94e4d0183f5db1420719d7e94afd3e803d21d62510087d7
6
+ metadata.gz: f56db0a74a7a6559189039a9f7f13a1f514b4fc956f7993a52ca16a7a978b4b93aa352b9f74b72da73c7b0485ebe1f80052d2e3c7b04a1baba231d06ba594f42
7
+ data.tar.gz: 597260a0eb43c0fdc187aeb961521fbce22c68fc75b35d445e73d60564b72208aaf1e83a8428daa99423e001572f187872f4077445d6bced8b35d197d7e700ff
data/lib/ulid.rb CHANGED
@@ -18,18 +18,25 @@ class ULID
18
18
  class OverflowError < Error; end
19
19
  class ParserError < Error; end
20
20
 
21
+ encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
22
  # Crockford's Base32. Excluded I, L, O, U.
22
23
  # @see https://www.crockford.com/base32.html
23
- ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.chars.map(&:freeze).freeze
24
+ ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
24
25
 
25
26
  TIME_PART_LENGTH = 10
26
27
  RANDOMNESS_PART_LENGTH = 16
27
28
  ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
- TIME_OCTETS_LENGTH = 6
29
+ TIMESTAMP_OCTETS_LENGTH = 6
29
30
  RANDOMNESS_OCTETS_LENGTH = 10
30
- OCTETS_LENGTH = TIME_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
+ OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
32
  MAX_MILLISECONDS = 281474976710655
32
33
  MAX_ENTROPY = 1208925819614629174706175
34
+ MAX_INTEGER = 340282366920938463463374607431768211455
35
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIME_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
36
+ STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
37
+
38
+ # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
39
+ 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
33
40
 
34
41
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
35
42
  # @see https://bugs.ruby-lang.org/issues/15958
@@ -76,7 +83,7 @@ class ULID
76
83
 
77
84
  MONOTONIC_GENERATOR = MonotonicGenerator.new
78
85
 
79
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT
86
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
80
87
 
81
88
  # @param [Integer, Time] moment
82
89
  # @param [Integer] entropy
@@ -100,6 +107,52 @@ class ULID
100
107
  MONOTONIC_GENERATOR.generate
101
108
  end
102
109
 
110
+ # @param [String, #to_str] string
111
+ # @return [Enumerator]
112
+ # @yieldparam [ULID] ulid
113
+ # @yieldreturn [self]
114
+ def self.scan(string)
115
+ string = string.to_str
116
+ return to_enum(__callee__, string) unless block_given?
117
+ string.scan(PATTERN) do |pair|
118
+ yield parse(pair.join)
119
+ end
120
+ self
121
+ end
122
+
123
+ # @param [String, #to_str] uuid
124
+ # @return [ULID]
125
+ # @raise [ParserError] if the given format is not correct for UUIDv4 specs
126
+ def self.from_uuidv4(uuid)
127
+ begin
128
+ uuid = uuid.to_str
129
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
130
+ raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
131
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
132
+ from_integer(normalized.to_i(16))
133
+ rescue => err
134
+ raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
135
+ end
136
+ end
137
+
138
+ # @param [Integer, #to_int] integer
139
+ # @return [ULID]
140
+ # @raise [OverflowError] if the given integer is larger than the ULID limit
141
+ # @raise [ArgumentError] if the given integer is negative number
142
+ def self.from_integer(integer)
143
+ integer = integer.to_int
144
+ raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
145
+ raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
146
+
147
+ octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
148
+ time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
149
+ randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
150
+ milliseconds = inverse_of_digits(time_octets)
151
+ entropy = inverse_of_digits(randomness_octets)
152
+
153
+ new milliseconds: milliseconds, entropy: entropy
154
+ end
155
+
103
156
  # @return [Integer]
104
157
  def self.current_milliseconds
105
158
  time_to_milliseconds(Time.now)
@@ -146,6 +199,29 @@ class ULID
146
199
  true
147
200
  end
148
201
 
202
+ # @param [Integer] integer
203
+ # @param [Integer] length
204
+ # @return [Array<Integer>]
205
+ def self.octets_from_integer(integer, length:)
206
+ digits = integer.digits(256)
207
+ (length - digits.size).times do
208
+ digits.push 0
209
+ end
210
+ digits.reverse!
211
+ end
212
+
213
+ # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
214
+ # @param [Array<Integer>] reversed_digits
215
+ # @return [Integer]
216
+ def self.inverse_of_digits(reversed_digits)
217
+ base = 256
218
+ num = 0
219
+ reversed_digits.each do |digit|
220
+ num = (num * base) + digit
221
+ end
222
+ num
223
+ end
224
+
149
225
  attr_reader :milliseconds, :entropy
150
226
 
151
227
  # @param [Integer] milliseconds
@@ -172,7 +248,7 @@ class ULID
172
248
 
173
249
  # @return [Integer]
174
250
  def to_i
175
- @integer ||= inverse_of_digits(octets)
251
+ @integer ||= self.class.inverse_of_digits(octets)
176
252
  end
177
253
  alias_method :hash, :to_i
178
254
 
@@ -210,22 +286,42 @@ class ULID
210
286
 
211
287
  # @return [Time]
212
288
  def to_time
213
- @time ||= Time.at(0, @milliseconds, :millisecond).utc
289
+ @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
214
290
  end
215
291
 
216
292
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
217
293
  def octets
218
- @octets ||= (time_octets + randomness_octets).freeze
294
+ @octets ||= (timestamp_octets + randomness_octets).freeze
219
295
  end
220
296
 
221
297
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
222
- def time_octets
223
- @time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
298
+ def timestamp_octets
299
+ @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
224
300
  end
225
301
 
226
302
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
227
303
  def randomness_octets
228
- @randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
304
+ @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
305
+ end
306
+
307
+ # @return [String]
308
+ def timestamp
309
+ @timestamp ||= matchdata[:timestamp].freeze
310
+ end
311
+
312
+ # @return [String]
313
+ def randomness
314
+ @randomness ||= matchdata[:randomness].freeze
315
+ end
316
+
317
+ # @return [Regexp]
318
+ def pattern
319
+ @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
320
+ end
321
+
322
+ # @return [Regexp]
323
+ def strict_pattern
324
+ @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
229
325
  end
230
326
 
231
327
  # @raise [OverflowError] if the next entropy part is larger than the ULID limit
@@ -242,31 +338,14 @@ class ULID
242
338
  octets
243
339
  succ
244
340
  to_i
341
+ strict_pattern
245
342
  super
246
343
  end
247
344
 
248
345
  private
249
346
 
250
- # @param [Integer] integer
251
- # @param [Integer] length
252
- # @return [Array<Integer>]
253
- def octets_from_integer(integer, length:)
254
- digits = integer.digits(256)
255
- (length - digits.size).times do
256
- digits.push 0
257
- end
258
- digits.reverse!
259
- end
260
-
261
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
262
- # @param [Array<Integer>] reversed_digits
263
- # @return [Integer]
264
- def inverse_of_digits(reversed_digits)
265
- base = 256
266
- num = 0
267
- reversed_digits.each do |digit|
268
- num = (num * base) + digit
269
- end
270
- num
347
+ # @return [MatchData]
348
+ def matchdata
349
+ @matchdata ||= STRICT_PATTERN.match(to_str).freeze
271
350
  end
272
351
  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.8'
5
+ VERSION = '0.0.9'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -5,12 +5,16 @@ class ULID
5
5
  TIME_PART_LENGTH: 10
6
6
  RANDOMNESS_PART_LENGTH: 16
7
7
  ENCODED_ID_LENGTH: 26
8
- TIME_OCTETS_LENGTH: 6
8
+ TIMESTAMP_OCTETS_LENGTH: 6
9
9
  RANDOMNESS_OCTETS_LENGTH: 10
10
10
  OCTETS_LENGTH: 16
11
11
  MAX_MILLISECONDS: 281474976710655
12
12
  MAX_ENTROPY: 1208925819614629174706175
13
+ MAX_INTEGER: 340282366920938463463374607431768211455
13
14
  TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
15
+ PATTERN: Regexp
16
+ STRICT_PATTERN: Regexp
17
+ UUIDV4_PATTERN: Regexp
14
18
  MONOTONIC_GENERATOR: MonotonicGenerator
15
19
  include Comparable
16
20
 
@@ -33,17 +37,24 @@ class ULID
33
37
  end
34
38
 
35
39
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
36
- type time_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
40
+ type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
37
41
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
38
42
 
39
- @string: String
40
- @integer: Integer
41
- @octets: octets
42
- @time_octets: time_octets
43
- @randomness_octets: randomness_octets
44
- @inspect: String
45
- @time: Time
46
- @next: ULID
43
+ @milliseconds: Integer
44
+ @entropy: Integer
45
+ @string: String?
46
+ @integer: Integer?
47
+ @octets: octets?
48
+ @timestamp_octets: timestamp_octets?
49
+ @randomness_octets: randomness_octets?
50
+ @timestamp: String?
51
+ @randomness: String?
52
+ @inspect: String?
53
+ @time: Time?
54
+ @next: ULID?
55
+ @pattern: Regexp?
56
+ @strict_pattern: Regexp?
57
+ @matchdata: MatchData?
47
58
 
48
59
  def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
49
60
  def self.monotonic_generate: -> ULID
@@ -51,7 +62,13 @@ class ULID
51
62
  def self.time_to_milliseconds: (Time time) -> Integer
52
63
  def self.reasonable_entropy: -> Integer
53
64
  def self.parse: (String string) -> ULID
65
+ def self.from_uuidv4: (String uuid) -> ULID
66
+ def self.from_integer: (Integer integer) -> ULID
54
67
  def self.valid?: (untyped string) -> bool
68
+ def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
69
+ | (String string) { (ULID ulid) -> void } -> singleton(ULID)
70
+ def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
71
+ def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
55
72
  attr_reader milliseconds: Integer
56
73
  attr_reader entropy: Integer
57
74
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
@@ -66,14 +83,17 @@ class ULID
66
83
  alias == eql?
67
84
  def ===: (untyped other) -> bool
68
85
  def to_time: -> Time
86
+ def timestamp: -> String
87
+ def randomness: -> String
88
+ def pattern: -> Regexp
89
+ def strict_pattern: -> Regexp
69
90
  def octets: -> octets
70
- def time_octets: -> time_octets
91
+ def timestamp_octets: -> timestamp_octets
71
92
  def randomness_octets: -> randomness_octets
72
93
  def next: -> ULID
73
94
  alias succ next
74
95
  def freeze: -> self
75
96
 
76
97
  private
77
- def octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
78
- def inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
98
+ def matchdata: -> MatchData
79
99
  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.8
4
+ version: 0.0.9
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-04-29 00:00:00.000000000 Z
11
+ date: 2021-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base