ruby-ulid 0.0.10 → 0.0.15
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 +99 -47
- data/lib/ulid.rb +116 -22
- data/lib/ulid/monotonic_generator.rb +15 -13
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +19 -8
- metadata +21 -9
- data/Steepfile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a620e80f82e91c778329ccf7da8cca96bda4b12e047610b9543768c5ec85d22
|
4
|
+
data.tar.gz: ea28469d63348758f52e1460b5330ce177ad2281e63060800a0669054758f3ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: acb07ae797c3a06a6626d34e15dc1056a30586e8bc151a3773770259d2f4047d0708593e702fbd67a7609758165f20a15f208fede068717a5555733535ed4bde
|
7
|
+
data.tar.gz: 57c9819d86f2a0f002cf3b9a7b76b3a36bc412fa874d50136ec296f1217fb905fa19feb52de2c9d5250547da384580cf882121f0e331054cd665bd113d1f10e3
|
data/README.md
CHANGED
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
A handy `ULID` library
|
4
4
|
|
5
|
-
The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
|
6
|
-
|
7
|
-
|
8
|
-
This gem aims to provide the generator, monotonic generator, parser and handy manipulation methods for the ID.
|
9
|
-
Also having rbs signature files.
|
5
|
+
The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
|
6
|
+
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
7
|
+
Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
|
10
8
|
|
11
9
|
---
|
12
10
|
|
@@ -30,13 +28,15 @@ Instead, herein is proposed ULID:
|
|
30
28
|
- 1.21e+24 unique ULIDs per millisecond
|
31
29
|
- Lexicographically sortable!
|
32
30
|
- Canonically encoded as a 26 character string, as opposed to the 36 character UUID
|
33
|
-
- Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
31
|
+
- Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) # See also exists issues in [Note](#note)
|
34
32
|
- Case insensitive
|
35
33
|
- No special characters (URL safe)
|
36
34
|
- Monotonic sort order (correctly detects and handles the same millisecond)
|
37
35
|
|
38
36
|
## Install
|
39
37
|
|
38
|
+
Require Ruby 2.6 or later
|
39
|
+
|
40
40
|
```console
|
41
41
|
$ gem install ruby-ulid
|
42
42
|
#=> Installed
|
@@ -57,13 +57,14 @@ ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 7
|
|
57
57
|
ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
|
58
58
|
```
|
59
59
|
|
60
|
-
You can
|
60
|
+
You can get the objects from exists encoded ULIDs
|
61
61
|
|
62
62
|
```ruby
|
63
63
|
ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
|
64
|
+
ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
|
64
65
|
```
|
65
66
|
|
66
|
-
|
67
|
+
ULIDs are sortable when they are generated in different timestamp with milliseconds precision
|
67
68
|
|
68
69
|
```ruby
|
69
70
|
ulids = 1000.times.map do
|
@@ -74,29 +75,71 @@ ulids.sort == ulids #=> true
|
|
74
75
|
ulids.uniq(&:to_time).size #=> 1000
|
75
76
|
```
|
76
77
|
|
77
|
-
|
78
|
+
`ULID.generate` can take fixed `Time` instance
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
|
82
|
+
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
83
|
+
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
84
|
+
|
85
|
+
ulids = 1000.times.map do |n|
|
86
|
+
ULID.generate(moment: time + n)
|
87
|
+
end
|
88
|
+
ulids.sort == ulids #=> true
|
89
|
+
```
|
90
|
+
|
91
|
+
The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
|
78
92
|
|
79
93
|
```ruby
|
80
94
|
ulids = 10000.times.map do
|
81
95
|
ULID.generate
|
82
96
|
end
|
83
|
-
ulids.uniq(&:to_time).size #=> 35 (the
|
97
|
+
ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment)
|
84
98
|
ulids.sort == ulids #=> false
|
99
|
+
```
|
85
100
|
|
101
|
+
If you want to ensure `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
|
102
|
+
(Though it starts with new random value when changed the timestamp)
|
86
103
|
|
104
|
+
```ruby
|
87
105
|
monotonic_generator = ULID::MonotonicGenerator.new
|
88
|
-
|
106
|
+
ulids = 10000.times.map do
|
89
107
|
monotonic_generator.generate
|
90
108
|
end
|
91
|
-
|
92
|
-
|
109
|
+
sample_ulids_by_the_time = ulids.uniq(&:to_time)
|
110
|
+
sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
|
111
|
+
|
112
|
+
# In same milliseconds creation, it just increments the end of randomness part
|
113
|
+
ulids.take(5) #=>
|
114
|
+
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
115
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
|
116
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
|
117
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
|
118
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
|
119
|
+
|
120
|
+
# When the milliseconds is updated, it starts with new randomness
|
121
|
+
sample_ulids_by_the_time.take(5) #=>
|
122
|
+
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
123
|
+
# ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
|
124
|
+
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K),
|
125
|
+
# ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
|
126
|
+
# ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
|
127
|
+
|
128
|
+
ulids.sort == ulids #=> true
|
93
129
|
```
|
94
130
|
|
95
|
-
|
131
|
+
When filtering ULIDs by `Time`, we should consider to handle the precision.
|
132
|
+
So this gem provides `ULID.range` to generate `Range[ULID]` from given `Range[Time]`
|
96
133
|
|
97
134
|
```ruby
|
98
|
-
ULID
|
99
|
-
|
135
|
+
# Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
|
136
|
+
include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
|
137
|
+
exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
|
138
|
+
|
139
|
+
# So you can use the generated range objects as below
|
140
|
+
ulids.grep(include_end)
|
141
|
+
ulids.grep(exclude_end)
|
142
|
+
#=> I hope the results should be actually you want!
|
100
143
|
```
|
101
144
|
|
102
145
|
For rough operations, `ULID.scan` might be useful.
|
@@ -150,50 +193,59 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
|
|
150
193
|
ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
151
194
|
```
|
152
195
|
|
153
|
-
|
154
|
-
|
155
|
-
At first, you should install development dependencies
|
196
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
156
197
|
|
157
|
-
```
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
# Executing first time might take longtime, because development mode dependent active_support via steep
|
198
|
+
```ruby
|
199
|
+
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
|
200
|
+
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000"
|
201
|
+
ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
|
162
202
|
```
|
163
203
|
|
164
|
-
|
204
|
+
`ULID#pred` returns predecessor ULID
|
165
205
|
|
166
|
-
```
|
167
|
-
|
168
|
-
|
169
|
-
|
206
|
+
```ruby
|
207
|
+
ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
|
208
|
+
ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ"
|
209
|
+
ULID.parse('00000000000000000000000000').pred #=> nil
|
170
210
|
```
|
171
211
|
|
212
|
+
### UUIDv4 converter for migration use-cases
|
213
|
+
|
214
|
+
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
215
|
+
The imported timestamp is meaningless. So ULID's benefit will lost
|
216
|
+
|
172
217
|
```ruby
|
173
|
-
#
|
174
|
-
ulid = ULID.
|
175
|
-
|
176
|
-
```
|
218
|
+
# Basically reversible
|
219
|
+
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
220
|
+
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
177
221
|
|
178
|
-
|
222
|
+
uuid_v4s = 10000.times.map { SecureRandom.uuid }
|
223
|
+
uuid_v4s.uniq.size == 10000 #=> Probably `true`
|
179
224
|
|
180
|
-
|
181
|
-
|
182
|
-
$ echo $?
|
183
|
-
0
|
184
|
-
```
|
225
|
+
ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
|
226
|
+
ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
|
185
227
|
|
186
|
-
|
228
|
+
# NOTE: Some boundary values are not reversible. See below.
|
187
229
|
|
188
|
-
|
189
|
-
|
190
|
-
|
230
|
+
ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
|
231
|
+
ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
232
|
+
|
233
|
+
# These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
|
234
|
+
reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
|
235
|
+
reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
|
236
|
+
|
237
|
+
# But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
|
238
|
+
ULID.min == reversed_min #=> false
|
239
|
+
ULID.max == reversed_max #=> false
|
191
240
|
```
|
192
241
|
|
193
|
-
##
|
242
|
+
## References
|
194
243
|
|
195
|
-
- [
|
244
|
+
- [Repository](https://github.com/kachick/ruby-ulid)
|
245
|
+
- [API documents](https://kachick.github.io/ruby-ulid/)
|
246
|
+
- [ulid/spec](https://github.com/ulid/spec)
|
196
247
|
|
197
|
-
##
|
248
|
+
## Note
|
198
249
|
|
199
|
-
|
250
|
+
- Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
251
|
+
- Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
|
data/lib/ulid.rb
CHANGED
@@ -35,7 +35,7 @@ class ULID
|
|
35
35
|
STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
|
36
36
|
|
37
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
|
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
|
39
39
|
|
40
40
|
# Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
|
41
41
|
# @see https://bugs.ruby-lang.org/issues/15958
|
@@ -45,20 +45,19 @@ class ULID
|
|
45
45
|
# @param [Integer] entropy
|
46
46
|
# @return [ULID]
|
47
47
|
def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
|
48
|
-
milliseconds
|
49
|
-
new milliseconds: milliseconds, entropy: entropy
|
48
|
+
new milliseconds: milliseconds_from_moment(moment), entropy: entropy
|
50
49
|
end
|
51
50
|
|
52
51
|
# @param [Integer, Time] moment
|
53
52
|
# @return [ULID]
|
54
53
|
def self.min(moment: 0)
|
55
|
-
generate(moment: moment, entropy: 0)
|
54
|
+
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
56
55
|
end
|
57
56
|
|
58
57
|
# @param [Integer, Time] moment
|
59
58
|
# @return [ULID]
|
60
59
|
def self.max(moment: MAX_MILLISECONDS)
|
61
|
-
generate(moment: moment, entropy: MAX_ENTROPY)
|
60
|
+
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
62
61
|
end
|
63
62
|
|
64
63
|
# @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
|
@@ -107,6 +106,7 @@ class ULID
|
|
107
106
|
# @return [ULID]
|
108
107
|
# @raise [OverflowError] if the given integer is larger than the ULID limit
|
109
108
|
# @raise [ArgumentError] if the given integer is negative number
|
109
|
+
# @todo Need optimized for performance
|
110
110
|
def self.from_integer(integer)
|
111
111
|
integer = integer.to_int
|
112
112
|
raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
|
@@ -121,17 +121,70 @@ class ULID
|
|
121
121
|
new milliseconds: milliseconds, entropy: entropy
|
122
122
|
end
|
123
123
|
|
124
|
+
# @param [Range<Time>, Range<nil>] time_range
|
125
|
+
# @return [Range<ULID>]
|
126
|
+
# @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
|
127
|
+
def self.range(time_range)
|
128
|
+
raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
|
129
|
+
begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
|
130
|
+
|
131
|
+
case begin_time
|
132
|
+
when Time
|
133
|
+
begin_ulid = min(moment: begin_time)
|
134
|
+
when nil
|
135
|
+
begin_ulid = min
|
136
|
+
else
|
137
|
+
raise argument_error_for_range_building(time_range)
|
138
|
+
end
|
139
|
+
|
140
|
+
case end_time
|
141
|
+
when Time
|
142
|
+
if exclude_end
|
143
|
+
end_ulid = min(moment: end_time)
|
144
|
+
else
|
145
|
+
end_ulid = max(moment: end_time)
|
146
|
+
end
|
147
|
+
when nil
|
148
|
+
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
149
|
+
end_ulid = max
|
150
|
+
exclude_end = false
|
151
|
+
else
|
152
|
+
raise argument_error_for_range_building(time_range)
|
153
|
+
end
|
154
|
+
|
155
|
+
begin_ulid.freeze
|
156
|
+
end_ulid.freeze
|
157
|
+
|
158
|
+
Range.new(begin_ulid, end_ulid, exclude_end)
|
159
|
+
end
|
160
|
+
|
161
|
+
# @param [Time] time
|
162
|
+
# @return [Time]
|
163
|
+
def self.floor(time)
|
164
|
+
if RUBY_VERSION >= '2.7'
|
165
|
+
time.floor(3)
|
166
|
+
else
|
167
|
+
Time.at(0, milliseconds_from_time(time), :millisecond)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
124
171
|
# @return [Integer]
|
125
172
|
def self.current_milliseconds
|
126
|
-
|
173
|
+
milliseconds_from_time(Time.now)
|
127
174
|
end
|
128
175
|
|
129
176
|
# @param [Time] time
|
130
177
|
# @return [Integer]
|
131
|
-
def self.
|
178
|
+
def self.milliseconds_from_time(time)
|
132
179
|
(time.to_r * 1000).to_i
|
133
180
|
end
|
134
181
|
|
182
|
+
# @param [Time, Integer] moment
|
183
|
+
# @return [Integer]
|
184
|
+
def self.milliseconds_from_moment(moment)
|
185
|
+
moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
|
186
|
+
end
|
187
|
+
|
135
188
|
# @return [Integer]
|
136
189
|
def self.reasonable_entropy
|
137
190
|
SecureRandom.random_number(MAX_ENTROPY)
|
@@ -167,6 +220,7 @@ class ULID
|
|
167
220
|
true
|
168
221
|
end
|
169
222
|
|
223
|
+
# @api private
|
170
224
|
# @param [Integer] integer
|
171
225
|
# @param [Integer] length
|
172
226
|
# @return [Array<Integer>]
|
@@ -178,6 +232,7 @@ class ULID
|
|
178
232
|
digits.reverse!
|
179
233
|
end
|
180
234
|
|
235
|
+
# @api private
|
181
236
|
# @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
|
182
237
|
# @param [Array<Integer>] reversed_digits
|
183
238
|
# @return [Integer]
|
@@ -190,8 +245,14 @@ class ULID
|
|
190
245
|
num
|
191
246
|
end
|
192
247
|
|
248
|
+
# @return [ArgumentError]
|
249
|
+
private_class_method def self.argument_error_for_range_building(argument)
|
250
|
+
ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
|
251
|
+
end
|
252
|
+
|
193
253
|
attr_reader :milliseconds, :entropy
|
194
254
|
|
255
|
+
# @api private
|
195
256
|
# @param [Integer] milliseconds
|
196
257
|
# @param [Integer] entropy
|
197
258
|
# @return [void]
|
@@ -209,10 +270,9 @@ class ULID
|
|
209
270
|
end
|
210
271
|
|
211
272
|
# @return [String]
|
212
|
-
def
|
273
|
+
def to_s
|
213
274
|
@string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
|
214
275
|
end
|
215
|
-
alias_method :to_s, :to_str
|
216
276
|
|
217
277
|
# @return [Integer]
|
218
278
|
def to_i
|
@@ -227,7 +287,7 @@ class ULID
|
|
227
287
|
|
228
288
|
# @return [String]
|
229
289
|
def inspect
|
230
|
-
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{
|
290
|
+
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
|
231
291
|
end
|
232
292
|
|
233
293
|
# @return [Boolean]
|
@@ -254,7 +314,13 @@ class ULID
|
|
254
314
|
|
255
315
|
# @return [Time]
|
256
316
|
def to_time
|
257
|
-
@time ||=
|
317
|
+
@time ||= begin
|
318
|
+
if RUBY_VERSION >= '2.7'
|
319
|
+
Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
|
320
|
+
else
|
321
|
+
Time.at(0, @milliseconds, :millisecond).utc.freeze
|
322
|
+
end
|
323
|
+
end
|
258
324
|
end
|
259
325
|
|
260
326
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
@@ -292,21 +358,36 @@ class ULID
|
|
292
358
|
@strict_pattern ||= /\A#{pattern.source}\z/i.freeze
|
293
359
|
end
|
294
360
|
|
295
|
-
# @
|
296
|
-
# @return [ULID]
|
361
|
+
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
297
362
|
def next
|
298
|
-
|
363
|
+
next_int = to_i.next
|
364
|
+
return nil if next_int > MAX_INTEGER
|
365
|
+
@next ||= self.class.from_integer(next_int)
|
299
366
|
end
|
300
367
|
alias_method :succ, :next
|
301
368
|
|
369
|
+
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
370
|
+
def pred
|
371
|
+
pre_int = to_i.pred
|
372
|
+
return nil if pre_int.negative?
|
373
|
+
@pred ||= self.class.from_integer(pre_int)
|
374
|
+
end
|
375
|
+
|
376
|
+
# @return [String]
|
377
|
+
def to_uuidv4
|
378
|
+
@uuidv4 ||= begin
|
379
|
+
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
380
|
+
array = octets.pack('C*').unpack('NnnnnN')
|
381
|
+
array[2] = (array[2] & 0x0fff) | 0x4000
|
382
|
+
array[3] = (array[3] & 0x3fff) | 0x8000
|
383
|
+
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
302
387
|
# @return [self]
|
303
388
|
def freeze
|
304
|
-
#
|
305
|
-
|
306
|
-
octets
|
307
|
-
succ
|
308
|
-
to_i
|
309
|
-
strict_pattern
|
389
|
+
# Need to cache before freezing, because frozen objects can't assign instance variables
|
390
|
+
cache_all_instance_variables
|
310
391
|
super
|
311
392
|
end
|
312
393
|
|
@@ -314,7 +395,18 @@ class ULID
|
|
314
395
|
|
315
396
|
# @return [MatchData]
|
316
397
|
def matchdata
|
317
|
-
@matchdata ||= STRICT_PATTERN.match(
|
398
|
+
@matchdata ||= STRICT_PATTERN.match(to_s).freeze
|
399
|
+
end
|
400
|
+
|
401
|
+
# @return [void]
|
402
|
+
def cache_all_instance_variables
|
403
|
+
inspect
|
404
|
+
octets
|
405
|
+
to_i
|
406
|
+
succ
|
407
|
+
pred
|
408
|
+
strict_pattern
|
409
|
+
to_uuidv4
|
318
410
|
end
|
319
411
|
end
|
320
412
|
|
@@ -322,7 +414,9 @@ require_relative 'ulid/version'
|
|
322
414
|
require_relative 'ulid/monotonic_generator'
|
323
415
|
|
324
416
|
class ULID
|
417
|
+
MIN = parse('00000000000000000000000000').freeze
|
418
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
325
419
|
MONOTONIC_GENERATOR = MonotonicGenerator.new
|
326
420
|
|
327
|
-
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
|
421
|
+
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX
|
328
422
|
end
|
@@ -4,35 +4,37 @@
|
|
4
4
|
|
5
5
|
class ULID
|
6
6
|
class MonotonicGenerator
|
7
|
+
# @api private
|
7
8
|
attr_accessor :latest_milliseconds, :latest_entropy
|
8
9
|
|
9
10
|
def initialize
|
10
11
|
reset
|
11
12
|
end
|
12
13
|
|
13
|
-
# @
|
14
|
+
# @param [Time, Integer] moment
|
14
15
|
# @return [ULID]
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
17
|
+
# @raise [ArgumentError] if the given moment(milliseconds) is negative number
|
18
|
+
def generate(moment: ULID.current_milliseconds)
|
19
|
+
milliseconds = ULID.milliseconds_from_moment(moment)
|
20
|
+
raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
|
18
21
|
|
19
|
-
@latest_milliseconds
|
20
|
-
@latest_entropy ||= reasonable_entropy
|
21
|
-
if @latest_milliseconds != milliseconds
|
22
|
+
if @latest_milliseconds < milliseconds
|
22
23
|
@latest_milliseconds = milliseconds
|
23
|
-
@latest_entropy = reasonable_entropy
|
24
|
+
@latest_entropy = ULID.reasonable_entropy
|
24
25
|
else
|
25
26
|
@latest_entropy += 1
|
26
27
|
end
|
27
28
|
|
28
|
-
ULID.new milliseconds:
|
29
|
+
ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
|
29
30
|
end
|
30
31
|
|
31
|
-
# @
|
32
|
+
# @api private
|
33
|
+
# @return [void]
|
32
34
|
def reset
|
33
|
-
@latest_milliseconds =
|
34
|
-
@latest_entropy =
|
35
|
-
|
35
|
+
@latest_milliseconds = 0
|
36
|
+
@latest_entropy = ULID.reasonable_entropy
|
37
|
+
nil
|
36
38
|
end
|
37
39
|
|
38
40
|
# @raise [TypeError] always raises exception and does not freeze self
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -16,8 +16,13 @@ class ULID
|
|
16
16
|
STRICT_PATTERN: Regexp
|
17
17
|
UUIDV4_PATTERN: Regexp
|
18
18
|
MONOTONIC_GENERATOR: MonotonicGenerator
|
19
|
+
MIN: ULID
|
20
|
+
MAX: ULID
|
19
21
|
include Comparable
|
20
22
|
|
23
|
+
# The `moment` is a `Time` or `Intger of the milliseconds`
|
24
|
+
type moment = Time | Integer
|
25
|
+
|
21
26
|
class Error < StandardError
|
22
27
|
end
|
23
28
|
|
@@ -28,15 +33,14 @@ class ULID
|
|
28
33
|
end
|
29
34
|
|
30
35
|
class MonotonicGenerator
|
31
|
-
attr_accessor latest_milliseconds: Integer
|
32
|
-
attr_accessor latest_entropy: Integer
|
36
|
+
attr_accessor latest_milliseconds: Integer
|
37
|
+
attr_accessor latest_entropy: Integer
|
33
38
|
def initialize: -> void
|
34
|
-
def generate: -> ULID
|
39
|
+
def generate: (?moment: moment) -> ULID
|
35
40
|
def reset: -> void
|
36
41
|
def freeze: -> void
|
37
42
|
end
|
38
43
|
|
39
|
-
type moment = Time | Integer
|
40
44
|
type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
41
45
|
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
|
42
46
|
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
@@ -55,12 +59,16 @@ class ULID
|
|
55
59
|
@next: ULID?
|
56
60
|
@pattern: Regexp?
|
57
61
|
@strict_pattern: Regexp?
|
62
|
+
@uuidv4: String?
|
58
63
|
@matchdata: MatchData?
|
59
64
|
|
60
65
|
def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
|
61
66
|
def self.monotonic_generate: -> ULID
|
62
67
|
def self.current_milliseconds: -> Integer
|
63
|
-
def self.
|
68
|
+
def self.milliseconds_from_time: (Time time) -> Integer
|
69
|
+
def self.milliseconds_from_moment: (moment moment) -> Integer
|
70
|
+
def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
|
71
|
+
def self.floor: (Time time) -> Time
|
64
72
|
def self.reasonable_entropy: -> Integer
|
65
73
|
def self.parse: (String string) -> ULID
|
66
74
|
def self.from_uuidv4: (String uuid) -> ULID
|
@@ -75,8 +83,7 @@ class ULID
|
|
75
83
|
attr_reader milliseconds: Integer
|
76
84
|
attr_reader entropy: Integer
|
77
85
|
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
78
|
-
def
|
79
|
-
alias to_s to_str
|
86
|
+
def to_s: -> String
|
80
87
|
def to_i: -> Integer
|
81
88
|
alias hash to_i
|
82
89
|
def <=>: (ULID other) -> Integer
|
@@ -93,10 +100,14 @@ class ULID
|
|
93
100
|
def octets: -> octets
|
94
101
|
def timestamp_octets: -> timestamp_octets
|
95
102
|
def randomness_octets: -> randomness_octets
|
96
|
-
def
|
103
|
+
def to_uuidv4: -> String
|
104
|
+
def next: -> ULID?
|
97
105
|
alias succ next
|
106
|
+
def pred: -> ULID?
|
98
107
|
def freeze: -> self
|
99
108
|
|
100
109
|
private
|
110
|
+
def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
|
101
111
|
def matchdata: -> MatchData
|
112
|
+
def cache_all_instance_variables: -> void
|
102
113
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-ulid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Kamiya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-05-
|
11
|
+
date: 2021-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: integer-base
|
@@ -30,6 +30,20 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 0.2.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rbs
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.2.0
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.2.0
|
33
47
|
- !ruby/object:Gem::Dependency
|
34
48
|
name: benchmark-ips
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,11 +84,10 @@ dependencies:
|
|
70
84
|
- - "<"
|
71
85
|
- !ruby/object:Gem::Version
|
72
86
|
version: '2'
|
73
|
-
description:
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
Also having rbs signature files.
|
87
|
+
description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
|
88
|
+
useful specs for applications (e.g. `Database key`). \n This gem aims to provide
|
89
|
+
the generator, monotonic generator, parser and handy manipulation features around
|
90
|
+
the ULID.\n Also providing `rbs` signature files.\n"
|
78
91
|
email:
|
79
92
|
- kachick1+ruby@gmail.com
|
80
93
|
executables: []
|
@@ -83,7 +96,6 @@ extra_rdoc_files: []
|
|
83
96
|
files:
|
84
97
|
- LICENSE
|
85
98
|
- README.md
|
86
|
-
- Steepfile
|
87
99
|
- lib/ulid.rb
|
88
100
|
- lib/ulid/monotonic_generator.rb
|
89
101
|
- lib/ulid/version.rb
|
@@ -103,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
115
|
requirements:
|
104
116
|
- - ">="
|
105
117
|
- !ruby/object:Gem::Version
|
106
|
-
version:
|
118
|
+
version: 2.6.0
|
107
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
120
|
requirements:
|
109
121
|
- - ">="
|