ruby-ulid 0.0.15 → 0.0.16
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 +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: []
|