ruby-ulid 0.0.15 → 0.0.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +95 -13
- data/lib/ulid.rb +68 -2
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +6 -0
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15a83604732cdc37f8015ee9382884e950852f8c9c82aba107ea4b3c065c5a4d
|
4
|
+
data.tar.gz: 06e7b69a5838786f4d23da19f5e1f00fb3173014e7438459165b66703637851a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ca46525e80b1d832a703b8cb867466327de333853ee236654f722dc6abc30f46fd149ca9633c42ececcea08db2a01a6d82ab60f8eeb61ebb947f0441436dd96
|
7
|
+
data.tar.gz: ccd08e78dfb047f82af02eeaf14ccd12fcb05fe882edeb2bf48c2e7ec5b9fd698feccf0b9f9e187aaff97c2e793f671a3f9d32c11e3741656c3df94f944bc808
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
-
|
3
|
+
## Overview
|
4
4
|
|
5
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
6
|
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
@@ -33,16 +33,26 @@ Instead, herein is proposed ULID:
|
|
33
33
|
- No special characters (URL safe)
|
34
34
|
- Monotonic sort order (correctly detects and handles the same millisecond)
|
35
35
|
|
36
|
-
##
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Install
|
37
39
|
|
38
40
|
Require Ruby 2.6 or later
|
39
41
|
|
42
|
+
This command will install the latest version into your environment
|
43
|
+
|
40
44
|
```console
|
41
45
|
$ gem install ruby-ulid
|
42
|
-
|
46
|
+
Should be installed!
|
43
47
|
```
|
44
48
|
|
45
|
-
|
49
|
+
Add this line to your application/library's `Gemfile` is needed in basic use-case
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
gem 'ruby-ulid', '0.0.16'
|
53
|
+
```
|
54
|
+
|
55
|
+
### Generator and Parser
|
46
56
|
|
47
57
|
The generated `ULID` is an object not just a string.
|
48
58
|
It means easily get the timestamps and binary formats.
|
@@ -54,7 +64,6 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
|
|
54
64
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
55
65
|
ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
56
66
|
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
67
|
```
|
59
68
|
|
60
69
|
You can get the objects from exists encoded ULIDs
|
@@ -64,6 +73,8 @@ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259
|
|
64
73
|
ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
|
65
74
|
```
|
66
75
|
|
76
|
+
### Sortable with the timestamp
|
77
|
+
|
67
78
|
ULIDs are sortable when they are generated in different timestamp with milliseconds precision
|
68
79
|
|
69
80
|
```ruby
|
@@ -71,14 +82,14 @@ ulids = 1000.times.map do
|
|
71
82
|
sleep(0.001)
|
72
83
|
ULID.generate
|
73
84
|
end
|
74
|
-
ulids.sort == ulids #=> true
|
75
85
|
ulids.uniq(&:to_time).size #=> 1000
|
86
|
+
ulids.sort == ulids #=> true
|
76
87
|
```
|
77
88
|
|
78
89
|
`ULID.generate` can take fixed `Time` instance
|
79
90
|
|
80
91
|
```ruby
|
81
|
-
time = Time.at(946684800
|
92
|
+
time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
|
82
93
|
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
83
94
|
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
84
95
|
|
@@ -98,7 +109,9 @@ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in en
|
|
98
109
|
ulids.sort == ulids #=> false
|
99
110
|
```
|
100
111
|
|
101
|
-
|
112
|
+
### How to keep `Sortable` even if in same timestamp
|
113
|
+
|
114
|
+
If you want to prefer `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
|
102
115
|
(Though it starts with new random value when changed the timestamp)
|
103
116
|
|
104
117
|
```ruby
|
@@ -128,20 +141,40 @@ sample_ulids_by_the_time.take(5) #=>
|
|
128
141
|
ulids.sort == ulids #=> true
|
129
142
|
```
|
130
143
|
|
131
|
-
|
132
|
-
|
144
|
+
### Filtering IDs with `Time`
|
145
|
+
|
146
|
+
`ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
include_end = ulid1..ulid2
|
150
|
+
exclude_end = ulid1...ulid2
|
151
|
+
|
152
|
+
ulids.grep(one_of_the_above)
|
153
|
+
ulids.grep_v(one_of_the_above)
|
154
|
+
```
|
155
|
+
|
156
|
+
When want to filter ULIDs with `Time`, we should consider to handle the precision.
|
157
|
+
So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
|
133
158
|
|
134
159
|
```ruby
|
135
160
|
# Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
|
136
161
|
include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
|
137
162
|
exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
|
138
163
|
|
164
|
+
# Below patterns are acceptable
|
165
|
+
pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
|
166
|
+
until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
|
167
|
+
until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
|
168
|
+
until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
|
169
|
+
|
139
170
|
# So you can use the generated range objects as below
|
140
|
-
ulids.grep(
|
141
|
-
ulids.
|
171
|
+
ulids.grep(one_of_the_above)
|
172
|
+
ulids.grep_v(one_of_the_above)
|
142
173
|
#=> I hope the results should be actually you want!
|
143
174
|
```
|
144
175
|
|
176
|
+
### Scanner for string (e.g. `JSON`)
|
177
|
+
|
145
178
|
For rough operations, `ULID.scan` might be useful.
|
146
179
|
|
147
180
|
```ruby
|
@@ -182,6 +215,8 @@ ULID.scan(json).to_a
|
|
182
215
|
ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
183
216
|
```
|
184
217
|
|
218
|
+
### Some methods to help manipulations
|
219
|
+
|
185
220
|
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
186
221
|
|
187
222
|
```ruby
|
@@ -193,7 +228,10 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
|
|
193
228
|
ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
194
229
|
```
|
195
230
|
|
196
|
-
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
231
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID.
|
232
|
+
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
233
|
+
|
234
|
+
NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
|
197
235
|
|
198
236
|
```ruby
|
199
237
|
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
|
@@ -239,6 +277,50 @@ ULID.min == reversed_min #=> false
|
|
239
277
|
ULID.max == reversed_max #=> false
|
240
278
|
```
|
241
279
|
|
280
|
+
## How to migrate from other gems
|
281
|
+
|
282
|
+
As far as I know, major prior arts are below
|
283
|
+
|
284
|
+
### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
|
285
|
+
|
286
|
+
It is just providing basic `String` generator only.
|
287
|
+
So you can replace the code as below
|
288
|
+
|
289
|
+
```diff
|
290
|
+
-ULID.generate
|
291
|
+
+ULID.generate.to_s
|
292
|
+
```
|
293
|
+
|
294
|
+
NOTE: It had crucial issue for handling precision, in version before `1.3.0`, when you extract timestamps from old generated ULIDs, it might be not accurate value.
|
295
|
+
|
296
|
+
1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
|
297
|
+
1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
|
298
|
+
1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
|
299
|
+
|
300
|
+
### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
|
301
|
+
|
302
|
+
It is providing basic generator(except monotonic generator) and parser.
|
303
|
+
Major methods can be replaced as below.
|
304
|
+
|
305
|
+
```diff
|
306
|
+
-ULID.generate
|
307
|
+
+ULID.generate.to_s
|
308
|
+
-ULID.at(time)
|
309
|
+
+ULID.generate(moment: time).to_s
|
310
|
+
-ULID.time(string)
|
311
|
+
+ULID.parse(string).to_time
|
312
|
+
-ULID.min_ulid_at(time)
|
313
|
+
+ULID.min(moment: time).to_s
|
314
|
+
-ULID.max_ulid_at(time)
|
315
|
+
+ULID.max(moment: time).to_s
|
316
|
+
```
|
317
|
+
|
318
|
+
NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
|
319
|
+
|
320
|
+
1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
|
321
|
+
1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
|
322
|
+
1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
|
323
|
+
|
242
324
|
## References
|
243
325
|
|
244
326
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
data/lib/ulid.rb
CHANGED
@@ -16,6 +16,7 @@ class ULID
|
|
16
16
|
class Error < StandardError; end
|
17
17
|
class OverflowError < Error; end
|
18
18
|
class ParserError < Error; end
|
19
|
+
class SetupError < ScriptError; end
|
19
20
|
|
20
21
|
encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
21
22
|
# Crockford's Base32. Excluded I, L, O, U.
|
@@ -190,11 +191,13 @@ class ULID
|
|
190
191
|
SecureRandom.random_number(MAX_ENTROPY)
|
191
192
|
end
|
192
193
|
|
194
|
+
# @api private
|
195
|
+
# @deprecated Just exists to compare performance with old implementation. ref: https://github.com/kachick/ruby-ulid/issues/7
|
193
196
|
# @param [String, #to_str] string
|
194
197
|
# @return [ULID]
|
195
198
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
196
199
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
197
|
-
def self.
|
200
|
+
def self.parse_with_integer_base(string)
|
198
201
|
begin
|
199
202
|
string = string.to_str
|
200
203
|
unless string.size == ENCODED_ID_LENGTH
|
@@ -211,6 +214,67 @@ class ULID
|
|
211
214
|
new milliseconds: milliseconds, entropy: entropy
|
212
215
|
end
|
213
216
|
|
217
|
+
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
218
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
219
|
+
|
220
|
+
n32_char_by_number = {}
|
221
|
+
n32_chars.each_with_index do |char, index|
|
222
|
+
n32_char_by_number[index] = char
|
223
|
+
end
|
224
|
+
n32_char_by_number.freeze
|
225
|
+
|
226
|
+
# Currently supporting only for `subset for actual use-case`
|
227
|
+
# See below
|
228
|
+
# * https://github.com/ulid/spec/pull/57
|
229
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
230
|
+
# * https://github.com/kachick/ruby-ulid/issues/78
|
231
|
+
crockford_base32_mappings = {
|
232
|
+
'J' => 18,
|
233
|
+
'K' => 19,
|
234
|
+
'M' => 20,
|
235
|
+
'N' => 21,
|
236
|
+
'P' => 22,
|
237
|
+
'Q' => 23,
|
238
|
+
'R' => 24,
|
239
|
+
'S' => 25,
|
240
|
+
'T' => 26,
|
241
|
+
'V' => 27,
|
242
|
+
'W' => 28,
|
243
|
+
'X' => 29,
|
244
|
+
'Y' => 30,
|
245
|
+
'Z' => 31
|
246
|
+
}.freeze
|
247
|
+
|
248
|
+
REPLACING_MAP = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
|
249
|
+
if n = crockford_base32_mappings[encoding_char]
|
250
|
+
char_32 = n32_char_by_number.fetch(n)
|
251
|
+
map[encoding_char] = char_32
|
252
|
+
end
|
253
|
+
end.freeze
|
254
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless REPLACING_MAP.keys == crockford_base32_mappings.keys
|
255
|
+
REPLACING_PATTERN = /[#{REPLACING_MAP.keys.join}]/.freeze
|
256
|
+
|
257
|
+
def self.parse(string)
|
258
|
+
begin
|
259
|
+
string = string.to_str
|
260
|
+
raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
|
261
|
+
n32encoded = convert_crockford_base32_to_n32(string.upcase)
|
262
|
+
timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
|
263
|
+
randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
264
|
+
milliseconds = timestamp.to_i(32)
|
265
|
+
entropy = randomness.to_i(32)
|
266
|
+
rescue => err
|
267
|
+
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
268
|
+
end
|
269
|
+
|
270
|
+
new milliseconds: milliseconds, entropy: entropy
|
271
|
+
end
|
272
|
+
|
273
|
+
# @api private
|
274
|
+
private_class_method def self.convert_crockford_base32_to_n32(string)
|
275
|
+
string.gsub(REPLACING_PATTERN, REPLACING_MAP)
|
276
|
+
end
|
277
|
+
|
214
278
|
# @return [Boolean]
|
215
279
|
def self.valid?(string)
|
216
280
|
parse(string)
|
@@ -348,11 +412,13 @@ class ULID
|
|
348
412
|
@randomness ||= matchdata[:randomness].freeze
|
349
413
|
end
|
350
414
|
|
415
|
+
# @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
|
351
416
|
# @return [Regexp]
|
352
417
|
def pattern
|
353
418
|
@pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
354
419
|
end
|
355
420
|
|
421
|
+
# @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
|
356
422
|
# @return [Regexp]
|
357
423
|
def strict_pattern
|
358
424
|
@strict_pattern ||= /\A#{pattern.source}\z/i.freeze
|
@@ -418,5 +484,5 @@ class ULID
|
|
418
484
|
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
419
485
|
MONOTONIC_GENERATOR = MonotonicGenerator.new
|
420
486
|
|
421
|
-
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX
|
487
|
+
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :REPLACING_PATTERN, :REPLACING_MAP
|
422
488
|
end
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -15,6 +15,8 @@ class ULID
|
|
15
15
|
PATTERN: Regexp
|
16
16
|
STRICT_PATTERN: Regexp
|
17
17
|
UUIDV4_PATTERN: Regexp
|
18
|
+
REPLACING_MAP: Hash[String, String]
|
19
|
+
REPLACING_PATTERN: Regexp
|
18
20
|
MONOTONIC_GENERATOR: MonotonicGenerator
|
19
21
|
MIN: ULID
|
20
22
|
MAX: ULID
|
@@ -32,6 +34,9 @@ class ULID
|
|
32
34
|
class ParserError < Error
|
33
35
|
end
|
34
36
|
|
37
|
+
class SetupError < ScriptError
|
38
|
+
end
|
39
|
+
|
35
40
|
class MonotonicGenerator
|
36
41
|
attr_accessor latest_milliseconds: Integer
|
37
42
|
attr_accessor latest_entropy: Integer
|
@@ -80,6 +85,7 @@ class ULID
|
|
80
85
|
| (String string) { (ULID ulid) -> void } -> singleton(ULID)
|
81
86
|
def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
|
82
87
|
def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
|
88
|
+
def self.convert_crockford_base32_to_n32: (String) -> String
|
83
89
|
attr_reader milliseconds: Integer
|
84
90
|
attr_reader entropy: Integer
|
85
91
|
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
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.16
|
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-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: integer-base
|
@@ -84,10 +84,10 @@ dependencies:
|
|
84
84
|
- - "<"
|
85
85
|
- !ruby/object:Gem::Version
|
86
86
|
version: '2'
|
87
|
-
description:
|
88
|
-
|
89
|
-
|
90
|
-
|
87
|
+
description: |2
|
88
|
+
The ULID(Universally Unique Lexicographically Sortable Identifier) has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
|
89
|
+
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
90
|
+
Also providing `ruby/rbs` signature files.
|
91
91
|
email:
|
92
92
|
- kachick1+ruby@gmail.com
|
93
93
|
executables: []
|