ruby-ulid 0.2.2 → 0.3.0
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 +24 -11
- data/lib/ruby-ulid.rb +1 -1
- data/lib/ulid/crockford_base32.rb +3 -3
- data/lib/ulid/monotonic_generator.rb +19 -15
- data/lib/ulid/uuid.rb +8 -5
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +99 -81
- data/sig/ulid.rbs +10 -8
- metadata +5 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 358b03a503f8a12c87f3f7daaf3b0acf6e55dc44f1aba7a38b9f9a38985c2c68
|
4
|
+
data.tar.gz: 15d25d443f4826b07fc9a74b092e35b9cd60dc34cc3615f03fee619d4fc39445
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b18d13597bd2f3dcd85a15a26d9086fb9a80b91775292c9da82d93144b6abd37585996f1bf64dfc4ac12b6d6d49683012dfea8e04624005358459667864114c
|
7
|
+
data.tar.gz: d987aa9b60717577fae96eef68fca76b2395f11a580df4863a69dd0931d955e4cf68608c101959db18f5e3a26031fbe284ff90a0d107fa84bbebb63949bba66b
|
data/README.md
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
+
[](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/?branch=main)
|
4
|
+
[](http://badge.fury.io/rb/ruby-ulid)
|
5
|
+
|
3
6
|
## Overview
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
+
[ulid/spec](https://github.com/ulid/spec) is useful.
|
9
|
+
Especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
|
10
|
+
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around ULID.
|
11
|
+
Also providing [ruby/rbs](https://github.com/ruby/rbs) signatures.
|
8
12
|
|
9
13
|
---
|
10
14
|
|
11
|
-
](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/?branch=main)
|
14
|
-
[](http://badge.fury.io/rb/ruby-ulid)
|
15
|
-
[](https://github.dev/kachick/ruby-ulid)
|
15
|
+

|
16
16
|
|
17
17
|
## Universally Unique Lexicographically Sortable Identifier
|
18
18
|
|
@@ -47,10 +47,10 @@ $ gem install ruby-ulid
|
|
47
47
|
Should be installed!
|
48
48
|
```
|
49
49
|
|
50
|
-
Add this line to your
|
50
|
+
Add this line to your Gemfile`.
|
51
51
|
|
52
52
|
```ruby
|
53
|
-
gem
|
53
|
+
gem('ruby-ulid', '~> 0.2.2')
|
54
54
|
```
|
55
55
|
|
56
56
|
### Generator and Parser
|
@@ -438,6 +438,18 @@ See [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
|
|
438
438
|
|
439
439
|
The results are not something to be proud of.
|
440
440
|
|
441
|
+
## How to use rbs
|
442
|
+
|
443
|
+
See structure at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox)
|
444
|
+
|
445
|
+
I have checked the behavior with [ruby/rbs@2.6.0](https://github.com/ruby/rbs) && [soutaro/steep@1.0.1](https://github.com/soutaro/steep) && [soutaro/steep-vscode](https://github.com/soutaro/steep-vscode).
|
446
|
+
|
447
|
+
* 
|
448
|
+
* 
|
449
|
+
* 
|
450
|
+
* 
|
451
|
+
* 
|
452
|
+
|
441
453
|
## References
|
442
454
|
|
443
455
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
@@ -446,4 +458,5 @@ The results are not something to be proud of.
|
|
446
458
|
|
447
459
|
## Note
|
448
460
|
|
449
|
-
-
|
461
|
+
- [UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html) is another choice for sortable and randomness ID.
|
462
|
+
However they are stayed in draft state. ref: [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
data/lib/ruby-ulid.rb
CHANGED
@@ -14,10 +14,10 @@ class ULID
|
|
14
14
|
# * https://github.com/kachick/ruby-ulid/issues/57
|
15
15
|
# * https://github.com/kachick/ruby-ulid/issues/78
|
16
16
|
module CrockfordBase32
|
17
|
-
class SetupError <
|
17
|
+
class SetupError < UnexpectedError; end
|
18
18
|
|
19
19
|
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
20
|
-
raise
|
20
|
+
raise(SetupError, 'obvious bug exists in the mapping algorithm') unless n32_chars.size == 32
|
21
21
|
|
22
22
|
n32_char_by_number = {}
|
23
23
|
n32_chars.each_with_index do |char, index|
|
@@ -48,7 +48,7 @@ class ULID
|
|
48
48
|
map[encoding_char] = char_32
|
49
49
|
end
|
50
50
|
end.freeze
|
51
|
-
raise
|
51
|
+
raise(SetupError, 'obvious bug exists in the mapping algorithm') unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
|
52
52
|
|
53
53
|
CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
|
54
54
|
|
@@ -5,15 +5,16 @@
|
|
5
5
|
|
6
6
|
class ULID
|
7
7
|
class MonotonicGenerator
|
8
|
-
include
|
8
|
+
include(MonitorMixin)
|
9
9
|
|
10
|
+
# @dynamic prev
|
10
11
|
# @return [ULID, nil]
|
11
|
-
attr_reader
|
12
|
+
attr_reader(:prev)
|
12
13
|
|
13
|
-
undef_method
|
14
|
+
undef_method(:instance_variable_set)
|
14
15
|
|
15
16
|
def initialize
|
16
|
-
super
|
17
|
+
super
|
17
18
|
@prev = nil
|
18
19
|
end
|
19
20
|
|
@@ -21,7 +22,8 @@ class ULID
|
|
21
22
|
def inspect
|
22
23
|
"ULID::MonotonicGenerator(prev: #{@prev.inspect})"
|
23
24
|
end
|
24
|
-
|
25
|
+
# @dynamic to_s
|
26
|
+
alias_method(:to_s, :inspect)
|
25
27
|
|
26
28
|
# @param [Time, Integer] moment
|
27
29
|
# @return [ULID]
|
@@ -30,23 +32,25 @@ class ULID
|
|
30
32
|
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
31
33
|
def generate(moment: ULID.current_milliseconds)
|
32
34
|
synchronize do
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
prev_ulid = @prev
|
36
|
+
unless prev_ulid
|
37
|
+
ret = ULID.generate(moment: moment)
|
38
|
+
@prev = ret
|
39
|
+
return ret
|
36
40
|
end
|
37
41
|
|
38
42
|
milliseconds = ULID.milliseconds_from_moment(moment)
|
39
43
|
|
40
44
|
ulid = (
|
41
|
-
if
|
45
|
+
if prev_ulid.milliseconds < milliseconds
|
42
46
|
ULID.generate(moment: milliseconds)
|
43
47
|
else
|
44
|
-
ULID.from_milliseconds_and_entropy(milliseconds:
|
48
|
+
ULID.from_milliseconds_and_entropy(milliseconds: prev_ulid.milliseconds, entropy: prev_ulid.entropy.succ)
|
45
49
|
end
|
46
50
|
)
|
47
51
|
|
48
|
-
unless ulid >
|
49
|
-
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{
|
52
|
+
unless ulid > prev_ulid
|
53
|
+
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{prev_ulid.inspect}"
|
50
54
|
additional_information = (
|
51
55
|
if Thread.list == [Thread.main]
|
52
56
|
'# NOTE: looks single thread only exist'
|
@@ -55,7 +59,7 @@ class ULID
|
|
55
59
|
end
|
56
60
|
)
|
57
61
|
|
58
|
-
raise
|
62
|
+
raise(UnexpectedError, base_message + additional_information)
|
59
63
|
end
|
60
64
|
|
61
65
|
@prev = ulid
|
@@ -63,12 +67,12 @@ class ULID
|
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
66
|
-
undef_method
|
70
|
+
undef_method(:freeze)
|
67
71
|
|
68
72
|
# @raise [TypeError] always raises exception and does not freeze self
|
69
73
|
# @return [void]
|
70
74
|
def freeze
|
71
|
-
raise
|
75
|
+
raise(TypeError, "cannot freeze #{self.class}")
|
72
76
|
end
|
73
77
|
end
|
74
78
|
end
|
data/lib/ulid/uuid.rb
CHANGED
@@ -10,18 +10,18 @@
|
|
10
10
|
class ULID
|
11
11
|
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
12
12
|
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
|
13
|
-
private_constant
|
13
|
+
private_constant(:UUIDV4_PATTERN)
|
14
14
|
|
15
15
|
# @param [String, #to_str] uuid
|
16
16
|
# @return [ULID]
|
17
17
|
# @raise [ParserError] if the given format is not correct for UUIDv4 specs
|
18
18
|
def self.from_uuidv4(uuid)
|
19
19
|
uuid = String.try_convert(uuid)
|
20
|
-
raise
|
20
|
+
raise(ArgumentError, 'ULID.from_uuidv4 takes only strings') unless uuid
|
21
21
|
|
22
22
|
prefix_trimmed = uuid.delete_prefix('urn:uuid:')
|
23
23
|
unless UUIDV4_PATTERN.match?(prefix_trimmed)
|
24
|
-
raise
|
24
|
+
raise(ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`")
|
25
25
|
end
|
26
26
|
|
27
27
|
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
@@ -32,8 +32,11 @@ class ULID
|
|
32
32
|
def to_uuidv4
|
33
33
|
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
34
34
|
array = octets.pack('C*').unpack('NnnnnN')
|
35
|
-
array[2]
|
36
|
-
|
35
|
+
ref2, ref3 = array[2], array[3]
|
36
|
+
raise unless Integer === ref2 && Integer === ref3
|
37
|
+
|
38
|
+
array[2] = (ref2 & 0x0fff) | 0x4000
|
39
|
+
array[3] = (ref3 & 0x3fff) | 0x8000
|
37
40
|
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
38
41
|
end
|
39
42
|
end
|
data/lib/ulid/version.rb
CHANGED
data/lib/ulid.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
5
5
|
|
6
|
-
require
|
6
|
+
require('securerandom')
|
7
7
|
|
8
8
|
# @see https://github.com/ulid/spec
|
9
9
|
# @!attribute [r] milliseconds
|
@@ -11,7 +11,7 @@ require 'securerandom'
|
|
11
11
|
# @!attribute [r] entropy
|
12
12
|
# @return [Integer]
|
13
13
|
class ULID
|
14
|
-
include
|
14
|
+
include(Comparable)
|
15
15
|
|
16
16
|
class Error < StandardError; end
|
17
17
|
class OverflowError < Error; end
|
@@ -25,10 +25,12 @@ class ULID
|
|
25
25
|
|
26
26
|
TIMESTAMP_ENCODED_LENGTH = 10
|
27
27
|
RANDOMNESS_ENCODED_LENGTH = 16
|
28
|
-
ENCODED_LENGTH =
|
28
|
+
ENCODED_LENGTH = 26
|
29
|
+
|
29
30
|
TIMESTAMP_OCTETS_LENGTH = 6
|
30
31
|
RANDOMNESS_OCTETS_LENGTH = 10
|
31
|
-
OCTETS_LENGTH =
|
32
|
+
OCTETS_LENGTH = 16
|
33
|
+
|
32
34
|
MAX_MILLISECONDS = 281474976710655
|
33
35
|
MAX_ENTROPY = 1208925819614629174706175
|
34
36
|
MAX_INTEGER = 340282366920938463463374607431768211455
|
@@ -43,11 +45,13 @@ class ULID
|
|
43
45
|
# This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
|
44
46
|
SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
|
45
47
|
|
46
|
-
#
|
48
|
+
# Similar as Time#inspect since Ruby 2.7, however it is NOT same.
|
49
|
+
# Time#inspect trancates needless digits. Keeping full milliseconds with "%3N" will fit for ULID.
|
47
50
|
# @see https://bugs.ruby-lang.org/issues/15958
|
51
|
+
# @see https://github.com/ruby/ruby/blob/744d17ff6c33b09334508e8110007ea2a82252f5/time.c#L4026-L4078
|
48
52
|
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
49
53
|
|
50
|
-
private_class_method
|
54
|
+
private_class_method(:new)
|
51
55
|
|
52
56
|
# @param [Integer, Time] moment
|
53
57
|
# @param [Integer] entropy
|
@@ -60,7 +64,7 @@ class ULID
|
|
60
64
|
# @param [Time] time
|
61
65
|
# @return [ULID]
|
62
66
|
def self.at(time)
|
63
|
-
raise
|
67
|
+
raise(ArgumentError, 'ULID.at takes only `Time` instance') unless Time === time
|
64
68
|
|
65
69
|
from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
|
66
70
|
end
|
@@ -92,14 +96,14 @@ class ULID
|
|
92
96
|
# * Do not ensure the uniqueness
|
93
97
|
# * Do not take random generator for the arguments
|
94
98
|
# * Raising error instead of truncating elements for the given number
|
95
|
-
def self.sample(
|
99
|
+
def self.sample(number=nil, period: nil)
|
96
100
|
int_generator = (
|
97
101
|
if period
|
98
102
|
ulid_range = range(period)
|
99
103
|
min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
|
100
104
|
|
101
105
|
possibilities = (max - min) + (exclude_end ? 0 : 1)
|
102
|
-
raise
|
106
|
+
raise(ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities") unless possibilities.positive?
|
103
107
|
|
104
108
|
-> {
|
105
109
|
SecureRandom.random_number(possibilities) + min
|
@@ -109,24 +113,21 @@ class ULID
|
|
109
113
|
end
|
110
114
|
)
|
111
115
|
|
112
|
-
case
|
113
|
-
when
|
116
|
+
case number
|
117
|
+
when nil
|
114
118
|
from_integer(int_generator.call)
|
115
|
-
when
|
116
|
-
number = args.first
|
117
|
-
raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
|
118
|
-
|
119
|
+
when Integer
|
119
120
|
if number > MAX_INTEGER || number.negative?
|
120
|
-
raise
|
121
|
+
raise(ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative")
|
121
122
|
end
|
122
123
|
|
123
|
-
if period && (number > possibilities)
|
124
|
-
raise
|
124
|
+
if period && possibilities && (number > possibilities)
|
125
|
+
raise(ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`")
|
125
126
|
end
|
126
127
|
|
127
128
|
Array.new(number) { from_integer(int_generator.call) }
|
128
129
|
else
|
129
|
-
raise
|
130
|
+
raise(ArgumentError, 'accepts no argument or integer only')
|
130
131
|
end
|
131
132
|
end
|
132
133
|
|
@@ -136,11 +137,13 @@ class ULID
|
|
136
137
|
# @yieldreturn [self]
|
137
138
|
def self.scan(string)
|
138
139
|
string = String.try_convert(string)
|
139
|
-
raise
|
140
|
-
return to_enum(
|
140
|
+
raise(ArgumentError, 'ULID.scan takes only strings') unless string
|
141
|
+
return to_enum(:scan, string) unless block_given?
|
141
142
|
|
142
143
|
string.scan(SCANNING_PATTERN) do |matched|
|
143
|
-
|
144
|
+
if String === matched
|
145
|
+
yield(parse(matched))
|
146
|
+
end
|
144
147
|
end
|
145
148
|
self
|
146
149
|
end
|
@@ -150,14 +153,16 @@ class ULID
|
|
150
153
|
# @raise [OverflowError] if the given integer is larger than the ULID limit
|
151
154
|
# @raise [ArgumentError] if the given integer is negative number
|
152
155
|
def self.from_integer(integer)
|
153
|
-
raise
|
154
|
-
raise
|
155
|
-
raise
|
156
|
+
raise(ArgumentError, 'ULID.from_integer takes only `Integer`') unless Integer === integer
|
157
|
+
raise(OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}") unless integer <= MAX_INTEGER
|
158
|
+
raise(ArgumentError, "integer should not be negative: given: #{integer}") if integer.negative?
|
156
159
|
|
157
160
|
n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
|
158
161
|
n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
|
159
162
|
n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
|
160
163
|
|
164
|
+
raise(UnexpectedError) unless n32encoded_timestamp && n32encoded_randomness
|
165
|
+
|
161
166
|
milliseconds = n32encoded_timestamp.to_i(32)
|
162
167
|
entropy = n32encoded_randomness.to_i(32)
|
163
168
|
|
@@ -168,37 +173,43 @@ class ULID
|
|
168
173
|
# @return [Range<ULID>]
|
169
174
|
# @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
|
170
175
|
def self.range(period)
|
171
|
-
raise
|
176
|
+
raise(ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`') unless Range === period
|
172
177
|
|
173
178
|
begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
179
|
+
new_begin, new_end = false, false
|
180
|
+
|
181
|
+
begin_ulid = (
|
182
|
+
case begin_element
|
183
|
+
when Time
|
184
|
+
new_begin = true
|
185
|
+
min(begin_element)
|
186
|
+
when nil
|
187
|
+
MIN
|
188
|
+
when self
|
189
|
+
begin_element
|
190
|
+
else
|
191
|
+
raise(ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}")
|
192
|
+
end
|
193
|
+
)
|
186
194
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
195
|
+
end_ulid = (
|
196
|
+
case end_element
|
197
|
+
when Time
|
198
|
+
new_end = true
|
199
|
+
exclude_end ? min(end_element) : max(end_element)
|
200
|
+
when nil
|
201
|
+
exclude_end = false
|
202
|
+
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
203
|
+
MAX
|
204
|
+
when self
|
205
|
+
end_element
|
206
|
+
else
|
207
|
+
raise(ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}")
|
208
|
+
end
|
209
|
+
)
|
199
210
|
|
200
|
-
begin_ulid.freeze
|
201
|
-
end_ulid.freeze
|
211
|
+
begin_ulid.freeze if new_begin
|
212
|
+
end_ulid.freeze if new_end
|
202
213
|
|
203
214
|
Range.new(begin_ulid, end_ulid, exclude_end)
|
204
215
|
end
|
@@ -206,7 +217,7 @@ class ULID
|
|
206
217
|
# @param [Time] time
|
207
218
|
# @return [Time]
|
208
219
|
def self.floor(time)
|
209
|
-
raise
|
220
|
+
raise(ArgumentError, 'ULID.floor takes only `Time` instance') unless Time === time
|
210
221
|
|
211
222
|
time.floor(3)
|
212
223
|
end
|
@@ -220,9 +231,9 @@ class ULID
|
|
220
231
|
# @api private
|
221
232
|
# @param [Time] time
|
222
233
|
# @return [Integer]
|
223
|
-
private_class_method
|
234
|
+
private_class_method(def self.milliseconds_from_time(time)
|
224
235
|
(time.to_r * 1000).to_i
|
225
|
-
end
|
236
|
+
end)
|
226
237
|
|
227
238
|
# @api private
|
228
239
|
# @param [Time, Integer] moment
|
@@ -234,24 +245,24 @@ class ULID
|
|
234
245
|
when Time
|
235
246
|
milliseconds_from_time(moment)
|
236
247
|
else
|
237
|
-
raise
|
248
|
+
raise(ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`')
|
238
249
|
end
|
239
250
|
end
|
240
251
|
|
241
252
|
# @return [Integer]
|
242
|
-
private_class_method
|
253
|
+
private_class_method(def self.reasonable_entropy
|
243
254
|
SecureRandom.random_number(MAX_ENTROPY)
|
244
|
-
end
|
255
|
+
end)
|
245
256
|
|
246
257
|
# @param [String, #to_str] string
|
247
258
|
# @return [ULID]
|
248
259
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
249
260
|
def self.parse(string)
|
250
261
|
string = String.try_convert(string)
|
251
|
-
raise
|
262
|
+
raise(ArgumentError, 'ULID.parse takes only strings') unless string
|
252
263
|
|
253
264
|
unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
|
254
|
-
raise
|
265
|
+
raise(ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`")
|
255
266
|
end
|
256
267
|
|
257
268
|
from_integer(CrockfordBase32.decode(string))
|
@@ -262,7 +273,7 @@ class ULID
|
|
262
273
|
# @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
|
263
274
|
def self.normalize(string)
|
264
275
|
string = String.try_convert(string)
|
265
|
-
raise
|
276
|
+
raise(ArgumentError, 'ULID.normalize takes only strings') unless string
|
266
277
|
|
267
278
|
normalized_in_crockford = CrockfordBase32.normalize(string)
|
268
279
|
# Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
|
@@ -298,14 +309,14 @@ class ULID
|
|
298
309
|
else
|
299
310
|
object_class_name = safe_get_class_name(object)
|
300
311
|
converted_class_name = safe_get_class_name(converted)
|
301
|
-
raise
|
312
|
+
raise(TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})")
|
302
313
|
end
|
303
314
|
end
|
304
315
|
end
|
305
316
|
|
306
317
|
# @param [BasicObject] object
|
307
318
|
# @return [String]
|
308
|
-
private_class_method
|
319
|
+
private_class_method(def self.safe_get_class_name(object)
|
309
320
|
fallback = 'UnknownObject'
|
310
321
|
|
311
322
|
# This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
|
@@ -314,8 +325,12 @@ class ULID
|
|
314
325
|
begin
|
315
326
|
object.class
|
316
327
|
rescue NoMethodError
|
328
|
+
# steep can't correctly handle singeton class assign. See https://github.com/soutaro/steep/pull/586 for further detail
|
329
|
+
# So this annotation is hack for the type infer.
|
330
|
+
# @type var object: BasicObject
|
331
|
+
# @type var singleton_class: untyped
|
317
332
|
singleton_class = class << object; self; end
|
318
|
-
singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) }
|
333
|
+
(Class === singleton_class) ? singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) } : fallback
|
319
334
|
end
|
320
335
|
)
|
321
336
|
|
@@ -326,7 +341,7 @@ class ULID
|
|
326
341
|
else
|
327
342
|
name || fallback
|
328
343
|
end
|
329
|
-
end
|
344
|
+
end)
|
330
345
|
|
331
346
|
# @api private
|
332
347
|
# @param [Integer] milliseconds
|
@@ -335,10 +350,10 @@ class ULID
|
|
335
350
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
336
351
|
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
337
352
|
def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
|
338
|
-
raise
|
339
|
-
raise
|
340
|
-
raise
|
341
|
-
raise
|
353
|
+
raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
|
354
|
+
raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
|
355
|
+
raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
|
356
|
+
raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
|
342
357
|
|
343
358
|
n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
344
359
|
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
@@ -347,7 +362,8 @@ class ULID
|
|
347
362
|
new(milliseconds: milliseconds, entropy: entropy, integer: integer)
|
348
363
|
end
|
349
364
|
|
350
|
-
|
365
|
+
# @dynamic milliseconds, entropy
|
366
|
+
attr_reader(:milliseconds, :entropy)
|
351
367
|
|
352
368
|
# @api private
|
353
369
|
# @param [Integer] milliseconds
|
@@ -390,7 +406,8 @@ class ULID
|
|
390
406
|
def eql?(other)
|
391
407
|
equal?(other) || (ULID === other && @integer == other.to_i)
|
392
408
|
end
|
393
|
-
|
409
|
+
# @dynamic ==
|
410
|
+
alias_method(:==, :eql?)
|
394
411
|
|
395
412
|
# @return [Boolean]
|
396
413
|
def ===(other)
|
@@ -420,22 +437,22 @@ class ULID
|
|
420
437
|
|
421
438
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
|
422
439
|
def timestamp_octets
|
423
|
-
octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
|
440
|
+
octets.slice(0, TIMESTAMP_OCTETS_LENGTH) || raise(UnexpectedError)
|
424
441
|
end
|
425
442
|
|
426
443
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
427
444
|
def randomness_octets
|
428
|
-
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
|
445
|
+
octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH) || raise(UnexpectedError)
|
429
446
|
end
|
430
447
|
|
431
448
|
# @return [String]
|
432
449
|
def timestamp
|
433
|
-
@timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
|
450
|
+
@timestamp ||= (to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
|
434
451
|
end
|
435
452
|
|
436
453
|
# @return [String]
|
437
454
|
def randomness
|
438
|
-
@randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
|
455
|
+
@randomness ||= (to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
|
439
456
|
end
|
440
457
|
|
441
458
|
# @note Providing for rough operations. The keys and values is not fixed.
|
@@ -461,7 +478,8 @@ class ULID
|
|
461
478
|
ULID.from_integer(succ_int)
|
462
479
|
end
|
463
480
|
end
|
464
|
-
|
481
|
+
# @dynamic next
|
482
|
+
alias_method(:next, :succ)
|
465
483
|
|
466
484
|
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
467
485
|
def pred
|
@@ -513,7 +531,7 @@ class ULID
|
|
513
531
|
self
|
514
532
|
end
|
515
533
|
|
516
|
-
undef_method
|
534
|
+
undef_method(:instance_variable_set)
|
517
535
|
|
518
536
|
private
|
519
537
|
|
@@ -525,13 +543,13 @@ class ULID
|
|
525
543
|
end
|
526
544
|
end
|
527
545
|
|
528
|
-
require_relative
|
529
|
-
require_relative
|
530
|
-
require_relative
|
546
|
+
require_relative('ulid/version')
|
547
|
+
require_relative('ulid/crockford_base32')
|
548
|
+
require_relative('ulid/monotonic_generator')
|
531
549
|
|
532
550
|
class ULID
|
533
551
|
MIN = parse('00000000000000000000000000').freeze
|
534
552
|
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
535
553
|
|
536
|
-
private_constant
|
554
|
+
private_constant(:TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR, :CROCKFORD_BASE32_ENCODING_STRING)
|
537
555
|
end
|
data/sig/ulid.rbs
CHANGED
@@ -36,7 +36,7 @@ class ULID < Object
|
|
36
36
|
end
|
37
37
|
|
38
38
|
module CrockfordBase32
|
39
|
-
class SetupError <
|
39
|
+
class SetupError < UnexpectedError
|
40
40
|
end
|
41
41
|
|
42
42
|
N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
|
@@ -97,9 +97,9 @@ class ULID < Object
|
|
97
97
|
def to_ulid: () -> ULID
|
98
98
|
end
|
99
99
|
|
100
|
-
type
|
101
|
-
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
|
102
|
-
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
100
|
+
type full_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
|
101
|
+
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
|
102
|
+
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
|
103
103
|
type period = Range[Time] | Range[nil] | Range[ULID]
|
104
104
|
|
105
105
|
@string: String?
|
@@ -318,7 +318,7 @@ class ULID < Object
|
|
318
318
|
# # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
|
319
319
|
# ```
|
320
320
|
def self.sample: (?period: period) -> ULID
|
321
|
-
| (Integer number, ?period: period) -> Array[ULID]
|
321
|
+
| (Integer number, ?period: period?) -> Array[ULID]
|
322
322
|
def self.valid?: (untyped) -> bool
|
323
323
|
|
324
324
|
# Returns normalized string
|
@@ -409,7 +409,9 @@ class ULID < Object
|
|
409
409
|
# ulid.to_i #=> 1957909092946624190749577070267409738
|
410
410
|
# ```
|
411
411
|
def to_i: -> Integer
|
412
|
-
|
412
|
+
|
413
|
+
# Returns integer for making as a hash key use-case
|
414
|
+
def hash: -> Integer
|
413
415
|
|
414
416
|
# Basically same as String based sort.
|
415
417
|
#
|
@@ -462,7 +464,7 @@ class ULID < Object
|
|
462
464
|
# Intentionally avoiding to use `Record type` ref: https://github.com/ruby/rbs/blob/4fb4c33b2325d1a73d79ff7aaeb49f21cec1e0e5/docs/syntax.md#record-type
|
463
465
|
# Because the returning values are not fixed.
|
464
466
|
def patterns: -> Hash[Symbol, Regexp | String]
|
465
|
-
def octets: ->
|
467
|
+
def octets: -> full_octets
|
466
468
|
def timestamp_octets: -> timestamp_octets
|
467
469
|
def randomness_octets: -> randomness_octets
|
468
470
|
|
@@ -525,7 +527,7 @@ class ULID < Object
|
|
525
527
|
private
|
526
528
|
|
527
529
|
# A private API. Should not be used in your code.
|
528
|
-
def self.new: (milliseconds: Integer, entropy: Integer, integer: Integer) ->
|
530
|
+
def self.new: (milliseconds: Integer, entropy: Integer, integer: Integer) -> ULID
|
529
531
|
|
530
532
|
# A private API. Should not be used in your code.
|
531
533
|
def self.reasonable_entropy: -> Integer
|
metadata
CHANGED
@@ -1,19 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-ulid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Kamiya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
14
|
-
|
15
|
-
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
16
|
-
Also providing `ruby/rbs` signature files.
|
13
|
+
description: " generator, monotonic generator, parser and manipulations for ULID
|
14
|
+
(ruby/rbs signatures included)\n"
|
17
15
|
email:
|
18
16
|
- kachick1+ruby@gmail.com
|
19
17
|
executables: []
|
@@ -46,7 +44,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
44
|
requirements:
|
47
45
|
- - ">="
|
48
46
|
- !ruby/object:Gem::Version
|
49
|
-
version: 2.7.
|
47
|
+
version: 2.7.2
|
50
48
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
49
|
requirements:
|
52
50
|
- - ">="
|