ruby-ulid 0.0.17 → 0.1.2

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: 29a9e3cd7ec91b93c24691976fef74ea5ab0cbc8f4fa237f449fa9322e42c6d2
4
- data.tar.gz: 9a3c0e12e7d2fe8b6057662ebf6a392c749f7254ee41c4e77221a2e9f45ff6ac
3
+ metadata.gz: 04b50bb9543b78d03a0e1676b988060b3e34348c0785979fde52c46e414be1d8
4
+ data.tar.gz: bf070ba0522e8ce743c67bcfa225bf5b49009ae4ecd6532eef19484d51eb14ce
5
5
  SHA512:
6
- metadata.gz: 53a12c330c32f76f13e651cef96ce29a6a442cf3dc7742faf2409a0ee17c7dfdcee1031c17696cd51d7bd3cf5b22b2df34b8be5a203cd1d4fd5df079eb357b82
7
- data.tar.gz: f6c8f8850272353bbd32cc15d437d0b021c13d9bc93c5c988255d6c50fe21ff41aef5b719261ca95b00c129024fe325e2d3c8683c70f9b55e89a09a261fff31f
6
+ metadata.gz: 4ad79f68c4903f775750417085676a98e810cae71b635b0b0e3e5547dd8b652a6bd89b01cb452510054f400a9bfef7ae26e308456d46a7571be10cb7ef1f334f
7
+ data.tar.gz: 88769b682521bb83b3775784e808b4e08636313d2055c7afe1f6b3a966e211237c597e0c354e0a6a6a26e227246c96845671f24cd7ebb8bf9d93014d043147f0
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.16'
52
+ gem 'ruby-ulid', '>= 0.1.2', '< 0.2.0'
53
53
  ```
54
54
 
55
55
  ### Generator and Parser
@@ -62,7 +62,11 @@ require 'ulid'
62
62
 
63
63
  ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
64
64
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
65
+ ulid.milliseconds #=> 1619544442826
65
66
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
67
+ ulid.timestamp #=> "01F4A5Y1YA"
68
+ ulid.randomness #=> "QCYAYCTC7GRMJ9AA"
69
+ ulid.to_i #=> 1957909092946624190749577070267409738
66
70
  ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
67
71
  ```
68
72
 
@@ -86,15 +90,16 @@ ulids.uniq(&:to_time).size #=> 1000
86
90
  ulids.sort == ulids #=> true
87
91
  ```
88
92
 
89
- `ULID.generate` can take fixed `Time` instance
93
+ `ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`
90
94
 
91
95
  ```ruby
92
96
  time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
93
97
  ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
94
98
  ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
99
+ ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
95
100
 
96
101
  ulids = 1000.times.map do |n|
97
- ULID.generate(moment: time + n)
102
+ ULID.at(time + n)
98
103
  end
99
104
  ulids.sort == ulids #=> true
100
105
  ```
@@ -141,6 +146,8 @@ sample_ulids_by_the_time.take(5) #=>
141
146
  ulids.sort == ulids #=> true
142
147
  ```
143
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
+
144
151
  ### Filtering IDs with `Time`
145
152
 
146
153
  `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
@@ -163,7 +170,7 @@ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the
163
170
 
164
171
  # Below patterns are acceptable
165
172
  pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
166
- 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)
167
174
  until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
168
175
  until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
169
176
 
@@ -173,6 +180,13 @@ ulids.grep_v(one_of_the_above)
173
180
  #=> I hope the results should be actually you want!
174
181
  ```
175
182
 
183
+ If you want to manually handle the Time objects, `ULID.floor` returns new `Time` with truncating excess precisions in ULID spec.
184
+
185
+ ```ruby
186
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
187
+ ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
188
+ ```
189
+
176
190
  ### Scanner for string (e.g. `JSON`)
177
191
 
178
192
  For rough operations, `ULID.scan` might be useful.
@@ -215,17 +229,31 @@ ULID.scan(json).to_a
215
229
  # ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
216
230
  ```
217
231
 
232
+ `ULID#patterns` is a util for text based operations.
233
+ The results and spec are not fixed. Should not be used except snippets/console operation
234
+
235
+ ```ruby
236
+ ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
237
+ #=> returns like a fallowing Hash
238
+ {
239
+ named_captures: /(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)/i,
240
+ strict_named_captures: /\A(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)\z/i
241
+ }
242
+ ```
243
+
218
244
  ### Some methods to help manipulations
219
245
 
220
246
  `ULID.min` and `ULID.max` return termination values for ULID spec.
221
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
+
222
250
  ```ruby
223
251
  ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
224
252
  ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
225
253
 
226
254
  time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
227
- ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
228
- 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)
229
257
  ```
230
258
 
231
259
  `ULID#next` and `ULID#succ` returns next(successor) ULID.
@@ -247,7 +275,9 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
247
275
  ULID.parse('00000000000000000000000000').pred #=> nil
248
276
  ```
249
277
 
250
- `ULID.sample` returns random ULIDs ignoring the generating time
278
+ `ULID.sample` returns random ULIDs.
279
+
280
+ Basically ignores generating time.
251
281
 
252
282
  ```ruby
253
283
  ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
@@ -263,13 +293,49 @@ ULID.sample(5)
263
293
  # ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
264
294
  ```
265
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
+
266
329
  ### UUIDv4 converter for migration use-cases
267
330
 
268
331
  `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
269
- The imported timestamp is meaningless. So ULID's benefit will lost
332
+ The imported timestamp is meaningless. So ULID's benefit will lost.
270
333
 
271
334
  ```ruby
272
- # Basically reversible
335
+ # Currently experimental feature, so needed to load the extension.
336
+ require 'ulid/uuid'
337
+
338
+ # Basically reversible
273
339
  ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
274
340
  ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
275
341
 
@@ -322,13 +388,13 @@ Major methods can be replaced as below.
322
388
  -ULID.generate
323
389
  +ULID.generate.to_s
324
390
  -ULID.at(time)
325
- +ULID.generate(moment: time).to_s
391
+ +ULID.at(time).to_s
326
392
  -ULID.time(string)
327
393
  +ULID.parse(string).to_time
328
394
  -ULID.min_ulid_at(time)
329
- +ULID.min(moment: time).to_s
395
+ +ULID.min(time).to_s
330
396
  -ULID.max_ulid_at(time)
331
- +ULID.max(moment: time).to_s
397
+ +ULID.max(time).to_s
332
398
  ```
333
399
 
334
400
  NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
@@ -337,6 +403,12 @@ NOTE: It is still having precision issue similar as `ulid gem` in the both gener
337
403
  1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
338
404
  1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
339
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
+
340
412
  ## References
341
413
 
342
414
  - [Repository](https://github.com/kachick/ruby-ulid)
data/lib/ulid.rb CHANGED
@@ -15,76 +15,114 @@ 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
- encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
- # Crockford's Base32. Excluded I, L, O, U.
22
- # @see https://www.crockford.com/base32.html
23
- 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'
24
24
 
25
- TIMESTAMP_PART_LENGTH = 10
26
- RANDOMNESS_PART_LENGTH = 16
27
- ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
25
+ TIMESTAMP_ENCODED_LENGTH = 10
26
+ RANDOMNESS_ENCODED_LENGTH = 16
27
+ ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
28
28
  TIMESTAMP_OCTETS_LENGTH = 6
29
29
  RANDOMNESS_OCTETS_LENGTH = 10
30
30
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
31
  MAX_MILLISECONDS = 281474976710655
32
32
  MAX_ENTROPY = 1208925819614629174706175
33
33
  MAX_INTEGER = 340282366920938463463374607431768211455
34
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
35
- STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
34
 
37
- # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
- UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i.freeze
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
- Kernel.instance_method(:freeze).bind(UNDEFINED).call
49
+ private_class_method :new
46
50
 
47
51
  # @param [Integer, Time] moment
48
52
  # @param [Integer] entropy
49
53
  # @return [ULID]
50
54
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
51
- new milliseconds: milliseconds_from_moment(moment), entropy: entropy
55
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
52
56
  end
53
57
 
54
- # @param [Integer, Time] moment
58
+ # Short hand of `ULID.generate(moment: time)`
59
+ # @param [Time] time
60
+ # @return [ULID]
61
+ def self.at(time)
62
+ raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
63
+ from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
64
+ end
65
+
66
+ # @param [Time, Integer] moment
55
67
  # @return [ULID]
56
- def self.min(moment: 0)
68
+ def self.min(moment=0)
57
69
  0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
58
70
  end
59
71
 
60
- # @param [Integer, Time] moment
72
+ # @param [Time, Integer] moment
61
73
  # @return [ULID]
62
- def self.max(moment: MAX_MILLISECONDS)
74
+ def self.max(moment=MAX_MILLISECONDS)
63
75
  MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
64
76
  end
65
77
 
66
- # @param [Integer] number
67
- # @return [ULID, Array<ULID>]
68
- # @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]
69
89
  # @note Major difference of `Array#sample` interface is below
70
90
  # * Do not ensure the uniqueness
71
91
  # * Do not take random generator for the arguments
72
92
  # * Raising error instead of truncating elements for the given number
73
- def self.sample(number=UNDEFINED)
74
- if UNDEFINED.equal?(number)
75
- 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
+ }
76
104
  else
77
- begin
78
- int = number.to_int
79
- rescue
80
- # Can not use `number.to_s` and `number.inspect` for considering BasicObject here
81
- 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"
82
117
  end
83
118
 
84
- if int > MAX_INTEGER || int.negative?
85
- 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}`"
86
121
  end
87
- 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)"
88
126
  end
89
127
  end
90
128
 
@@ -93,77 +131,68 @@ class ULID
93
131
  # @yieldparam [ULID] ulid
94
132
  # @yieldreturn [self]
95
133
  def self.scan(string)
96
- string = string.to_str
134
+ string = String.try_convert(string)
135
+ raise ArgumentError, 'ULID.scan takes only strings' unless string
97
136
  return to_enum(__callee__, string) unless block_given?
98
- string.scan(PATTERN) do |pair|
99
- yield parse(pair.join)
137
+ string.scan(SCANNING_PATTERN) do |matched|
138
+ yield parse(matched)
100
139
  end
101
140
  self
102
141
  end
103
142
 
104
- # @param [String, #to_str] uuid
105
- # @return [ULID]
106
- # @raise [ParserError] if the given format is not correct for UUIDv4 specs
107
- def self.from_uuidv4(uuid)
108
- begin
109
- uuid = uuid.to_str
110
- prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
111
- raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
112
- normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
113
- from_integer(normalized.to_i(16))
114
- rescue => err
115
- raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
116
- end
117
- end
118
-
119
- # @param [Integer, #to_int] integer
143
+ # @param [Integer] integer
120
144
  # @return [ULID]
121
145
  # @raise [OverflowError] if the given integer is larger than the ULID limit
122
146
  # @raise [ArgumentError] if the given integer is negative number
123
- # @todo Need optimized for performance
124
147
  def self.from_integer(integer)
125
- integer = integer.to_int
148
+ raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
126
149
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
127
150
  raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
128
151
 
129
- octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
130
- time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
131
- randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
132
- milliseconds = inverse_of_digits(time_octets)
133
- entropy = inverse_of_digits(randomness_octets)
152
+ n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
153
+ n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
154
+ n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
134
155
 
135
- new milliseconds: milliseconds, entropy: entropy
156
+ milliseconds = n32encoded_timestamp.to_i(32)
157
+ entropy = n32encoded_randomness.to_i(32)
158
+
159
+ new milliseconds: milliseconds, entropy: entropy, integer: integer
136
160
  end
137
161
 
138
- # @param [Range<Time>, Range<nil>] time_range
162
+ # @param [Range<Time>, Range<nil>, Range[ULID]] period
139
163
  # @return [Range<ULID>]
140
- # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
141
- def self.range(time_range)
142
- raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
143
- 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
144
169
 
145
- case begin_time
170
+ case begin_element
146
171
  when Time
147
- begin_ulid = min(moment: begin_time)
172
+ begin_ulid = min(begin_element)
148
173
  when nil
149
174
  begin_ulid = MIN
175
+ when self
176
+ begin_ulid = begin_element
150
177
  else
151
- 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}"
152
179
  end
153
180
 
154
- case end_time
181
+ case end_element
155
182
  when Time
156
183
  if exclude_end
157
- end_ulid = min(moment: end_time)
184
+ end_ulid = min(end_element)
158
185
  else
159
- end_ulid = max(moment: end_time)
186
+ end_ulid = max(end_element)
160
187
  end
161
188
  when nil
162
189
  # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
163
190
  end_ulid = MAX
164
191
  exclude_end = false
192
+ when self
193
+ end_ulid = end_element
165
194
  else
166
- 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}"
167
196
  end
168
197
 
169
198
  begin_ulid.freeze
@@ -175,6 +204,8 @@ class ULID
175
204
  # @param [Time] time
176
205
  # @return [Time]
177
206
  def self.floor(time)
207
+ raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
208
+
178
209
  if RUBY_VERSION >= '2.7'
179
210
  time.floor(3)
180
211
  else
@@ -191,7 +222,7 @@ class ULID
191
222
  # @api private
192
223
  # @param [Time] time
193
224
  # @return [Integer]
194
- def self.milliseconds_from_time(time)
225
+ private_class_method def self.milliseconds_from_time(time)
195
226
  (time.to_r * 1000).to_i
196
227
  end
197
228
 
@@ -199,154 +230,121 @@ class ULID
199
230
  # @param [Time, Integer] moment
200
231
  # @return [Integer]
201
232
  def self.milliseconds_from_moment(moment)
202
- 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
203
241
  end
204
242
 
205
- # @api private
206
243
  # @return [Integer]
207
- def self.reasonable_entropy
244
+ private_class_method def self.reasonable_entropy
208
245
  SecureRandom.random_number(MAX_ENTROPY)
209
246
  end
210
247
 
211
- n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
212
- raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
213
-
214
- n32_char_by_number = {}
215
- n32_chars.each_with_index do |char, index|
216
- n32_char_by_number[index] = char
217
- end
218
- n32_char_by_number.freeze
219
-
220
- # Currently supporting only for `subset for actual use-case`
221
- # See below
222
- # * https://github.com/ulid/spec/pull/57
223
- # * https://github.com/kachick/ruby-ulid/issues/57
224
- # * https://github.com/kachick/ruby-ulid/issues/78
225
- crockford_base32_mappings = {
226
- 'J' => 18,
227
- 'K' => 19,
228
- 'M' => 20,
229
- 'N' => 21,
230
- 'P' => 22,
231
- 'Q' => 23,
232
- 'R' => 24,
233
- 'S' => 25,
234
- 'T' => 26,
235
- 'V' => 27,
236
- 'W' => 28,
237
- 'X' => 29,
238
- 'Y' => 30,
239
- 'Z' => 31
240
- }.freeze
241
-
242
- N32_CHAR_BY_CROCKFORD_BASE32_CHAR = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
243
- if n = crockford_base32_mappings[encoding_char]
244
- char_32 = n32_char_by_number.fetch(n)
245
- map[encoding_char] = char_32
246
- end
247
- end.freeze
248
- raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
249
- CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
250
-
251
- CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
252
- N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
253
-
254
248
  # @param [String, #to_str] string
255
249
  # @return [ULID]
256
250
  # @raise [ParserError] if the given format is not correct for ULID specs
257
251
  def self.parse(string)
258
- begin
259
- string = string.to_str
260
- raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
261
- n32encoded = convert_crockford_base32_to_n32(string.upcase)
262
- timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
263
- randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
264
- milliseconds = timestamp.to_i(32)
265
- entropy = randomness.to_i(32)
266
- rescue => err
267
- raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
268
- end
252
+ string = String.try_convert(string)
253
+ raise ArgumentError, 'ULID.parse takes only strings' unless string
269
254
 
270
- new milliseconds: milliseconds, entropy: entropy
271
- end
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}`"
257
+ end
272
258
 
273
- # @api private
274
- private_class_method def self.convert_crockford_base32_to_n32(string)
275
- string.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
259
+ from_integer(CrockfordBase32.decode(string))
276
260
  end
277
261
 
278
262
  # @return [Boolean]
279
- def self.valid?(string)
280
- parse(string)
281
- rescue Exception
282
- false
283
- else
284
- true
263
+ def self.valid?(object)
264
+ string = String.try_convert(object)
265
+ string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
285
266
  end
286
267
 
287
- # @api private
288
- # @param [Integer] integer
289
- # @param [Integer] length
290
- # @return [Array<Integer>]
291
- def self.octets_from_integer(integer, length:)
292
- digits = integer.digits(256)
293
- (length - digits.size).times do
294
- digits.push 0
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
295
284
  end
296
- digits.reverse!
297
285
  end
298
286
 
299
- # @api private
300
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
301
- # @param [Array<Integer>] reversed_digits
302
- # @return [Integer]
303
- def self.inverse_of_digits(reversed_digits)
304
- base = 256
305
- num = 0
306
- reversed_digits.each do |digit|
307
- num = (num * base) + digit
308
- end
309
- num
310
- end
287
+ # @param [BasicObject] object
288
+ # @return [String]
289
+ private_class_method def self.safe_get_class_name(object)
290
+ fallback = 'UnknownObject'
311
291
 
312
- # @return [ArgumentError]
313
- private_class_method def self.argument_error_for_range_building(argument)
314
- ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
292
+ begin
293
+ name = String.try_convert(object.class.name)
294
+ rescue Exception
295
+ fallback
296
+ else
297
+ name || fallback
298
+ end
315
299
  end
316
300
 
317
- attr_reader :milliseconds, :entropy
318
-
319
301
  # @api private
320
302
  # @param [Integer] milliseconds
321
303
  # @param [Integer] entropy
322
- # @return [void]
304
+ # @return [ULID]
323
305
  # @raise [OverflowError] if the given value is larger than the ULID limit
324
306
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
325
- def initialize(milliseconds:, entropy:)
326
- milliseconds = milliseconds.to_int
327
- entropy = entropy.to_int
307
+ def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
308
+ raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
328
309
  raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
329
310
  raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
330
311
  raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
331
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
318
+ end
319
+
320
+ attr_reader :milliseconds, :entropy
321
+
322
+ # @api private
323
+ # @param [Integer] milliseconds
324
+ # @param [Integer] entropy
325
+ # @param [Integer] integer
326
+ # @return [void]
327
+ def initialize(milliseconds:, entropy:, integer:)
328
+ # All arguments check should be done with each constructors, not here
329
+ @integer = integer
332
330
  @milliseconds = milliseconds
333
331
  @entropy = entropy
334
332
  end
335
333
 
336
334
  # @return [String]
337
335
  def to_s
338
- @string ||= convert_n32_to_crockford_base32(to_i.to_s(32).rjust(ENCODED_ID_LENGTH, '0').upcase).freeze
336
+ @string ||= CrockfordBase32.encode(@integer).freeze
339
337
  end
340
338
 
341
339
  # @return [Integer]
342
340
  def to_i
343
- @integer ||= self.class.inverse_of_digits(octets)
341
+ @integer
344
342
  end
345
343
  alias_method :hash, :to_i
346
344
 
347
345
  # @return [Integer, nil]
348
346
  def <=>(other)
349
- other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
347
+ (ULID === other) ? (@integer <=> other.to_i) : nil
350
348
  end
351
349
 
352
350
  # @return [String]
@@ -356,7 +354,7 @@ class ULID
356
354
 
357
355
  # @return [Boolean]
358
356
  def eql?(other)
359
- other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
357
+ equal?(other) || (ULID === other && @integer == other.to_i)
360
358
  end
361
359
  alias_method :==, :eql?
362
360
 
@@ -364,13 +362,9 @@ class ULID
364
362
  def ===(other)
365
363
  case other
366
364
  when ULID
367
- self == other
365
+ @integer == other.to_i
368
366
  when String
369
- begin
370
- self == self.class.parse(other)
371
- rescue Exception
372
- false
373
- end
367
+ to_s == other.upcase
374
368
  else
375
369
  false
376
370
  end
@@ -389,64 +383,69 @@ class ULID
389
383
 
390
384
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
391
385
  def octets
392
- @octets ||= (timestamp_octets + randomness_octets).freeze
386
+ digits = @integer.digits(256)
387
+ (OCTETS_LENGTH - digits.size).times do
388
+ digits.push 0
389
+ end
390
+ digits.reverse!
393
391
  end
394
392
 
395
393
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
396
394
  def timestamp_octets
397
- @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
395
+ octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
398
396
  end
399
397
 
400
398
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
401
399
  def randomness_octets
402
- @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
400
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
403
401
  end
404
402
 
405
403
  # @return [String]
406
404
  def timestamp
407
- @timestamp ||= matchdata[:timestamp].freeze
405
+ @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
408
406
  end
409
407
 
410
408
  # @return [String]
411
409
  def randomness
412
- @randomness ||= matchdata[:randomness].freeze
410
+ @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
413
411
  end
414
412
 
415
- # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
416
- # @return [Regexp]
417
- def pattern
418
- @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
419
- end
420
-
421
- # @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
422
- # @return [Regexp]
423
- def strict_pattern
424
- @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
413
+ # @note Providing for rough operations. The keys and values is not fixed.
414
+ # @return [Hash{Symbol => Regexp, String}]
415
+ def patterns
416
+ named_captures = /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
417
+ {
418
+ named_captures: named_captures,
419
+ strict_named_captures: /\A#{named_captures.source}\z/i.freeze
420
+ }
425
421
  end
426
422
 
427
423
  # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
428
- def next
429
- next_int = to_i.next
430
- return nil if next_int > MAX_INTEGER
431
- @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
432
435
  end
433
- alias_method :succ, :next
436
+ alias_method :next, :succ
434
437
 
435
438
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
436
439
  def pred
437
- pre_int = to_i.pred
438
- return nil if pre_int.negative?
439
- @pred ||= self.class.from_integer(pre_int)
440
- end
441
-
442
- # @return [String]
443
- def to_uuidv4
444
- @uuidv4 ||= begin
445
- # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
446
- array = octets.pack('C*').unpack('NnnnnN')
447
- array[2] = (array[2] & 0x0fff) | 0x4000
448
- array[3] = (array[3] & 0x3fff) | 0x8000
449
- ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
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)
450
449
  end
451
450
  end
452
451
 
@@ -457,36 +456,40 @@ class ULID
457
456
  super
458
457
  end
459
458
 
460
- private
459
+ # @return [self]
460
+ def to_ulid
461
+ self
462
+ end
461
463
 
462
- # @api private
463
- def convert_n32_to_crockford_base32(string)
464
- string.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR)
464
+ # @return [self]
465
+ def dup
466
+ self
465
467
  end
466
468
 
467
- # @return [MatchData]
468
- def matchdata
469
- @matchdata ||= STRICT_PATTERN.match(to_s).freeze
469
+ # @return [self]
470
+ def clone(freeze: true)
471
+ self
470
472
  end
471
473
 
474
+ undef_method :instance_variable_set
475
+
476
+ private
477
+
472
478
  # @return [void]
473
479
  def cache_all_instance_variables
474
480
  inspect
475
- octets
476
- to_i
477
- succ
478
- pred
479
- strict_pattern
480
- to_uuidv4
481
+ timestamp
482
+ randomness
481
483
  end
482
484
  end
483
485
 
484
486
  require_relative 'ulid/version'
487
+ require_relative 'ulid/crockford_base32'
485
488
  require_relative 'ulid/monotonic_generator'
486
489
 
487
490
  class ULID
488
491
  MIN = parse('00000000000000000000000000').freeze
489
492
  MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
490
493
 
491
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :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
492
495
  end