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 +4 -4
- data/README.md +54 -9
- data/lib/ulid.rb +205 -203
- data/lib/ulid/crockford_base32.rb +70 -0
- data/lib/ulid/monotonic_generator.rb +42 -21
- data/lib/ulid/uuid.rb +9 -8
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +418 -43
- metadata +199 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4101c27c1bd3f61d9882bb06444cb929975461e4e8258d44869e40a564a8ede
|
4
|
+
data.tar.gz: 716df050e1cff54368ad826d04065eea06e7fdcca9973509bdd69c980ce2c347
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|

|
12
12
|
|
13
|
-

|
14
14
|
[](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.
|
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(
|
252
|
-
ULID.max(
|
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
|
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(
|
395
|
+
+ULID.min(time).to_s
|
357
396
|
-ULID.max_ulid_at(time)
|
358
|
-
+ULID.max(
|
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
|
18
|
+
class UnexpectedError < Error; end
|
19
19
|
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
63
|
+
from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
|
71
64
|
end
|
72
65
|
|
73
|
-
# @param [
|
66
|
+
# @param [Time, Integer] moment
|
74
67
|
# @return [ULID]
|
75
|
-
def self.min(moment
|
68
|
+
def self.min(moment=0)
|
76
69
|
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
77
70
|
end
|
78
71
|
|
79
|
-
# @param [
|
72
|
+
# @param [Time, Integer] moment
|
80
73
|
# @return [ULID]
|
81
|
-
def self.max(moment
|
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
|
-
|
86
|
-
|
87
|
-
|
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(
|
93
|
-
if
|
94
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
104
|
-
raise ArgumentError, "given number is larger than
|
119
|
+
if period && (number > possibilities)
|
120
|
+
raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
|
105
121
|
end
|
106
|
-
|
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
|
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(
|
118
|
-
yield parse(
|
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
|
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
|
-
|
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
|
162
|
+
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
143
163
|
# @return [Range<ULID>]
|
144
|
-
# @raise [ArgumentError] if the given
|
145
|
-
def self.range(
|
146
|
-
raise
|
147
|
-
|
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
|
170
|
+
case begin_element
|
150
171
|
when Time
|
151
|
-
begin_ulid = min(
|
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
|
178
|
+
raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
|
156
179
|
end
|
157
180
|
|
158
|
-
case
|
181
|
+
case end_element
|
159
182
|
when Time
|
160
183
|
if exclude_end
|
161
|
-
end_ulid = min(
|
184
|
+
end_ulid = min(end_element)
|
162
185
|
else
|
163
|
-
end_ulid = max(
|
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
|
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
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
raise ParserError, "
|
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
|
-
|
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?(
|
275
|
-
|
276
|
-
|
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
|
-
# @
|
283
|
-
# @
|
284
|
-
# @
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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
|
-
# @
|
296
|
-
# @
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
# @
|
305
|
-
|
306
|
-
|
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
|
-
|
317
|
-
|
318
|
-
|
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 ||=
|
336
|
+
@string ||= CrockfordBase32.encode(@integer).freeze
|
337
337
|
end
|
338
338
|
|
339
339
|
# @return [Integer]
|
340
340
|
def to_i
|
341
|
-
@integer
|
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) ? (
|
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 &&
|
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
|
-
|
365
|
+
@integer == other.to_i
|
370
366
|
when String
|
371
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
441
|
-
|
442
|
-
|
443
|
-
|
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 :
|
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
|
-
|
450
|
-
|
451
|
-
|
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
|
-
|
459
|
+
# @return [self]
|
460
|
+
def to_ulid
|
461
|
+
self
|
462
|
+
end
|
462
463
|
|
463
|
-
# @
|
464
|
-
|
465
|
-
|
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, :
|
494
|
+
private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
|
493
495
|
end
|