ruby-ulid 0.0.18 → 0.1.3

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: f4101c27c1bd3f61d9882bb06444cb929975461e4e8258d44869e40a564a8ede
4
+ data.tar.gz: 716df050e1cff54368ad826d04065eea06e7fdcca9973509bdd69c980ce2c347
5
5
  SHA512:
6
- metadata.gz: f073c7b240ef96b511c84c6bc5649c483fc97c62a3892f58531b6f36e8235e49dcd865433fe522eec48a3532c98c4deec8713f453a4b2a117157788eabddaf6b
7
- data.tar.gz: a2486bbefd62d0d019c15044c9d67b6aabc33e370f4ee88239c1e2b33a40738c736fcc9221dd3ce457eabf83a3580be5dc501fef0c359345a453331bdd62d139
6
+ metadata.gz: c4e70f2f648e8cb165cf03014a35d8c83a879a037eecc89f4b0d91717ccc3107a19ae12372dd6788ae601abbc6498a01e0cad08247f684d1bc5b72c0a43d017c
7
+ data.tar.gz: 821bdac8833a3034b5842133961d8bb1c8e6c9f67c487ec25533c0ee23f3933a6bc71ef0b995cda957706f3bf6813c08a3e7e1d4f1ab8d749522db76892101ed
data/README.md CHANGED
@@ -10,7 +10,7 @@ Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
10
10
 
11
11
  ![ULIDlogo](https://raw.githubusercontent.com/kachick/ruby-ulid/main/logo.png)
12
12
 
13
- ![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test.yml/badge.svg?branch=main)
13
+ ![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/badge.svg?branch=main)
14
14
  [![Gem Version](https://badge.fury.io/rb/ruby-ulid.png)](http://badge.fury.io/rb/ruby-ulid)
15
15
 
16
16
  ## Universally Unique Lexicographically Sortable Identifier
@@ -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.1.3', '< 0.2.0'
53
53
  ```
54
54
 
55
55
  ### Generator and Parser
@@ -146,6 +146,8 @@ sample_ulids_by_the_time.take(5) #=>
146
146
  ulids.sort == ulids #=> true
147
147
  ```
148
148
 
149
+ Same generator does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Thread::Mutex](https://github.com/ruby/ruby/blob/5f8bca32571fa9c651f6903d36f66082363f8879/thread_sync.c#L1572-L1582)
150
+
149
151
  ### Filtering IDs with `Time`
150
152
 
151
153
  `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
@@ -168,7 +170,7 @@ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the
168
170
 
169
171
  # Below patterns are acceptable
170
172
  pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
171
- until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
173
+ # until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
172
174
  until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
173
175
  until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
174
176
 
@@ -243,13 +245,15 @@ ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
243
245
 
244
246
  `ULID.min` and `ULID.max` return termination values for ULID spec.
245
247
 
248
+ It can take `Time` instance as an optional argument. Then returns min/max ID that has limit of randomness part in the time.
249
+
246
250
  ```ruby
247
251
  ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
248
252
  ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
249
253
 
250
254
  time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
251
- ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
252
- ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
255
+ ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
256
+ ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
253
257
  ```
254
258
 
255
259
  `ULID#next` and `ULID#succ` returns next(successor) ULID.
@@ -271,7 +275,9 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
271
275
  ULID.parse('00000000000000000000000000').pred #=> nil
272
276
  ```
273
277
 
274
- `ULID.sample` returns random ULIDs ignoring the generating time
278
+ `ULID.sample` returns random ULIDs.
279
+
280
+ Basically ignores generating time.
275
281
 
276
282
  ```ruby
277
283
  ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
@@ -287,6 +293,39 @@ ULID.sample(5)
287
293
  # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
288
294
  ```
289
295
 
296
+ You can specify a range object for the timestamp restriction, see also `ULID.range`.
297
+
298
+ ```ruby
299
+ ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
300
+ ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
301
+ ulids = ULID.sample(1000, period: ulid1..ulid2)
302
+ ulids.uniq.size #=> 1000
303
+ ulids.take(10)
304
+ #=>
305
+ #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
306
+ # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
307
+ # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
308
+ # ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
309
+ # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
310
+ # ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
311
+ # ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
312
+ # ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
313
+ # ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
314
+ # ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
315
+ ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
316
+ #=>
317
+ # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
318
+ # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
319
+ # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
320
+ # ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
321
+ # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
322
+ # ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
323
+ # ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
324
+ # ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
325
+ # ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
326
+ # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
327
+ ```
328
+
290
329
  ### UUIDv4 converter for migration use-cases
291
330
 
292
331
  `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
@@ -296,7 +335,7 @@ The imported timestamp is meaningless. So ULID's benefit will lost.
296
335
  # Currently experimental feature, so needed to load the extension.
297
336
  require 'ulid/uuid'
298
337
 
299
- # Basically reversible
338
+ # Basically reversible
300
339
  ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
301
340
  ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
302
341
 
@@ -353,9 +392,9 @@ Major methods can be replaced as below.
353
392
  -ULID.time(string)
354
393
  +ULID.parse(string).to_time
355
394
  -ULID.min_ulid_at(time)
356
- +ULID.min(moment: time).to_s
395
+ +ULID.min(time).to_s
357
396
  -ULID.max_ulid_at(time)
358
- +ULID.max(moment: time).to_s
397
+ +ULID.max(time).to_s
359
398
  ```
360
399
 
361
400
  NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
@@ -364,6 +403,12 @@ NOTE: It is still having precision issue similar as `ulid gem` in the both gener
364
403
  1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
365
404
  1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
366
405
 
406
+ ### Compare performance with them
407
+
408
+ See [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
409
+
410
+ The results are not something to be proud of.
411
+
367
412
  ## References
368
413
 
369
414
  - [Repository](https://github.com/kachick/ruby-ulid)
data/lib/ulid.rb CHANGED
@@ -15,15 +15,12 @@ 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
18
+ class UnexpectedError < Error; 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
25
- encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
26
- encoding_chars = encoding_string.chars.map(&:freeze).freeze
20
+ # Excluded I, L, O, U, -.
21
+ # This is the encoding patterns.
22
+ # The decoding issue is written in ULID::CrockfordBase32
23
+ CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
27
24
 
28
25
  TIMESTAMP_ENCODED_LENGTH = 10
29
26
  RANDOMNESS_ENCODED_LENGTH = 16
@@ -34,32 +31,28 @@ class ULID
34
31
  MAX_MILLISECONDS = 281474976710655
35
32
  MAX_ENTROPY = 1208925819614629174706175
36
33
  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
34
+
35
+ # @see https://github.com/ulid/spec/pull/57
36
+ # Currently not used as a constant, but kept as a reference for now.
37
+ 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
38
+
39
+ STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
40
+
41
+ # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
42
+ # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
43
+ SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
39
44
 
40
45
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
46
  # @see https://bugs.ruby-lang.org/issues/15958
42
47
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
43
48
 
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
49
  private_class_method :new
57
50
 
58
51
  # @param [Integer, Time] moment
59
52
  # @param [Integer] entropy
60
53
  # @return [ULID]
61
54
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
62
- new milliseconds: milliseconds_from_moment(moment), entropy: entropy
55
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
63
56
  end
64
57
 
65
58
  # Short hand of `ULID.generate(moment: time)`
@@ -67,43 +60,69 @@ class ULID
67
60
  # @return [ULID]
68
61
  def self.at(time)
69
62
  raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
70
- new milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy
63
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
71
64
  end
72
65
 
73
- # @param [Integer, Time] moment
66
+ # @param [Time, Integer] moment
74
67
  # @return [ULID]
75
- def self.min(moment: 0)
68
+ def self.min(moment=0)
76
69
  0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
77
70
  end
78
71
 
79
- # @param [Integer, Time] moment
72
+ # @param [Time, Integer] moment
80
73
  # @return [ULID]
81
- def self.max(moment: MAX_MILLISECONDS)
74
+ def self.max(moment=MAX_MILLISECONDS)
82
75
  MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
83
76
  end
84
77
 
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
78
+ RANDOM_INTEGER_GENERATOR = -> {
79
+ SecureRandom.random_number(MAX_INTEGER)
80
+ }
81
+
82
+ # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
83
+ # @overload sample(number, period: nil)
84
+ # @param [Integer] number
85
+ # @return [Array<ULID>]
86
+ # @raise [ArgumentError] if the given number is lager than `ULID spec limits` or `Possibilities of given period`, or given negative number
87
+ # @overload sample(period: nil)
88
+ # @return [ULID]
88
89
  # @note Major difference of `Array#sample` interface is below
89
90
  # * Do not ensure the uniqueness
90
91
  # * Do not take random generator for the arguments
91
92
  # * 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))
93
+ def self.sample(*args, period: nil)
94
+ int_generator = if period
95
+ ulid_range = range(period)
96
+ min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
97
+
98
+ possibilities = (max - min) + (exclude_end ? 0 : 1)
99
+ raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
100
+
101
+ -> {
102
+ SecureRandom.random_number(possibilities) + min
103
+ }
95
104
  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'
105
+ RANDOM_INTEGER_GENERATOR
106
+ end
107
+
108
+ case args.size
109
+ when 0
110
+ from_integer(int_generator.call)
111
+ when 1
112
+ number = args.first
113
+ raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
114
+
115
+ if number > MAX_INTEGER || number.negative?
116
+ raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
101
117
  end
102
118
 
103
- if int > MAX_INTEGER || int.negative?
104
- raise ArgumentError, "given number is larger than ULID limit #{MAX_INTEGER} or negative: #{number.inspect}"
119
+ if period && (number > possibilities)
120
+ raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
105
121
  end
106
- int.times.map { from_integer(SecureRandom.random_number(MAX_INTEGER)) }
122
+
123
+ Array.new(number) { from_integer(int_generator.call) }
124
+ else
125
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
107
126
  end
108
127
  end
109
128
 
@@ -112,20 +131,21 @@ class ULID
112
131
  # @yieldparam [ULID] ulid
113
132
  # @yieldreturn [self]
114
133
  def self.scan(string)
115
- string = string.to_str
134
+ string = String.try_convert(string)
135
+ raise ArgumentError, 'ULID.scan takes only strings' unless string
116
136
  return to_enum(__callee__, string) unless block_given?
117
- string.scan(PATTERN) do |pair|
118
- yield parse(pair.join)
137
+ string.scan(SCANNING_PATTERN) do |matched|
138
+ yield parse(matched)
119
139
  end
120
140
  self
121
141
  end
122
142
 
123
- # @param [Integer, #to_int] integer
143
+ # @param [Integer] integer
124
144
  # @return [ULID]
125
145
  # @raise [OverflowError] if the given integer is larger than the ULID limit
126
146
  # @raise [ArgumentError] if the given integer is negative number
127
147
  def self.from_integer(integer)
128
- integer = integer.to_int
148
+ raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
129
149
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
130
150
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
131
151
 
@@ -139,35 +159,40 @@ class ULID
139
159
  new milliseconds: milliseconds, entropy: entropy, integer: integer
140
160
  end
141
161
 
142
- # @param [Range<Time>, Range<nil>] time_range
162
+ # @param [Range<Time>, Range<nil>, Range[ULID]] period
143
163
  # @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?
164
+ # @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
165
+ def self.range(period)
166
+ raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period
167
+ begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
168
+ return period if self === begin_element && self === end_element
148
169
 
149
- case begin_time
170
+ case begin_element
150
171
  when Time
151
- begin_ulid = min(moment: begin_time)
172
+ begin_ulid = min(begin_element)
152
173
  when nil
153
174
  begin_ulid = MIN
175
+ when self
176
+ begin_ulid = begin_element
154
177
  else
155
- raise argument_error_for_range_building(time_range)
178
+ raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
156
179
  end
157
180
 
158
- case end_time
181
+ case end_element
159
182
  when Time
160
183
  if exclude_end
161
- end_ulid = min(moment: end_time)
184
+ end_ulid = min(end_element)
162
185
  else
163
- end_ulid = max(moment: end_time)
186
+ end_ulid = max(end_element)
164
187
  end
165
188
  when nil
166
189
  # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
167
190
  end_ulid = MAX
168
191
  exclude_end = false
192
+ when self
193
+ end_ulid = end_element
169
194
  else
170
- raise argument_error_for_range_building(time_range)
195
+ raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
171
196
  end
172
197
 
173
198
  begin_ulid.freeze
@@ -179,6 +204,8 @@ class ULID
179
204
  # @param [Time] time
180
205
  # @return [Time]
181
206
  def self.floor(time)
207
+ raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
208
+
182
209
  if RUBY_VERSION >= '2.7'
183
210
  time.floor(3)
184
211
  else
@@ -195,7 +222,7 @@ class ULID
195
222
  # @api private
196
223
  # @param [Time] time
197
224
  # @return [Integer]
198
- def self.milliseconds_from_time(time)
225
+ private_class_method def self.milliseconds_from_time(time)
199
226
  (time.to_r * 1000).to_i
200
227
  end
201
228
 
@@ -203,107 +230,91 @@ class ULID
203
230
  # @param [Time, Integer] moment
204
231
  # @return [Integer]
205
232
  def self.milliseconds_from_moment(moment)
206
- moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
233
+ case moment
234
+ when Integer
235
+ moment
236
+ when Time
237
+ milliseconds_from_time(moment)
238
+ else
239
+ raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
240
+ end
207
241
  end
208
242
 
209
- # @api private
210
243
  # @return [Integer]
211
- def self.reasonable_entropy
244
+ private_class_method def self.reasonable_entropy
212
245
  SecureRandom.random_number(MAX_ENTROPY)
213
246
  end
214
247
 
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
248
  # @param [String, #to_str] string
259
249
  # @return [ULID]
260
250
  # @raise [ParserError] if the given format is not correct for ULID specs
261
251
  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}"
252
+ string = String.try_convert(string)
253
+ raise ArgumentError, 'ULID.parse takes only strings' unless string
254
+
255
+ unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
256
+ raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
267
257
  end
268
258
 
269
- n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
270
- from_integer(n32encoded.to_i(32))
259
+ from_integer(CrockfordBase32.decode(string))
271
260
  end
272
261
 
273
262
  # @return [Boolean]
274
- def self.valid?(string)
275
- parse(string)
276
- rescue Exception
277
- false
278
- else
279
- true
263
+ def self.valid?(object)
264
+ string = String.try_convert(object)
265
+ string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
280
266
  end
281
267
 
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
268
+ # @param [ULID, #to_ulid] object
269
+ # @return [ULID, nil]
270
+ # @raise [TypeError] if `object.to_ulid` did not return ULID instance
271
+ def self.try_convert(object)
272
+ begin
273
+ converted = object.to_ulid
274
+ rescue NoMethodError
275
+ nil
276
+ else
277
+ if ULID === converted
278
+ converted
279
+ else
280
+ object_class_name = safe_get_class_name(object)
281
+ converted_class_name = safe_get_class_name(converted)
282
+ raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
283
+ end
291
284
  end
292
- num
293
285
  end
294
286
 
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
287
+ # @param [BasicObject] object
288
+ # @return [String]
289
+ private_class_method def self.safe_get_class_name(object)
290
+ fallback = 'UnknownObject'
291
+
292
+ begin
293
+ name = String.try_convert(object.class.name)
294
+ rescue Exception
295
+ fallback
296
+ else
297
+ name || fallback
298
+ end
301
299
  end
302
300
 
303
301
  # @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}"
302
+ # @param [Integer] milliseconds
303
+ # @param [Integer] entropy
304
+ # @return [ULID]
305
+ # @raise [OverflowError] if the given value is larger than the ULID limit
306
+ # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
307
+ def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
308
+ raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
309
+ raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
310
+ raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
311
+ raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
312
+
313
+ n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
314
+ n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
315
+ integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)
316
+
317
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
307
318
  end
308
319
 
309
320
  attr_reader :milliseconds, :entropy
@@ -313,42 +324,27 @@ class ULID
313
324
  # @param [Integer] entropy
314
325
  # @param [Integer] integer
315
326
  # @return [void]
316
- # @raise [OverflowError] if the given value is larger than the ULID limit
317
- # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
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
329
-
327
+ def initialize(milliseconds:, entropy:, integer:)
328
+ # All arguments check should be done with each constructors, not here
329
+ @integer = integer
330
330
  @milliseconds = milliseconds
331
331
  @entropy = entropy
332
332
  end
333
333
 
334
334
  # @return [String]
335
335
  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
336
+ @string ||= CrockfordBase32.encode(@integer).freeze
337
337
  end
338
338
 
339
339
  # @return [Integer]
340
340
  def to_i
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
341
+ @integer
346
342
  end
347
343
  alias_method :hash, :to_i
348
344
 
349
345
  # @return [Integer, nil]
350
346
  def <=>(other)
351
- (ULID === other) ? (to_i <=> other.to_i) : nil
347
+ (ULID === other) ? (@integer <=> other.to_i) : nil
352
348
  end
353
349
 
354
350
  # @return [String]
@@ -358,7 +354,7 @@ class ULID
358
354
 
359
355
  # @return [Boolean]
360
356
  def eql?(other)
361
- equal?(other) || (ULID === other && to_i == other.to_i)
357
+ equal?(other) || (ULID === other && @integer == other.to_i)
362
358
  end
363
359
  alias_method :==, :eql?
364
360
 
@@ -366,13 +362,9 @@ class ULID
366
362
  def ===(other)
367
363
  case other
368
364
  when ULID
369
- self == other
365
+ @integer == other.to_i
370
366
  when String
371
- begin
372
- self == self.class.parse(other)
373
- rescue Exception
374
- false
375
- end
367
+ to_s == other.upcase
376
368
  else
377
369
  false
378
370
  end
@@ -391,17 +383,21 @@ class ULID
391
383
 
392
384
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
393
385
  def octets
394
- @octets ||= octets_from_integer(to_i).freeze
386
+ digits = @integer.digits(256)
387
+ (OCTETS_LENGTH - digits.size).times do
388
+ digits.push 0
389
+ end
390
+ digits.reverse!
395
391
  end
396
392
 
397
393
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
398
394
  def timestamp_octets
399
- @timestamp_octets ||= octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
395
+ octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
400
396
  end
401
397
 
402
398
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
403
399
  def randomness_octets
404
- @randomness_octets ||= octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
400
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
405
401
  end
406
402
 
407
403
  # @return [String]
@@ -424,31 +420,33 @@ class ULID
424
420
  }
425
421
  end
426
422
 
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
423
  # @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)
424
+ def succ
425
+ succ_int = @integer.succ
426
+ if succ_int >= MAX_INTEGER
427
+ if succ_int == MAX_INTEGER
428
+ MAX
429
+ else
430
+ nil
431
+ end
432
+ else
433
+ ULID.from_integer(succ_int)
434
+ end
444
435
  end
445
- alias_method :succ, :next
436
+ alias_method :next, :succ
446
437
 
447
438
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
448
439
  def pred
449
- pre_int = to_i.pred
450
- return nil if pre_int.negative?
451
- @pred ||= self.class.from_integer(pre_int)
440
+ pred_int = @integer.pred
441
+ if pred_int <= 0
442
+ if pred_int == 0
443
+ MIN
444
+ else
445
+ nil
446
+ end
447
+ else
448
+ ULID.from_integer(pred_int)
449
+ end
452
450
  end
453
451
 
454
452
  # @return [self]
@@ -458,36 +456,40 @@ class ULID
458
456
  super
459
457
  end
460
458
 
461
- private
459
+ # @return [self]
460
+ def to_ulid
461
+ self
462
+ end
462
463
 
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!
464
+ # @return [self]
465
+ def dup
466
+ self
471
467
  end
472
468
 
469
+ # @return [self]
470
+ def clone(freeze: true)
471
+ self
472
+ end
473
+
474
+ undef_method :instance_variable_set
475
+
476
+ private
477
+
473
478
  # @return [void]
474
479
  def cache_all_instance_variables
475
480
  inspect
476
- octets
477
- to_i
478
- succ
479
- pred
480
481
  timestamp
481
482
  randomness
482
483
  end
483
484
  end
484
485
 
485
486
  require_relative 'ulid/version'
487
+ require_relative 'ulid/crockford_base32'
486
488
  require_relative 'ulid/monotonic_generator'
487
489
 
488
490
  class ULID
489
491
  MIN = parse('00000000000000000000000000').freeze
490
492
  MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
491
493
 
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
494
+ private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
493
495
  end