ruby-ulid 0.0.18 → 0.0.19

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: 8b1641b0894c0140c3a9c6f59fa8abcfca386362ad1aaa92fe196f3bf5f75cf1
4
- data.tar.gz: 932fa04dceb504330289d4236f9670fecd702989504bdd402873d60c0bb1c0e5
3
+ metadata.gz: 646d2a4b433ffbe28d4801483d3f1c43cfb5839bf5f80e759447304a1c8dad52
4
+ data.tar.gz: 1e4ddc37266eb09f3635ba5509e66e6ff93e15fad3e6574fb3b50e8ad9322b10
5
5
  SHA512:
6
- metadata.gz: f073c7b240ef96b511c84c6bc5649c483fc97c62a3892f58531b6f36e8235e49dcd865433fe522eec48a3532c98c4deec8713f453a4b2a117157788eabddaf6b
7
- data.tar.gz: a2486bbefd62d0d019c15044c9d67b6aabc33e370f4ee88239c1e2b33a40738c736fcc9221dd3ce457eabf83a3580be5dc501fef0c359345a453331bdd62d139
6
+ metadata.gz: 9a564dbad8c3c88b729353826c599e618cd8963806759ce8a6bf041a95aef60044c8f75873560129bcef7550eec226c3bf04bcfec339c4b07fb72c3372342811
7
+ data.tar.gz: 187c475f9332cb69827e2664193faa7f001cb14a5709b3c28600286ac9e6c1fb3176e14e3a7ae3a658d232af6157a3dea83ad992cee75db966ff80213c3a0e52
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.18'
52
+ gem 'ruby-ulid', '0.0.19'
53
53
  ```
54
54
 
55
55
  ### Generator and Parser
@@ -271,7 +271,9 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
271
271
  ULID.parse('00000000000000000000000000').pred #=> nil
272
272
  ```
273
273
 
274
- `ULID.sample` returns random ULIDs ignoring the generating time
274
+ `ULID.sample` returns random ULIDs.
275
+
276
+ Basically ignores generating time.
275
277
 
276
278
  ```ruby
277
279
  ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
@@ -287,6 +289,39 @@ ULID.sample(5)
287
289
  # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
288
290
  ```
289
291
 
292
+ You can specify a range object for the timestamp restriction, see also `ULID.range`.
293
+
294
+ ```ruby
295
+ ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
296
+ ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
297
+ ulids = ULID.sample(1000, period: ulid1..ulid2)
298
+ ulids.uniq.size #=> 1000
299
+ ulids.take(10)
300
+ #=>
301
+ #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
302
+ # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
303
+ # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
304
+ # ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
305
+ # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
306
+ # ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
307
+ # ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
308
+ # ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
309
+ # ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
310
+ # ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
311
+ ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
312
+ #=>
313
+ # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
314
+ # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
315
+ # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
316
+ # ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
317
+ # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
318
+ # ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
319
+ # ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
320
+ # ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
321
+ # ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
322
+ # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
323
+ ```
324
+
290
325
  ### UUIDv4 converter for migration use-cases
291
326
 
292
327
  `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
data/lib/ulid.rb CHANGED
@@ -15,15 +15,11 @@ class ULID
15
15
  class Error < StandardError; end
16
16
  class OverflowError < Error; end
17
17
  class ParserError < Error; end
18
- class SetupError < ScriptError; end
19
18
 
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
25
- encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
26
- encoding_chars = encoding_string.chars.map(&:freeze).freeze
19
+ # Excluded I, L, O, U, -.
20
+ # This is the encoding patterns.
21
+ # The decoding issue is written in ULID::CrockfordBase32
22
+ CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
27
23
 
28
24
  TIMESTAMP_ENCODED_LENGTH = 10
29
25
  RANDOMNESS_ENCODED_LENGTH = 16
@@ -34,25 +30,21 @@ class ULID
34
30
  MAX_MILLISECONDS = 281474976710655
35
31
  MAX_ENTROPY = 1208925819614629174706175
36
32
  MAX_INTEGER = 340282366920938463463374607431768211455
37
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
38
- STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
33
+
34
+ # @see https://github.com/ulid/spec/pull/57
35
+ # Currently not used as a constant, but kept as a reference for now.
36
+ PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
37
+
38
+ STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
39
+
40
+ # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
41
+ # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
42
+ SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
39
43
 
40
44
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
45
  # @see https://bugs.ruby-lang.org/issues/15958
42
46
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
43
47
 
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
54
- Kernel.instance_method(:freeze).bind(UNDEFINED).call
55
-
56
48
  private_class_method :new
57
49
 
58
50
  # @param [Integer, Time] moment
@@ -82,28 +74,54 @@ class ULID
82
74
  MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
83
75
  end
84
76
 
85
- # @param [Integer] number
86
- # @return [ULID, Array<ULID>]
87
- # @raise [ArgumentError] if the given number is lager than ULID spec limits or given negative number
77
+ RANDOM_INTEGER_GENERATOR = -> {
78
+ SecureRandom.random_number(MAX_INTEGER)
79
+ }
80
+
81
+ # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
82
+ # @overload sample(number, period: nil)
83
+ # @param [Integer] number
84
+ # @return [Array<ULID>]
85
+ # @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
86
+ # @overload sample(period: nil)
87
+ # @return [ULID]
88
88
  # @note Major difference of `Array#sample` interface is below
89
89
  # * Do not ensure the uniqueness
90
90
  # * Do not take random generator for the arguments
91
91
  # * Raising error instead of truncating elements for the given number
92
- def self.sample(number=UNDEFINED)
93
- if UNDEFINED.equal?(number)
94
- from_integer(SecureRandom.random_number(MAX_INTEGER))
92
+ def self.sample(*args, period: nil)
93
+ int_generator = if period
94
+ ulid_range = range(period)
95
+ min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
96
+
97
+ possibilities = (max - min) + (exclude_end ? 0 : 1)
98
+ raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
99
+
100
+ -> {
101
+ SecureRandom.random_number(possibilities) + min
102
+ }
95
103
  else
96
- begin
97
- int = number.to_int
98
- rescue
99
- # Can not use `number.to_s` and `number.inspect` for considering BasicObject here
100
- raise TypeError, 'accepts no argument or integer only'
104
+ RANDOM_INTEGER_GENERATOR
105
+ end
106
+
107
+ case args.size
108
+ when 0
109
+ from_integer(int_generator.call)
110
+ when 1
111
+ number = args.first
112
+ raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
113
+
114
+ if number > MAX_INTEGER || number.negative?
115
+ raise ArgumentError, "given number #{number} is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
101
116
  end
102
117
 
103
- if int > MAX_INTEGER || int.negative?
104
- raise ArgumentError, "given number is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
118
+ if period && (number > possibilities)
119
+ raise ArgumentError, "given number #{number} is larger than given possibilities #{possibilities}"
105
120
  end
106
- int.times.map { from_integer(SecureRandom.random_number(MAX_INTEGER)) }
121
+
122
+ Array.new(number) { from_integer(int_generator.call) }
123
+ else
124
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
107
125
  end
108
126
  end
109
127
 
@@ -112,10 +130,11 @@ class ULID
112
130
  # @yieldparam [ULID] ulid
113
131
  # @yieldreturn [self]
114
132
  def self.scan(string)
115
- string = string.to_str
133
+ string = String.try_convert(string)
134
+ raise ArgumentError, 'ULID.scan takes only strings' unless string
116
135
  return to_enum(__callee__, string) unless block_given?
117
- string.scan(PATTERN) do |pair|
118
- yield parse(pair.join)
136
+ string.scan(SCANNING_PATTERN) do |matched|
137
+ yield parse(matched)
119
138
  end
120
139
  self
121
140
  end
@@ -139,35 +158,40 @@ class ULID
139
158
  new milliseconds: milliseconds, entropy: entropy, integer: integer
140
159
  end
141
160
 
142
- # @param [Range<Time>, Range<nil>] time_range
161
+ # @param [Range<Time>, Range<nil>, Range[ULID]] period
143
162
  # @return [Range<ULID>]
144
- # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
145
- def self.range(time_range)
146
- raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
147
- begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
163
+ # @raise [ArgumentError] if the given period is not a `Range[Time]` or `Range[nil]`
164
+ def self.range(period)
165
+ raise ArgumentError, 'ULID.range takes only `Range[Time]` or `Range[nil]`' unless Range === period
166
+ begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
167
+ return period if self === begin_element && self === end_element
148
168
 
149
- case begin_time
169
+ case begin_element
150
170
  when Time
151
- begin_ulid = min(moment: begin_time)
171
+ begin_ulid = min(moment: begin_element)
152
172
  when nil
153
173
  begin_ulid = MIN
174
+ when self
175
+ begin_ulid = begin_element
154
176
  else
155
- raise argument_error_for_range_building(time_range)
177
+ raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
156
178
  end
157
179
 
158
- case end_time
180
+ case end_element
159
181
  when Time
160
182
  if exclude_end
161
- end_ulid = min(moment: end_time)
183
+ end_ulid = min(moment: end_element)
162
184
  else
163
- end_ulid = max(moment: end_time)
185
+ end_ulid = max(moment: end_element)
164
186
  end
165
187
  when nil
166
188
  # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
167
189
  end_ulid = MAX
168
190
  exclude_end = false
191
+ when self
192
+ end_ulid = end_element
169
193
  else
170
- raise argument_error_for_range_building(time_range)
194
+ raise ArgumentError, "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{period.inspect}"
171
195
  end
172
196
 
173
197
  begin_ulid.freeze
@@ -179,6 +203,8 @@ class ULID
179
203
  # @param [Time] time
180
204
  # @return [Time]
181
205
  def self.floor(time)
206
+ raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
207
+
182
208
  if RUBY_VERSION >= '2.7'
183
209
  time.floor(3)
184
210
  else
@@ -192,10 +218,9 @@ class ULID
192
218
  milliseconds_from_time(Time.now)
193
219
  end
194
220
 
195
- # @api private
196
221
  # @param [Time] time
197
222
  # @return [Integer]
198
- def self.milliseconds_from_time(time)
223
+ private_class_method def self.milliseconds_from_time(time)
199
224
  (time.to_r * 1000).to_i
200
225
  end
201
226
 
@@ -203,7 +228,14 @@ class ULID
203
228
  # @param [Time, Integer] moment
204
229
  # @return [Integer]
205
230
  def self.milliseconds_from_moment(moment)
206
- moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
231
+ case moment
232
+ when Integer
233
+ moment
234
+ when Time
235
+ milliseconds_from_time(moment)
236
+ else
237
+ raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
238
+ end
207
239
  end
208
240
 
209
241
  # @api private
@@ -212,84 +244,25 @@ class ULID
212
244
  SecureRandom.random_number(MAX_ENTROPY)
213
245
  end
214
246
 
215
- n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
216
- raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
217
-
218
- n32_char_by_number = {}
219
- n32_chars.each_with_index do |char, index|
220
- n32_char_by_number[index] = char
221
- end
222
- n32_char_by_number.freeze
223
-
224
- # Currently supporting only for `subset for actual use-case`
225
- # See below
226
- # * https://github.com/ulid/spec/pull/57
227
- # * https://github.com/kachick/ruby-ulid/issues/57
228
- # * https://github.com/kachick/ruby-ulid/issues/78
229
- crockford_base32_mappings = {
230
- 'J' => 18,
231
- 'K' => 19,
232
- 'M' => 20,
233
- 'N' => 21,
234
- 'P' => 22,
235
- 'Q' => 23,
236
- 'R' => 24,
237
- 'S' => 25,
238
- 'T' => 26,
239
- 'V' => 27,
240
- 'W' => 28,
241
- 'X' => 29,
242
- 'Y' => 30,
243
- 'Z' => 31
244
- }.freeze
245
-
246
- N32_CHAR_BY_CROCKFORD_BASE32_CHAR = encoding_chars.each_with_object({}) do |encoding_char, map|
247
- if n = crockford_base32_mappings[encoding_char]
248
- char_32 = n32_char_by_number.fetch(n)
249
- map[encoding_char] = char_32
250
- end
251
- end.freeze
252
- raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
253
- CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
254
-
255
- CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
256
- N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
257
-
258
247
  # @param [String, #to_str] string
259
248
  # @return [ULID]
260
249
  # @raise [ParserError] if the given format is not correct for ULID specs
261
250
  def self.parse(string)
262
- begin
263
- string = string.to_str
264
- raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
265
- rescue => err
266
- raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
251
+ string = String.try_convert(string)
252
+ raise ArgumentError, 'ULID.parse takes only strings' unless string
253
+
254
+ unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
255
+ raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
267
256
  end
268
257
 
269
- n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
270
- from_integer(n32encoded.to_i(32))
258
+ from_integer(CrockfordBase32.decode(string))
271
259
  end
272
260
 
261
+ # @param [String, #to_str] string
273
262
  # @return [Boolean]
274
263
  def self.valid?(string)
275
- parse(string)
276
- rescue Exception
277
- false
278
- else
279
- true
280
- end
281
-
282
- # @api private
283
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
284
- # @param [Array<Integer>] reversed_digits
285
- # @return [Integer]
286
- def self.inverse_of_digits(reversed_digits)
287
- base = 256
288
- num = 0
289
- reversed_digits.each do |digit|
290
- num = (num * base) + digit
291
- end
292
- num
264
+ string = String.try_convert(string)
265
+ string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
293
266
  end
294
267
 
295
268
  # @api private
@@ -300,12 +273,6 @@ class ULID
300
273
  new milliseconds: generator.latest_milliseconds, entropy: generator.latest_entropy
301
274
  end
302
275
 
303
- # @api private
304
- # @return [ArgumentError]
305
- private_class_method def self.argument_error_for_range_building(argument)
306
- ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
307
- end
308
-
309
276
  attr_reader :milliseconds, :entropy
310
277
 
311
278
  # @api private
@@ -315,16 +282,16 @@ class ULID
315
282
  # @return [void]
316
283
  # @raise [OverflowError] if the given value is larger than the ULID limit
317
284
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
318
- def initialize(milliseconds:, entropy:, integer: UNDEFINED)
319
- if UNDEFINED.equal?(integer)
285
+ def initialize(milliseconds:, entropy:, integer: nil)
286
+ if integer
287
+ @integer = integer
288
+ else
320
289
  milliseconds = milliseconds.to_int
321
290
  entropy = entropy.to_int
322
291
 
323
292
  raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
324
293
  raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
325
294
  raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
326
- else
327
- @integer = integer
328
295
  end
329
296
 
330
297
  @milliseconds = milliseconds
@@ -333,7 +300,7 @@ class ULID
333
300
 
334
301
  # @return [String]
335
302
  def to_s
336
- @string ||= to_i.to_s(32).upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0').freeze
303
+ @string ||= CrockfordBase32.encode(to_i).freeze
337
304
  end
338
305
 
339
306
  # @return [Integer]
@@ -366,13 +333,9 @@ class ULID
366
333
  def ===(other)
367
334
  case other
368
335
  when ULID
369
- self == other
336
+ to_i == other.to_i
370
337
  when String
371
- begin
372
- self == self.class.parse(other)
373
- rescue Exception
374
- false
375
- end
338
+ to_s == other.upcase
376
339
  else
377
340
  false
378
341
  end
@@ -391,17 +354,21 @@ class ULID
391
354
 
392
355
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
393
356
  def octets
394
- @octets ||= octets_from_integer(to_i).freeze
357
+ digits = to_i.digits(256)
358
+ (OCTETS_LENGTH - digits.size).times do
359
+ digits.push 0
360
+ end
361
+ digits.reverse!
395
362
  end
396
363
 
397
364
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
398
365
  def timestamp_octets
399
- @timestamp_octets ||= octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
366
+ octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
400
367
  end
401
368
 
402
369
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
403
370
  def randomness_octets
404
- @randomness_octets ||= octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
371
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
405
372
  end
406
373
 
407
374
  # @return [String]
@@ -424,31 +391,33 @@ class ULID
424
391
  }
425
392
  end
426
393
 
427
- # @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
428
- # @return [Regexp]
429
- def pattern
430
- patterns.fetch(:named_captures)
431
- end
432
-
433
- # @deprecated Use {#patterns} instead. ref: https://github.com/kachick/ruby-ulid/issues/84
434
- # @return [Regexp]
435
- def strict_pattern
436
- patterns.fetch(:strict_named_captures)
437
- end
438
-
439
394
  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
440
- def next
441
- next_int = to_i.next
442
- return nil if next_int > MAX_INTEGER
443
- @next ||= self.class.from_integer(next_int)
395
+ def succ
396
+ succ_int = to_i.succ
397
+ if succ_int >= MAX_INTEGER
398
+ if succ_int == MAX_INTEGER
399
+ MAX
400
+ else
401
+ nil
402
+ end
403
+ else
404
+ ULID.from_integer(succ_int)
405
+ end
444
406
  end
445
- alias_method :succ, :next
407
+ alias_method :next, :succ
446
408
 
447
409
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
448
410
  def pred
449
- pre_int = to_i.pred
450
- return nil if pre_int.negative?
451
- @pred ||= self.class.from_integer(pre_int)
411
+ pred_int = to_i.pred
412
+ if pred_int <= 0
413
+ if pred_int == 0
414
+ MIN
415
+ else
416
+ nil
417
+ end
418
+ else
419
+ ULID.from_integer(pred_int)
420
+ end
452
421
  end
453
422
 
454
423
  # @return [self]
@@ -460,34 +429,21 @@ class ULID
460
429
 
461
430
  private
462
431
 
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!
471
- end
472
-
473
432
  # @return [void]
474
433
  def cache_all_instance_variables
475
434
  inspect
476
- octets
477
- to_i
478
- succ
479
- pred
480
435
  timestamp
481
436
  randomness
482
437
  end
483
438
  end
484
439
 
485
440
  require_relative 'ulid/version'
441
+ require_relative 'ulid/crockford_base32'
486
442
  require_relative 'ulid/monotonic_generator'
487
443
 
488
444
  class ULID
489
445
  MIN = parse('00000000000000000000000000').freeze
490
446
  MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
491
447
 
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
448
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
493
449
  end
@@ -0,0 +1,68 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ class ULID
6
+ # Currently supporting only for `subset` for actual use-case`
7
+ # Original decoding spec allows other characters.
8
+ # But I think ULID should allow `subset` of Crockford's Base32.
9
+ # See below
10
+ # * https://github.com/ulid/spec/pull/57
11
+ # * https://github.com/kachick/ruby-ulid/issues/57
12
+ # * https://github.com/kachick/ruby-ulid/issues/78
13
+ module CrockfordBase32
14
+ class SetupError < ScriptError; end
15
+
16
+ n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
17
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
18
+
19
+ n32_char_by_number = {}
20
+ n32_chars.each_with_index do |char, index|
21
+ n32_char_by_number[index] = char
22
+ end
23
+ n32_char_by_number.freeze
24
+
25
+ crockford_base32_mappings = {
26
+ 'J' => 18,
27
+ 'K' => 19,
28
+ 'M' => 20,
29
+ 'N' => 21,
30
+ 'P' => 22,
31
+ 'Q' => 23,
32
+ 'R' => 24,
33
+ 'S' => 25,
34
+ 'T' => 26,
35
+ 'V' => 27,
36
+ 'W' => 28,
37
+ 'X' => 29,
38
+ 'Y' => 30,
39
+ 'Z' => 31
40
+ }.freeze
41
+
42
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR = CROCKFORD_BASE32_ENCODING_STRING.chars.map(&:freeze).each_with_object({}) do |encoding_char, map|
43
+ if n = crockford_base32_mappings[encoding_char]
44
+ char_32 = n32_char_by_number.fetch(n)
45
+ map[encoding_char] = char_32
46
+ end
47
+ end.freeze
48
+ raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
49
+ CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
50
+
51
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
52
+ N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
53
+
54
+ # @param [String] string
55
+ # @return [Integer]
56
+ def self.decode(string)
57
+ n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
58
+ n32encoded.to_i(32)
59
+ end
60
+
61
+ # @param [Integer] integer
62
+ # @return [String]
63
+ def self.encode(integer)
64
+ n32encoded = integer.to_s(32)
65
+ n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
66
+ end
67
+ end
68
+ end
@@ -8,6 +8,7 @@ class ULID
8
8
  attr_accessor :latest_milliseconds, :latest_entropy
9
9
 
10
10
  def initialize
11
+ @mutex = Thread::Mutex.new
11
12
  reset
12
13
  end
13
14
 
@@ -19,14 +20,15 @@ class ULID
19
20
  milliseconds = ULID.milliseconds_from_moment(moment)
20
21
  raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
21
22
 
22
- if @latest_milliseconds < milliseconds
23
- @latest_milliseconds = milliseconds
24
- @latest_entropy = ULID.reasonable_entropy
25
- else
26
- @latest_entropy += 1
23
+ @mutex.synchronize do
24
+ if @latest_milliseconds < milliseconds
25
+ @latest_milliseconds = milliseconds
26
+ @latest_entropy = ULID.reasonable_entropy
27
+ else
28
+ @latest_entropy += 1
29
+ end
30
+ ULID.from_monotonic_generator(self)
27
31
  end
28
-
29
- ULID.from_monotonic_generator(self)
30
32
  end
31
33
 
32
34
  # @api private
data/lib/ulid/uuid.rb CHANGED
@@ -15,15 +15,16 @@ class ULID
15
15
  # @return [ULID]
16
16
  # @raise [ParserError] if the given format is not correct for UUIDv4 specs
17
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}"
18
+ uuid = String.try_convert(uuid)
19
+ raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid
20
+
21
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
22
+ unless UUIDV4_PATTERN.match?(prefix_trimmed)
23
+ raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
26
24
  end
25
+
26
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
27
+ from_integer(normalized.to_i(16))
27
28
  end
28
29
 
29
30
  # @return [String]
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.18'
5
+ VERSION = '0.0.19'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -1,6 +1,7 @@
1
1
  # Classes
2
2
  class ULID
3
3
  VERSION: String
4
+ CROCKFORD_BASE32_ENCODING_STRING: String
4
5
  TIMESTAMP_ENCODED_LENGTH: 10
5
6
  RANDOMNESS_ENCODED_LENGTH: 16
6
7
  ENCODED_LENGTH: 26
@@ -11,16 +12,13 @@ class ULID
11
12
  MAX_ENTROPY: 1208925819614629174706175
12
13
  MAX_INTEGER: 340282366920938463463374607431768211455
13
14
  TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
14
- PATTERN: Regexp
15
- STRICT_PATTERN: Regexp
15
+ RANDOM_INTEGER_GENERATOR: ^() -> Integer
16
+ PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
17
+ STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
18
+ SCANNING_PATTERN: Regexp
16
19
  UUIDV4_PATTERN: Regexp
17
- N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
18
- CROCKFORD_BASE32_CHAR_PATTERN: Regexp
19
- CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
20
- N32_CHAR_PATTERN: Regexp
21
20
  MIN: ULID
22
21
  MAX: ULID
23
- UNDEFINED: BasicObject
24
22
  include Comparable
25
23
 
26
24
  # The `moment` is a `Time` or `Intger of the milliseconds`
@@ -35,7 +33,17 @@ class ULID
35
33
  class ParserError < Error
36
34
  end
37
35
 
38
- class SetupError < ScriptError
36
+ module CrockfordBase32
37
+ class SetupError < ScriptError
38
+ end
39
+
40
+ N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
41
+ CROCKFORD_BASE32_CHAR_PATTERN: Regexp
42
+ CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
43
+ N32_CHAR_PATTERN: Regexp
44
+
45
+ def self.encode: (Integer integer) -> String
46
+ def self.decode: (String string) -> Integer
39
47
  end
40
48
 
41
49
  class MonotonicGenerator
@@ -50,26 +58,22 @@ class ULID
50
58
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
51
59
  type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
52
60
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
61
+ type period = Range[Time] | Range[nil] | Range[ULID]
53
62
 
54
63
  @milliseconds: Integer
55
64
  @entropy: Integer
56
65
  @string: String?
57
66
  @integer: Integer?
58
- @octets: octets?
59
- @timestamp_octets: timestamp_octets?
60
- @randomness_octets: randomness_octets?
61
67
  @timestamp: String?
62
68
  @randomness: String?
63
69
  @inspect: String?
64
70
  @time: Time?
65
- @next: ULID?
66
71
 
67
72
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
68
73
  def self.at: (Time time) -> ULID
69
74
  def self.current_milliseconds: -> Integer
70
- def self.milliseconds_from_time: (Time time) -> Integer
71
75
  def self.milliseconds_from_moment: (moment moment) -> Integer
72
- def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
76
+ def self.range: (period period) -> Range[ULID]
73
77
  def self.floor: (Time time) -> Time
74
78
  def self.reasonable_entropy: -> Integer
75
79
  def self.parse: (String string) -> ULID
@@ -77,13 +81,11 @@ class ULID
77
81
  def self.from_integer: (Integer integer) -> ULID
78
82
  def self.min: (?moment: moment) -> ULID
79
83
  def self.max: (?moment: moment) -> ULID
80
- def self.sample: -> ULID
81
- | (Integer number) -> Array[ULID]
84
+ def self.sample: (?period: period) -> ULID
85
+ | (Integer number, ?period: period) -> Array[ULID]
82
86
  def self.valid?: (untyped string) -> bool
83
87
  def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
84
88
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
85
- def self.octets_from_integer: (Integer integer) -> octets
86
- def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
87
89
  def self.from_monotonic_generator: (MonotonicGenerator generator) -> ULID
88
90
  attr_reader milliseconds: Integer
89
91
  attr_reader entropy: Integer
@@ -101,8 +103,6 @@ class ULID
101
103
  def timestamp: -> String
102
104
  def randomness: -> String
103
105
  def patterns: -> Hash[Symbol, Regexp | String]
104
- def pattern: -> Regexp
105
- def strict_pattern: -> Regexp
106
106
  def octets: -> octets
107
107
  def timestamp_octets: -> timestamp_octets
108
108
  def randomness_octets: -> randomness_octets
@@ -113,6 +113,6 @@ class ULID
113
113
  def freeze: -> self
114
114
 
115
115
  private
116
- def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
116
+ def self.milliseconds_from_time: (Time time) -> Integer
117
117
  def cache_all_instance_variables: -> void
118
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.18
4
+ version: 0.0.19
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-07 00:00:00.000000000 Z
11
+ date: 2021-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbs
@@ -77,6 +77,7 @@ files:
77
77
  - LICENSE
78
78
  - README.md
79
79
  - lib/ulid.rb
80
+ - lib/ulid/crockford_base32.rb
80
81
  - lib/ulid/monotonic_generator.rb
81
82
  - lib/ulid/uuid.rb
82
83
  - lib/ulid/version.rb