ruby-ulid 0.0.18 → 0.0.19

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