ruby-ulid 0.0.9 → 0.0.14
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/LICENSE +21 -0
- data/README.md +225 -0
- data/Steepfile +7 -0
- data/lib/ulid.rb +104 -61
- data/lib/ulid/monotonic_generator.rb +46 -0
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +16 -9
- metadata +26 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa89ecbb2a09940666b57e690d43a985872bb85f40b2b1175c79a762d79c8e45
|
4
|
+
data.tar.gz: 2404fe5e1efa77899262fbf8979529fc474e44ffa0a8572ee3cf71eb1caf941a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a86224846b27985d641bf485f952583c73dafc87a0ca8f65744cfdfa35371a6f5cfedb4246e7839da6e51fdcfd53632f20057c399038f3399f2986a9bf20085
|
7
|
+
data.tar.gz: 24daff089a2b0564a2e7a93c34fef4d44420d1e0bbc6a054c5bcdfac02986c66204515bc085a7dc6499257692682217a888d8a04e04a7ae8de36e884cc182965
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Kenichi Kamiya
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# ruby-ulid
|
2
|
+
|
3
|
+
A handy `ULID` library
|
4
|
+
|
5
|
+
The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
|
6
|
+
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
7
|
+
Also providing rbs signature files.
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+

|
12
|
+
|
13
|
+

|
14
|
+
[](http://badge.fury.io/rb/ruby-ulid)
|
15
|
+
|
16
|
+
## Universally Unique Lexicographically Sortable Identifier
|
17
|
+
|
18
|
+
UUID can be suboptimal for many uses-cases because:
|
19
|
+
|
20
|
+
- It isn't the most character efficient way of encoding 128 bits of randomness
|
21
|
+
- UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address
|
22
|
+
- UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures
|
23
|
+
- UUID v4 provides no other information than randomness which can cause fragmentation in many data structures
|
24
|
+
|
25
|
+
Instead, herein is proposed ULID:
|
26
|
+
|
27
|
+
- 128-bit compatibility with UUID
|
28
|
+
- 1.21e+24 unique ULIDs per millisecond
|
29
|
+
- Lexicographically sortable!
|
30
|
+
- Canonically encoded as a 26 character string, as opposed to the 36 character UUID
|
31
|
+
- Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
32
|
+
- Case insensitive
|
33
|
+
- No special characters (URL safe)
|
34
|
+
- Monotonic sort order (correctly detects and handles the same millisecond)
|
35
|
+
|
36
|
+
## Install
|
37
|
+
|
38
|
+
Require Ruby 2.6 or later
|
39
|
+
|
40
|
+
```console
|
41
|
+
$ gem install ruby-ulid
|
42
|
+
#=> Installed
|
43
|
+
```
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
The generated `ULID` is an object not just a string.
|
48
|
+
It means easily get the timestamps and binary formats.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require 'ulid'
|
52
|
+
|
53
|
+
ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
54
|
+
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
55
|
+
ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
56
|
+
ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
|
57
|
+
ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
|
58
|
+
```
|
59
|
+
|
60
|
+
You can get the objects from exists encoded ULIDs
|
61
|
+
|
62
|
+
```ruby
|
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
|
65
|
+
```
|
66
|
+
|
67
|
+
ULIDs are sortable when they are generated in different timestamp with milliseconds precision
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
ulids = 1000.times.map do
|
71
|
+
sleep(0.001)
|
72
|
+
ULID.generate
|
73
|
+
end
|
74
|
+
ulids.sort == ulids #=> true
|
75
|
+
ulids.uniq(&:to_time).size #=> 1000
|
76
|
+
```
|
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.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
ulids = 10000.times.map do
|
95
|
+
ULID.generate
|
96
|
+
end
|
97
|
+
ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment)
|
98
|
+
ulids.sort == ulids #=> false
|
99
|
+
```
|
100
|
+
|
101
|
+
If you want to prefer `sortable` rather than the `randomness`, 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)
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
monotonic_generator = ULID::MonotonicGenerator.new
|
106
|
+
monotonic_ulids = 10000.times.map do
|
107
|
+
monotonic_generator.generate
|
108
|
+
end
|
109
|
+
sample_ulids_by_the_time = monotonic_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
|
+
monotonic_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
|
+
monotonic_ulids.sort == monotonic_ulids #=> true
|
129
|
+
```
|
130
|
+
|
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]`
|
133
|
+
|
134
|
+
```ruby
|
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!
|
143
|
+
```
|
144
|
+
|
145
|
+
For rough operations, `ULID.scan` might be useful.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
json =<<'EOD'
|
149
|
+
{
|
150
|
+
"id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3",
|
151
|
+
"author": {
|
152
|
+
"id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
|
153
|
+
"name": "kachick"
|
154
|
+
},
|
155
|
+
"title": "My awesome blog post",
|
156
|
+
"comments": [
|
157
|
+
{
|
158
|
+
"id": "01F4GNCNC3CH0BCRZBPPDEKBKS",
|
159
|
+
"commenter": {
|
160
|
+
"id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
|
161
|
+
"name": "kachick"
|
162
|
+
}
|
163
|
+
},
|
164
|
+
{
|
165
|
+
"id": "01F4GNCXAMXQ1SGBH5XCR6ZH0M",
|
166
|
+
"commenter": {
|
167
|
+
"id": "01F4GND4RYYSKNAADHQ9BNXAWJ",
|
168
|
+
"name": "pankona"
|
169
|
+
}
|
170
|
+
}
|
171
|
+
]
|
172
|
+
}
|
173
|
+
EOD
|
174
|
+
|
175
|
+
ULID.scan(json).to_a
|
176
|
+
#=>
|
177
|
+
[ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
|
178
|
+
ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
|
179
|
+
ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
|
180
|
+
ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
|
181
|
+
ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
|
182
|
+
ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
183
|
+
```
|
184
|
+
|
185
|
+
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
|
189
|
+
ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
|
190
|
+
|
191
|
+
time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
|
192
|
+
ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
|
193
|
+
ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
194
|
+
```
|
195
|
+
|
196
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
|
200
|
+
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000"
|
201
|
+
ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
|
202
|
+
```
|
203
|
+
|
204
|
+
`ULID#pred` returns predecessor ULID
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
|
208
|
+
ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ"
|
209
|
+
ULID.parse('00000000000000000000000000').pred #=> nil
|
210
|
+
```
|
211
|
+
|
212
|
+
UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
|
216
|
+
#=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
217
|
+
```
|
218
|
+
|
219
|
+
## References
|
220
|
+
|
221
|
+
- [Repository](https://github.com/kachick/ruby-ulid)
|
222
|
+
- [API documents](https://kachick.github.io/ruby-ulid/)
|
223
|
+
- [ulid/spec](https://github.com/ulid/spec)
|
224
|
+
- [Another choices are UUIDv6, UUIDv7, UUIDv8. 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)
|
225
|
+
- Current parser/validator/matcher implementation aims `strict`, It might be changed in [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57).
|
data/Steepfile
ADDED
data/lib/ulid.rb
CHANGED
@@ -4,7 +4,6 @@
|
|
4
4
|
|
5
5
|
require 'securerandom'
|
6
6
|
require 'integer/base'
|
7
|
-
require_relative 'ulid/version'
|
8
7
|
|
9
8
|
# @see https://github.com/ulid/spec
|
10
9
|
# @!attribute [r] milliseconds
|
@@ -23,16 +22,16 @@ class ULID
|
|
23
22
|
# @see https://www.crockford.com/base32.html
|
24
23
|
ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
|
25
24
|
|
26
|
-
|
25
|
+
TIMESTAMP_PART_LENGTH = 10
|
27
26
|
RANDOMNESS_PART_LENGTH = 16
|
28
|
-
ENCODED_ID_LENGTH =
|
27
|
+
ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
|
29
28
|
TIMESTAMP_OCTETS_LENGTH = 6
|
30
29
|
RANDOMNESS_OCTETS_LENGTH = 10
|
31
30
|
OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
|
32
31
|
MAX_MILLISECONDS = 281474976710655
|
33
32
|
MAX_ENTROPY = 1208925819614629174706175
|
34
33
|
MAX_INTEGER = 340282366920938463463374607431768211455
|
35
|
-
PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{
|
34
|
+
PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
|
36
35
|
STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
|
37
36
|
|
38
37
|
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
@@ -42,55 +41,23 @@ class ULID
|
|
42
41
|
# @see https://bugs.ruby-lang.org/issues/15958
|
43
42
|
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
53
|
-
# @return [ULID]
|
54
|
-
def generate
|
55
|
-
milliseconds = ULID.current_milliseconds
|
56
|
-
reasonable_entropy = ULID.reasonable_entropy
|
57
|
-
|
58
|
-
@latest_milliseconds ||= milliseconds
|
59
|
-
@latest_entropy ||= reasonable_entropy
|
60
|
-
if @latest_milliseconds != milliseconds
|
61
|
-
@latest_milliseconds = milliseconds
|
62
|
-
@latest_entropy = reasonable_entropy
|
63
|
-
else
|
64
|
-
@latest_entropy += 1
|
65
|
-
end
|
66
|
-
|
67
|
-
ULID.new milliseconds: milliseconds, entropy: @latest_entropy
|
68
|
-
end
|
69
|
-
|
70
|
-
# @return [self]
|
71
|
-
def reset
|
72
|
-
@latest_milliseconds = nil
|
73
|
-
@latest_entropy = nil
|
74
|
-
self
|
75
|
-
end
|
76
|
-
|
77
|
-
# @raise [TypeError] always raises exception and does not freeze self
|
78
|
-
# @return [void]
|
79
|
-
def freeze
|
80
|
-
raise TypeError, "cannot freeze #{self.class}"
|
81
|
-
end
|
44
|
+
# @param [Integer, Time] moment
|
45
|
+
# @param [Integer] entropy
|
46
|
+
# @return [ULID]
|
47
|
+
def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
|
48
|
+
new milliseconds: milliseconds_from_moment(moment), entropy: entropy
|
82
49
|
end
|
83
50
|
|
84
|
-
|
85
|
-
|
86
|
-
|
51
|
+
# @param [Integer, Time] moment
|
52
|
+
# @return [ULID]
|
53
|
+
def self.min(moment: 0)
|
54
|
+
generate(moment: moment, entropy: 0)
|
55
|
+
end
|
87
56
|
|
88
57
|
# @param [Integer, Time] moment
|
89
|
-
# @param [Integer] entropy
|
90
58
|
# @return [ULID]
|
91
|
-
def self.
|
92
|
-
|
93
|
-
new milliseconds: milliseconds, entropy: entropy
|
59
|
+
def self.max(moment: MAX_MILLISECONDS)
|
60
|
+
generate(moment: moment, entropy: MAX_ENTROPY)
|
94
61
|
end
|
95
62
|
|
96
63
|
# @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
|
@@ -139,6 +106,7 @@ class ULID
|
|
139
106
|
# @return [ULID]
|
140
107
|
# @raise [OverflowError] if the given integer is larger than the ULID limit
|
141
108
|
# @raise [ArgumentError] if the given integer is negative number
|
109
|
+
# @todo Need optimized for performance
|
142
110
|
def self.from_integer(integer)
|
143
111
|
integer = integer.to_int
|
144
112
|
raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
|
@@ -153,17 +121,66 @@ class ULID
|
|
153
121
|
new milliseconds: milliseconds, entropy: entropy
|
154
122
|
end
|
155
123
|
|
124
|
+
# @param [Range<Time>] time_range
|
125
|
+
# @return [Range<ULID>]
|
126
|
+
def self.range(time_range)
|
127
|
+
raise ArgumentError, 'ULID.range takes only Range[Time]' unless time_range.kind_of?(Range)
|
128
|
+
begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
|
129
|
+
|
130
|
+
case begin_time
|
131
|
+
when Time
|
132
|
+
begin_ulid = min(moment: begin_time)
|
133
|
+
when nil
|
134
|
+
begin_ulid = min
|
135
|
+
else
|
136
|
+
raise ArgumentError, 'ULID.range takes only Range[Time]'
|
137
|
+
end
|
138
|
+
|
139
|
+
case end_time
|
140
|
+
when Time
|
141
|
+
if exclude_end
|
142
|
+
end_ulid = min(moment: end_time)
|
143
|
+
else
|
144
|
+
end_ulid = max(moment: end_time)
|
145
|
+
end
|
146
|
+
when nil
|
147
|
+
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
148
|
+
end_ulid = max
|
149
|
+
exclude_end = false
|
150
|
+
else
|
151
|
+
raise ArgumentError, 'ULID.range takes only Range[Time]'
|
152
|
+
end
|
153
|
+
|
154
|
+
Range.new(begin_ulid, end_ulid, exclude_end)
|
155
|
+
end
|
156
|
+
|
157
|
+
# @param [Time] time
|
158
|
+
# @return [Time]
|
159
|
+
def self.floor(time)
|
160
|
+
if RUBY_VERSION >= '2.7'
|
161
|
+
time.floor(3)
|
162
|
+
else
|
163
|
+
Time.at(0, milliseconds_from_time(time), :millisecond)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
156
167
|
# @return [Integer]
|
157
168
|
def self.current_milliseconds
|
158
|
-
|
169
|
+
milliseconds_from_time(Time.now)
|
159
170
|
end
|
160
171
|
|
161
172
|
# @param [Time] time
|
162
173
|
# @return [Integer]
|
163
|
-
def self.
|
174
|
+
def self.milliseconds_from_time(time)
|
164
175
|
(time.to_r * 1000).to_i
|
165
176
|
end
|
166
177
|
|
178
|
+
# @param [Time, Integer] moment
|
179
|
+
# @return [Integer]
|
180
|
+
def self.milliseconds_from_moment(moment)
|
181
|
+
moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
|
182
|
+
end
|
183
|
+
|
167
184
|
# @return [Integer]
|
168
185
|
def self.reasonable_entropy
|
169
186
|
SecureRandom.random_number(MAX_ENTROPY)
|
@@ -179,8 +196,8 @@ class ULID
|
|
179
196
|
unless string.size == ENCODED_ID_LENGTH
|
180
197
|
raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
|
181
198
|
end
|
182
|
-
timestamp = string.slice(0,
|
183
|
-
randomness = string.slice(
|
199
|
+
timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
|
200
|
+
randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
184
201
|
milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
|
185
202
|
entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
|
186
203
|
rescue => err
|
@@ -199,6 +216,7 @@ class ULID
|
|
199
216
|
true
|
200
217
|
end
|
201
218
|
|
219
|
+
# @api private
|
202
220
|
# @param [Integer] integer
|
203
221
|
# @param [Integer] length
|
204
222
|
# @return [Array<Integer>]
|
@@ -210,6 +228,7 @@ class ULID
|
|
210
228
|
digits.reverse!
|
211
229
|
end
|
212
230
|
|
231
|
+
# @api private
|
213
232
|
# @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
|
214
233
|
# @param [Array<Integer>] reversed_digits
|
215
234
|
# @return [Integer]
|
@@ -224,6 +243,7 @@ class ULID
|
|
224
243
|
|
225
244
|
attr_reader :milliseconds, :entropy
|
226
245
|
|
246
|
+
# @api private
|
227
247
|
# @param [Integer] milliseconds
|
228
248
|
# @param [Integer] entropy
|
229
249
|
# @return [void]
|
@@ -241,10 +261,9 @@ class ULID
|
|
241
261
|
end
|
242
262
|
|
243
263
|
# @return [String]
|
244
|
-
def
|
264
|
+
def to_s
|
245
265
|
@string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
|
246
266
|
end
|
247
|
-
alias_method :to_s, :to_str
|
248
267
|
|
249
268
|
# @return [Integer]
|
250
269
|
def to_i
|
@@ -259,7 +278,7 @@ class ULID
|
|
259
278
|
|
260
279
|
# @return [String]
|
261
280
|
def inspect
|
262
|
-
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{
|
281
|
+
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
|
263
282
|
end
|
264
283
|
|
265
284
|
# @return [Boolean]
|
@@ -286,7 +305,13 @@ class ULID
|
|
286
305
|
|
287
306
|
# @return [Time]
|
288
307
|
def to_time
|
289
|
-
@time ||=
|
308
|
+
@time ||= begin
|
309
|
+
if RUBY_VERSION >= '2.7'
|
310
|
+
Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
|
311
|
+
else
|
312
|
+
Time.at(0, @milliseconds, :millisecond).utc.freeze
|
313
|
+
end
|
314
|
+
end
|
290
315
|
end
|
291
316
|
|
292
317
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
@@ -324,20 +349,29 @@ class ULID
|
|
324
349
|
@strict_pattern ||= /\A#{pattern.source}\z/i.freeze
|
325
350
|
end
|
326
351
|
|
327
|
-
# @
|
328
|
-
# @return [ULID]
|
352
|
+
# @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
|
329
353
|
def next
|
330
|
-
|
354
|
+
next_int = to_i.next
|
355
|
+
return nil if next_int > MAX_INTEGER
|
356
|
+
@next ||= self.class.from_integer(next_int)
|
331
357
|
end
|
332
358
|
alias_method :succ, :next
|
333
359
|
|
360
|
+
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
361
|
+
def pred
|
362
|
+
pre_int = to_i.pred
|
363
|
+
return nil if pre_int.negative?
|
364
|
+
@pred ||= self.class.from_integer(pre_int)
|
365
|
+
end
|
366
|
+
|
334
367
|
# @return [self]
|
335
368
|
def freeze
|
336
369
|
# Evaluate all caching
|
337
370
|
inspect
|
338
371
|
octets
|
339
|
-
succ
|
340
372
|
to_i
|
373
|
+
succ
|
374
|
+
pred
|
341
375
|
strict_pattern
|
342
376
|
super
|
343
377
|
end
|
@@ -346,6 +380,15 @@ class ULID
|
|
346
380
|
|
347
381
|
# @return [MatchData]
|
348
382
|
def matchdata
|
349
|
-
@matchdata ||= STRICT_PATTERN.match(
|
383
|
+
@matchdata ||= STRICT_PATTERN.match(to_s).freeze
|
350
384
|
end
|
351
385
|
end
|
386
|
+
|
387
|
+
require_relative 'ulid/version'
|
388
|
+
require_relative 'ulid/monotonic_generator'
|
389
|
+
|
390
|
+
class ULID
|
391
|
+
MONOTONIC_GENERATOR = MonotonicGenerator.new
|
392
|
+
|
393
|
+
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
|
394
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
4
|
+
|
5
|
+
class ULID
|
6
|
+
class MonotonicGenerator
|
7
|
+
# @api private
|
8
|
+
attr_accessor :latest_milliseconds, :latest_entropy
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
reset
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Time, Integer] moment
|
15
|
+
# @return [ULID]
|
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?
|
21
|
+
|
22
|
+
if @latest_milliseconds < milliseconds
|
23
|
+
@latest_milliseconds = milliseconds
|
24
|
+
@latest_entropy = ULID.reasonable_entropy
|
25
|
+
else
|
26
|
+
@latest_entropy += 1
|
27
|
+
end
|
28
|
+
|
29
|
+
ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
# @return [void]
|
34
|
+
def reset
|
35
|
+
@latest_milliseconds = 0
|
36
|
+
@latest_entropy = ULID.reasonable_entropy
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# @raise [TypeError] always raises exception and does not freeze self
|
41
|
+
# @return [void]
|
42
|
+
def freeze
|
43
|
+
raise TypeError, "cannot freeze #{self.class}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
class ULID
|
3
3
|
VERSION: String
|
4
4
|
ENCODING_CHARS: Array[String]
|
5
|
-
|
5
|
+
TIMESTAMP_PART_LENGTH: 10
|
6
6
|
RANDOMNESS_PART_LENGTH: 16
|
7
7
|
ENCODED_ID_LENGTH: 26
|
8
8
|
TIMESTAMP_OCTETS_LENGTH: 6
|
@@ -18,6 +18,8 @@ class ULID
|
|
18
18
|
MONOTONIC_GENERATOR: MonotonicGenerator
|
19
19
|
include Comparable
|
20
20
|
|
21
|
+
type moment = Time | Integer
|
22
|
+
|
21
23
|
class Error < StandardError
|
22
24
|
end
|
23
25
|
|
@@ -28,10 +30,10 @@ class ULID
|
|
28
30
|
end
|
29
31
|
|
30
32
|
class MonotonicGenerator
|
31
|
-
attr_accessor latest_milliseconds: Integer
|
32
|
-
attr_accessor latest_entropy: Integer
|
33
|
+
attr_accessor latest_milliseconds: Integer
|
34
|
+
attr_accessor latest_entropy: Integer
|
33
35
|
def initialize: -> void
|
34
|
-
def generate: -> ULID
|
36
|
+
def generate: (?moment: moment) -> ULID
|
35
37
|
def reset: -> void
|
36
38
|
def freeze: -> void
|
37
39
|
end
|
@@ -56,14 +58,19 @@ class ULID
|
|
56
58
|
@strict_pattern: Regexp?
|
57
59
|
@matchdata: MatchData?
|
58
60
|
|
59
|
-
def self.generate: (?moment:
|
61
|
+
def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
|
60
62
|
def self.monotonic_generate: -> ULID
|
61
63
|
def self.current_milliseconds: -> Integer
|
62
|
-
def self.
|
64
|
+
def self.milliseconds_from_time: (Time time) -> Integer
|
65
|
+
def self.milliseconds_from_moment: (moment moment) -> Integer
|
66
|
+
def self.range: (Range[Time] time_range) -> Range[ULID]
|
67
|
+
def self.floor: (Time time) -> Time
|
63
68
|
def self.reasonable_entropy: -> Integer
|
64
69
|
def self.parse: (String string) -> ULID
|
65
70
|
def self.from_uuidv4: (String uuid) -> ULID
|
66
71
|
def self.from_integer: (Integer integer) -> ULID
|
72
|
+
def self.min: (?moment: moment) -> ULID
|
73
|
+
def self.max: (?moment: moment) -> ULID
|
67
74
|
def self.valid?: (untyped string) -> bool
|
68
75
|
def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
|
69
76
|
| (String string) { (ULID ulid) -> void } -> singleton(ULID)
|
@@ -72,8 +79,7 @@ class ULID
|
|
72
79
|
attr_reader milliseconds: Integer
|
73
80
|
attr_reader entropy: Integer
|
74
81
|
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
75
|
-
def
|
76
|
-
alias to_s to_str
|
82
|
+
def to_s: -> String
|
77
83
|
def to_i: -> Integer
|
78
84
|
alias hash to_i
|
79
85
|
def <=>: (ULID other) -> Integer
|
@@ -90,8 +96,9 @@ class ULID
|
|
90
96
|
def octets: -> octets
|
91
97
|
def timestamp_octets: -> timestamp_octets
|
92
98
|
def randomness_octets: -> randomness_octets
|
93
|
-
def next: -> ULID
|
99
|
+
def next: -> ULID?
|
94
100
|
alias succ next
|
101
|
+
def pred: -> ULID?
|
95
102
|
def freeze: -> self
|
96
103
|
|
97
104
|
private
|
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.14
|
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-
|
11
|
+
date: 2021-05-03 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,18 +84,21 @@ 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: []
|
81
94
|
extensions: []
|
82
95
|
extra_rdoc_files: []
|
83
96
|
files:
|
97
|
+
- LICENSE
|
98
|
+
- README.md
|
99
|
+
- Steepfile
|
84
100
|
- lib/ulid.rb
|
101
|
+
- lib/ulid/monotonic_generator.rb
|
85
102
|
- lib/ulid/version.rb
|
86
103
|
- sig/ulid.rbs
|
87
104
|
homepage: https://github.com/kachick/ruby-ulid
|
@@ -99,14 +116,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
116
|
requirements:
|
100
117
|
- - ">="
|
101
118
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
119
|
+
version: 2.6.0
|
103
120
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
121
|
requirements:
|
105
122
|
- - ">="
|
106
123
|
- !ruby/object:Gem::Version
|
107
124
|
version: '0'
|
108
125
|
requirements: []
|
109
|
-
rubygems_version: 3.
|
126
|
+
rubygems_version: 3.1.4
|
110
127
|
signing_key:
|
111
128
|
specification_version: 4
|
112
129
|
summary: A handy ULID library
|