ruby-ulid 0.0.18 → 0.1.3

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