ruby-ulid 0.0.17 → 0.1.2

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