ruby-ulid 0.5.0 → 0.7.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 +66 -46
- data/lib/ulid/crockford_base32.rb +62 -51
- data/lib/ulid/errors.rb +10 -0
- data/lib/ulid/monotonic_generator.rb +17 -6
- data/lib/ulid/utils.rb +117 -0
- data/lib/ulid/uuid.rb +3 -1
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +111 -144
- data/sig/ulid.rbs +67 -46
- metadata +4 -3
- data/lib/ulid/ractor_unshareable_constants.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 637c2b95fc9d18a59e1bc6d23a3791bc80362b146c52ff962c407894c29c782e
|
4
|
+
data.tar.gz: 74a9e3ea4be5d9a541d4cb6dd012997406ef3cb68a08f7a1a42959f40f7ab4e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c39c42f8da0aac22356cb26fefb7d23855d118f68148be90c1e253577847b91135f8a04ef1fdca1af59075bd673e61cf2f492f046f30616f7c6b493e28332174
|
7
|
+
data.tar.gz: 623f7f9b4d46e46c2a47a143330b3d1c2cf83a033e83c8e68ab3af13e56f1ff5a082b3a15f57efcf4a3180fcc5786e18e94a0221fd03c44343322d641b69e31a
|
data/README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
-
[](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml?query=branch%3Amain)
|
4
4
|
[](http://badge.fury.io/rb/ruby-ulid)
|
5
5
|
|
6
6
|
## Overview
|
7
7
|
|
8
|
-
[ulid/spec](https://github.com/ulid/spec)
|
9
|
-
Especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `
|
10
|
-
This gem aims to provide the generator, optional monotonicity, parser and other manipulation
|
8
|
+
[ulid/spec](https://github.com/ulid/spec) has some useful features.\
|
9
|
+
Especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortability`.\
|
10
|
+
This gem aims to provide the generator, optional monotonicity, parser and other manipulation ways around ULID with included [RBS](https://github.com/ruby/rbs).
|
11
11
|
|
12
12
|
---
|
13
13
|
|
@@ -49,7 +49,7 @@ Should be installed!
|
|
49
49
|
Add this line in your Gemfile.
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
gem('ruby-ulid', '~> 0.
|
52
|
+
gem('ruby-ulid', '~> 0.7.0')
|
53
53
|
```
|
54
54
|
|
55
55
|
### How to use
|
@@ -58,33 +58,31 @@ gem('ruby-ulid', '~> 0.5.0')
|
|
58
58
|
require 'ulid'
|
59
59
|
|
60
60
|
ULID::VERSION
|
61
|
-
# => "0.
|
61
|
+
# => "0.7.0"
|
62
62
|
```
|
63
63
|
|
64
|
-
|
64
|
+
NOTE: This README includes info about development version. If you would see released version's one. [Look at the ref](https://github.com/kachick/ruby-ulid/tree/v0.7.0).
|
65
65
|
|
66
|
-
|
66
|
+
### Generator and Parser
|
67
|
+
|
68
|
+
`ULID.generate` returns `ULID` instance. It is not just a string.
|
67
69
|
|
68
70
|
```ruby
|
69
71
|
ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
70
72
|
```
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
Get the objects from exists encoded ULIDs.
|
74
|
+
`ULID.parse` returns `ULID` instance from exists encoded ULIDs.
|
75
75
|
|
76
76
|
```ruby
|
77
|
-
ulid = ULID.parse('
|
77
|
+
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
78
78
|
```
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
Extract timestamps and binary formats.
|
80
|
+
It is helpful to inspect.
|
83
81
|
|
84
82
|
```ruby
|
85
|
-
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
86
83
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
87
84
|
ulid.milliseconds #=> 1619544442826
|
85
|
+
ulid.encode #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
88
86
|
ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
89
87
|
ulid.timestamp #=> "01F4A5Y1YA"
|
90
88
|
ulid.randomness #=> "QCYAYCTC7GRMJ9AA"
|
@@ -92,6 +90,42 @@ ulid.to_i #=> 1957909092946624190749577070267409738
|
|
92
90
|
ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
|
93
91
|
```
|
94
92
|
|
93
|
+
`ULID.generate` can take fixed `Time` instance. `ULID.at` is the shorthand.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
|
97
|
+
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
98
|
+
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
99
|
+
ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
|
100
|
+
```
|
101
|
+
|
102
|
+
Also `ULID.encode` and `ULID.decode_time` can be used to get primitive values for most usecases.
|
103
|
+
|
104
|
+
`ULID.encode` returns [normalized](#variants-of-format) String without ULID object creation.\
|
105
|
+
It can take same arguments as `ULID.generate`.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
ULID.encode #=> "01G86M42Q6SJ9XQM2ZRM6JRDSF"
|
109
|
+
ULID.encode(moment: Time.at(946684800).utc) #=> "00VHNCZB00SYG7RCEXZC9DA4E1"
|
110
|
+
```
|
111
|
+
|
112
|
+
`ULID.decode_time` returns Time. It can take `in` keyarg as same as `Time.at`.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1') #=> 2000-01-01 00:00:00 UTC
|
116
|
+
ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1', in: '+09:00') #=> 2000-01-01 09:00:00 +0900
|
117
|
+
```
|
118
|
+
|
119
|
+
This project does not prioritize on the speed. However it actually works faster than others! :zap:
|
120
|
+
|
121
|
+
Snapshot on 0.7.0 is below
|
122
|
+
|
123
|
+
- Generator is 1.6x faster than - [ulid gem](https://github.com/rafaelsales/ulid)
|
124
|
+
- Generator is 1.9x faster than - [ulid-ruby gem](https://github.com/abachman/ulid-ruby)
|
125
|
+
- Parser is 2.6x faster than - [ulid-ruby gem](https://github.com/abachman/ulid-ruby)
|
126
|
+
|
127
|
+
You can see further detail at [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
|
128
|
+
|
95
129
|
### Sortable with the timestamp
|
96
130
|
|
97
131
|
ULIDs are sortable when they are generated in different timestamp with milliseconds precision.
|
@@ -105,20 +139,6 @@ ulids.uniq(&:to_time).size #=> 1000
|
|
105
139
|
ulids.sort == ulids #=> true
|
106
140
|
```
|
107
141
|
|
108
|
-
`ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`.
|
109
|
-
|
110
|
-
```ruby
|
111
|
-
time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
|
112
|
-
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
113
|
-
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
114
|
-
ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
|
115
|
-
|
116
|
-
ulids = 1000.times.map do |n|
|
117
|
-
ULID.at(time + n)
|
118
|
-
end
|
119
|
-
ulids.sort == ulids #=> true
|
120
|
-
```
|
121
|
-
|
122
142
|
Basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
|
123
143
|
|
124
144
|
```ruby
|
@@ -131,7 +151,7 @@ ulids.sort == ulids #=> false
|
|
131
151
|
|
132
152
|
### How to keep `Sortable` even if in same timestamp
|
133
153
|
|
134
|
-
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
|
154
|
+
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.\
|
135
155
|
(Though it starts with new random value when changed the timestamp)
|
136
156
|
|
137
157
|
```ruby
|
@@ -161,7 +181,7 @@ sample_ulids_by_the_time.take(5) #=>
|
|
161
181
|
ulids.sort == ulids #=> true
|
162
182
|
```
|
163
183
|
|
164
|
-
Same
|
184
|
+
Same instance of `ULID::MonotonicGenerator` does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255).
|
165
185
|
|
166
186
|
### Filtering IDs with `Time`
|
167
187
|
|
@@ -175,7 +195,7 @@ ulids.grep(one_of_the_above)
|
|
175
195
|
ulids.grep_v(one_of_the_above)
|
176
196
|
```
|
177
197
|
|
178
|
-
When want to filter ULIDs with `Time`, we should consider to handle the precision
|
198
|
+
When want to filter ULIDs with `Time`, we should consider to handle the precision.\
|
179
199
|
So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
|
180
200
|
|
181
201
|
```ruby
|
@@ -274,10 +294,10 @@ ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
|
274
294
|
|
275
295
|
#### As element in Enumerable
|
276
296
|
|
277
|
-
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
297
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID.\
|
278
298
|
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
279
299
|
|
280
|
-
NOTE: However basically `Range[ULID]#each` should not be used.
|
300
|
+
NOTE: However basically `Range[ULID]#each` should not be used. Incrementing 128 bits IDs are not reasonable operation in most cases.
|
281
301
|
|
282
302
|
```ruby
|
283
303
|
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
|
@@ -340,15 +360,15 @@ ULID.sample(5, period: ulid1.to_time..ulid2.to_time)
|
|
340
360
|
|
341
361
|
I'm afraid so, we should consider [Current ULID spec](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#universally-unique-lexicographically-sortable-identifier) has `orthographical variants of the format` possibilities.
|
342
362
|
|
343
|
-
>Case insensitive
|
363
|
+
> Case insensitive
|
344
364
|
|
345
|
-
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase
|
365
|
+
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase.\
|
346
366
|
However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
|
347
367
|
|
348
|
-
>Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
368
|
+
> Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
349
369
|
|
350
|
-
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0
|
351
|
-
And accepts freestyle inserting `Hyphens (-)
|
370
|
+
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.\
|
371
|
+
And accepts freestyle inserting `Hyphens (-)`.\
|
352
372
|
To consider this patterns or not is different in each implementations.
|
353
373
|
|
354
374
|
I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57).
|
@@ -367,7 +387,7 @@ ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:
|
|
367
387
|
|
368
388
|
#### UUIDv4 converter (experimental)
|
369
389
|
|
370
|
-
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter
|
390
|
+
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.\
|
371
391
|
The imported timestamp is meaningless. So ULID's benefit will lost.
|
372
392
|
|
373
393
|
```ruby
|
@@ -406,11 +426,11 @@ See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-
|
|
406
426
|
|
407
427
|
Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
|
408
428
|
|
409
|
-
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) +
|
429
|
+
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).
|
410
430
|
|
411
|
-
|
412
|
-
|
413
|
-
|
431
|
+
- 
|
432
|
+
- 
|
433
|
+
- 
|
414
434
|
|
415
435
|
## References
|
416
436
|
|
@@ -420,5 +440,5 @@ I have checked the behavior with [ruby/rbs@2.6.0](https://github.com/ruby/rbs) +
|
|
420
440
|
|
421
441
|
## Note
|
422
442
|
|
423
|
-
- [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
|
443
|
+
- [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.\
|
424
444
|
However they are stayed in draft state. ref: [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
-
# shareable_constant_value: literal
|
4
3
|
|
5
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
5
|
|
6
|
+
require_relative('utils')
|
7
|
+
|
7
8
|
class ULID
|
8
9
|
# @see https://www.crockford.com/base32.html
|
9
10
|
#
|
@@ -15,79 +16,89 @@ class ULID
|
|
15
16
|
# * https://github.com/kachick/ruby-ulid/issues/57
|
16
17
|
# * https://github.com/kachick/ruby-ulid/issues/78
|
17
18
|
module CrockfordBase32
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
'
|
31
|
-
'
|
32
|
-
'
|
33
|
-
'
|
34
|
-
'
|
35
|
-
'
|
36
|
-
'
|
37
|
-
'S' => 25,
|
38
|
-
'T' => 26,
|
39
|
-
'V' => 27,
|
40
|
-
'W' => 28,
|
41
|
-
'X' => 29,
|
42
|
-
'Y' => 30,
|
43
|
-
'Z' => 31
|
19
|
+
same_definitions = {
|
20
|
+
'0' => '0',
|
21
|
+
'1' => '1',
|
22
|
+
'2' => '2',
|
23
|
+
'3' => '3',
|
24
|
+
'4' => '4',
|
25
|
+
'5' => '5',
|
26
|
+
'6' => '6',
|
27
|
+
'7' => '7',
|
28
|
+
'8' => '8',
|
29
|
+
'9' => '9',
|
30
|
+
'A' => 'A',
|
31
|
+
'B' => 'B',
|
32
|
+
'C' => 'C',
|
33
|
+
'D' => 'D',
|
34
|
+
'E' => 'E',
|
35
|
+
'F' => 'F',
|
36
|
+
'G' => 'G',
|
37
|
+
'H' => 'H'
|
44
38
|
}.freeze
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
40
|
+
# Excluded I, L, O, U, - from Base32
|
41
|
+
base32_to_crockford = {
|
42
|
+
'I' => 'J',
|
43
|
+
'J' => 'K',
|
44
|
+
'K' => 'M',
|
45
|
+
'L' => 'N',
|
46
|
+
'M' => 'P',
|
47
|
+
'N' => 'Q',
|
48
|
+
'O' => 'R',
|
49
|
+
'P' => 'S',
|
50
|
+
'Q' => 'T',
|
51
|
+
'R' => 'V',
|
52
|
+
'S' => 'W',
|
53
|
+
'T' => 'X',
|
54
|
+
'U' => 'Y',
|
55
|
+
'V' => 'Z'
|
56
|
+
}.freeze
|
57
|
+
BASE32_TR_PATTERN = base32_to_crockford.keys.join.freeze
|
58
|
+
CROCKFORD_TR_PATTERN = base32_to_crockford.values.join.freeze
|
59
|
+
ENCODING_STRING = "#{same_definitions.values.join}#{CROCKFORD_TR_PATTERN}".freeze
|
58
60
|
|
59
|
-
|
61
|
+
variant_to_normarized = {
|
60
62
|
'L' => '1',
|
61
63
|
'l' => '1',
|
62
64
|
'I' => '1',
|
63
65
|
'i' => '1',
|
64
66
|
'O' => '0',
|
65
|
-
'o' => '0'
|
66
|
-
'-' => ''
|
67
|
+
'o' => '0'
|
67
68
|
}.freeze
|
68
|
-
|
69
|
+
VARIANT_TR_PATTERN = variant_to_normarized.keys.join.freeze
|
70
|
+
NORMALIZED_TR_PATTERN = variant_to_normarized.values.join.freeze
|
71
|
+
|
72
|
+
Utils.make_sharable_constantans(self)
|
73
|
+
|
74
|
+
# @note Avoid to depend regex as possible. `tr(string, string)` is almost 2x Faster than `gsub(regex, hash)` in Ruby 3.1
|
69
75
|
|
70
|
-
# @api private
|
71
76
|
# @param [String] string
|
72
77
|
# @return [Integer]
|
73
78
|
def self.decode(string)
|
74
|
-
|
75
|
-
|
79
|
+
base32encoded = string.upcase.tr(CROCKFORD_TR_PATTERN, BASE32_TR_PATTERN)
|
80
|
+
Integer(base32encoded, 32, exception: true)
|
76
81
|
end
|
77
82
|
|
78
|
-
# @api private
|
79
83
|
# @param [Integer] integer
|
80
84
|
# @return [String]
|
81
85
|
def self.encode(integer)
|
82
|
-
|
83
|
-
|
86
|
+
base32encoded = integer.to_s(32)
|
87
|
+
from_base32(base32encoded).rjust(ENCODED_LENGTH, '0')
|
84
88
|
end
|
85
89
|
|
86
|
-
# @api private
|
87
90
|
# @param [String] string
|
88
91
|
# @return [String]
|
89
92
|
def self.normalize(string)
|
90
|
-
string.
|
93
|
+
string.delete('-').tr(VARIANT_TR_PATTERN, NORMALIZED_TR_PATTERN)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param [String] base32encoded
|
97
|
+
# @return [String]
|
98
|
+
def self.from_base32(base32encoded)
|
99
|
+
base32encoded.upcase.tr(BASE32_TR_PATTERN, CROCKFORD_TR_PATTERN)
|
91
100
|
end
|
92
101
|
end
|
102
|
+
|
103
|
+
private_constant(:CrockfordBase32)
|
93
104
|
end
|
data/lib/ulid/errors.rb
ADDED
@@ -4,15 +4,18 @@
|
|
4
4
|
|
5
5
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
6
|
|
7
|
+
require_relative('errors')
|
8
|
+
require_relative('utils')
|
9
|
+
|
7
10
|
class ULID
|
8
11
|
class MonotonicGenerator
|
9
12
|
# @note When use https://github.com/ko1/ractor-tvar might realize Ractor based thread safe monotonic generator.
|
10
13
|
# However it is a C extention, I'm pending to use it for now.
|
11
14
|
include(MonitorMixin)
|
12
15
|
|
13
|
-
# @dynamic prev
|
14
16
|
# @return [ULID, nil]
|
15
|
-
|
17
|
+
attr_accessor(:prev)
|
18
|
+
private(:prev=)
|
16
19
|
|
17
20
|
undef_method(:instance_variable_set)
|
18
21
|
|
@@ -25,7 +28,6 @@ class ULID
|
|
25
28
|
def inspect
|
26
29
|
"ULID::MonotonicGenerator(prev: #{@prev.inspect})"
|
27
30
|
end
|
28
|
-
# @dynamic to_s
|
29
31
|
alias_method(:to_s, :inspect)
|
30
32
|
|
31
33
|
# @param [Time, Integer] moment
|
@@ -33,7 +35,7 @@ class ULID
|
|
33
35
|
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
34
36
|
# @raise [UnexpectedError] if the generated ULID is an invalid value in monotonicity spec.
|
35
37
|
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
36
|
-
def generate(moment:
|
38
|
+
def generate(moment: Utils.current_milliseconds)
|
37
39
|
synchronize do
|
38
40
|
prev_ulid = @prev
|
39
41
|
unless prev_ulid
|
@@ -42,13 +44,13 @@ class ULID
|
|
42
44
|
return ret
|
43
45
|
end
|
44
46
|
|
45
|
-
milliseconds =
|
47
|
+
milliseconds = Utils.milliseconds_from_moment(moment)
|
46
48
|
|
47
49
|
ulid = (
|
48
50
|
if prev_ulid.milliseconds < milliseconds
|
49
51
|
ULID.generate(moment: milliseconds)
|
50
52
|
else
|
51
|
-
ULID.
|
53
|
+
ULID.generate(moment: prev_ulid.milliseconds, entropy: prev_ulid.entropy.succ)
|
52
54
|
end
|
53
55
|
)
|
54
56
|
|
@@ -70,6 +72,15 @@ class ULID
|
|
70
72
|
end
|
71
73
|
end
|
72
74
|
|
75
|
+
# Just providing similar api as `ULID.generate` and `ULID.encode` relation. No performance benefit exists in monotonic generator's one.
|
76
|
+
#
|
77
|
+
# @see https://github.com/kachick/ruby-ulid/pull/220
|
78
|
+
# @param [Time, Integer] moment
|
79
|
+
# @return [String]
|
80
|
+
def encode(moment: Utils.current_milliseconds)
|
81
|
+
generate(moment: moment).encode
|
82
|
+
end
|
83
|
+
|
73
84
|
undef_method(:freeze)
|
74
85
|
|
75
86
|
# @raise [TypeError] always raises exception and does not freeze self
|
data/lib/ulid/utils.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# shareable_constant_value: literal
|
4
|
+
|
5
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
6
|
+
|
7
|
+
require('securerandom')
|
8
|
+
|
9
|
+
class ULID
|
10
|
+
# @note I don't have confidence for the naming of `Utils`. However some standard libraries have same name.
|
11
|
+
# https://github.com/ruby/webrick/blob/14612a7540fdd7373344461851c4bfff64985b3e/lib/webrick/utils.rb#L17
|
12
|
+
# https://docs.ruby-lang.org/ja/latest/class/ERB=3a=3aUtil.html
|
13
|
+
# https://github.com/ruby/rss/blob/af1c3c9c9630ec0a48abec48ed1ef348ba82aa13/lib/rss/utils.rb#L9
|
14
|
+
module Utils
|
15
|
+
# @return [Integer]
|
16
|
+
def self.current_milliseconds
|
17
|
+
milliseconds_from_time(Time.now)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Time] time
|
21
|
+
# @return [Integer]
|
22
|
+
def self.milliseconds_from_time(time)
|
23
|
+
(time.to_r * 1000).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [Time, Integer] moment
|
27
|
+
# @return [Integer]
|
28
|
+
def self.milliseconds_from_moment(moment)
|
29
|
+
case moment
|
30
|
+
when Integer
|
31
|
+
moment
|
32
|
+
when Time
|
33
|
+
milliseconds_from_time(moment)
|
34
|
+
else
|
35
|
+
raise(ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Integer]
|
40
|
+
def self.reasonable_entropy
|
41
|
+
SecureRandom.random_number(MAX_ENTROPY)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Integer] milliseconds
|
45
|
+
# @param [Integer] entropy
|
46
|
+
# @return [String]
|
47
|
+
# @raise [OverflowError] if the given value is larger than the ULID limit
|
48
|
+
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
49
|
+
def self.encode_base32(milliseconds:, entropy:)
|
50
|
+
raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
|
51
|
+
raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
|
52
|
+
raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
|
53
|
+
raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
|
54
|
+
|
55
|
+
base32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
56
|
+
base32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
57
|
+
"#{base32encoded_timestamp}#{base32encoded_randomness}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [BasicObject] object
|
61
|
+
# @return [String]
|
62
|
+
def self.safe_get_class_name(object)
|
63
|
+
fallback = 'UnknownObject'
|
64
|
+
|
65
|
+
# This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
|
66
|
+
# ref: https://twitter.com/_kachick/status/1400064896759304196
|
67
|
+
klass = (
|
68
|
+
begin
|
69
|
+
object.class
|
70
|
+
rescue NoMethodError
|
71
|
+
# steep can't correctly handle singleton class assign. See https://github.com/soutaro/steep/pull/586 for further detail
|
72
|
+
# So this annotation is hack for the type infer.
|
73
|
+
# @type var object: BasicObject
|
74
|
+
# @type var singleton_class: untyped
|
75
|
+
singleton_class = class << object; self; end
|
76
|
+
(Class === singleton_class) ? singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) } : fallback
|
77
|
+
end
|
78
|
+
)
|
79
|
+
|
80
|
+
begin
|
81
|
+
name = String.try_convert(klass.name)
|
82
|
+
rescue Exception
|
83
|
+
fallback
|
84
|
+
else
|
85
|
+
name || fallback
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.make_sharable_value(value)
|
90
|
+
value.freeze
|
91
|
+
if defined?(Ractor)
|
92
|
+
case value
|
93
|
+
when ULID, Time
|
94
|
+
if ractor_can_make_shareable_time?
|
95
|
+
Ractor.make_shareable(value)
|
96
|
+
end
|
97
|
+
else
|
98
|
+
Ractor.make_shareable(value)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @note Call before Module#private_constant
|
104
|
+
def self.make_sharable_constantans(mod)
|
105
|
+
mod.constants.each do |const_name|
|
106
|
+
value = mod.const_get(const_name)
|
107
|
+
make_sharable_value(value)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.ractor_can_make_shareable_time?
|
112
|
+
RUBY_VERSION >= '3.1'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private_constant(:Utils)
|
117
|
+
end
|
data/lib/ulid/uuid.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
-
# shareable_constant_value: literal
|
4
3
|
|
5
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
5
|
|
6
|
+
require_relative('errors')
|
7
|
+
|
7
8
|
# Extracted features around UUID from some reasons
|
8
9
|
# ref:
|
9
10
|
# * https://github.com/kachick/ruby-ulid/issues/105
|
@@ -11,6 +12,7 @@
|
|
11
12
|
class ULID
|
12
13
|
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
13
14
|
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
|
15
|
+
Utils.make_sharable_value(UUIDV4_PATTERN)
|
14
16
|
private_constant(:UUIDV4_PATTERN)
|
15
17
|
|
16
18
|
# @param [String, #to_str] uuid
|
data/lib/ulid/version.rb
CHANGED
data/lib/ulid.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
-
# shareable_constant_value: experimental_everything
|
4
3
|
|
5
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
6
5
|
|
7
6
|
require('securerandom')
|
8
7
|
|
8
|
+
require_relative('ulid/version')
|
9
|
+
require_relative('ulid/errors')
|
10
|
+
require_relative('ulid/crockford_base32')
|
11
|
+
require_relative('ulid/utils')
|
12
|
+
require_relative('ulid/monotonic_generator')
|
13
|
+
|
9
14
|
# @see https://github.com/ulid/spec
|
10
15
|
# @!attribute [r] milliseconds
|
11
16
|
# @return [Integer]
|
@@ -14,16 +19,6 @@ require('securerandom')
|
|
14
19
|
class ULID
|
15
20
|
include(Comparable)
|
16
21
|
|
17
|
-
class Error < StandardError; end
|
18
|
-
class OverflowError < Error; end
|
19
|
-
class ParserError < Error; end
|
20
|
-
class UnexpectedError < Error; end
|
21
|
-
|
22
|
-
# Excluded I, L, O, U, -.
|
23
|
-
# This is the encoding patterns.
|
24
|
-
# The decoding issue is written in ULID::CrockfordBase32
|
25
|
-
CROCKFORD_BASE32_ENCODING_STRING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
26
|
-
|
27
22
|
TIMESTAMP_ENCODED_LENGTH = 10
|
28
23
|
RANDOMNESS_ENCODED_LENGTH = 16
|
29
24
|
ENCODED_LENGTH = 26
|
@@ -38,12 +33,12 @@ class ULID
|
|
38
33
|
|
39
34
|
# @see https://github.com/ulid/spec/pull/57
|
40
35
|
# Currently not used as a constant, but kept as a reference for now.
|
41
|
-
PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{
|
36
|
+
PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /(?<timestamp>[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}})(?<randomness>[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}})/i.freeze
|
42
37
|
|
43
38
|
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
|
44
39
|
|
45
40
|
# Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
|
46
|
-
SCANNING_PATTERN = /\b[0-7][#{
|
41
|
+
SCANNING_PATTERN = /\b[0-7][#{CrockfordBase32::ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CrockfordBase32::ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}\b/i.freeze
|
47
42
|
|
48
43
|
# Similar as Time#inspect since Ruby 2.7, however it is NOT same.
|
49
44
|
# Time#inspect trancates needless digits. Keeping full milliseconds with "%3N" will fit for ULID.
|
@@ -51,13 +46,46 @@ class ULID
|
|
51
46
|
# @see https://github.com/ruby/ruby/blob/744d17ff6c33b09334508e8110007ea2a82252f5/time.c#L4026-L4078
|
52
47
|
TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
|
53
48
|
|
49
|
+
RANDOM_INTEGER_GENERATOR = -> {
|
50
|
+
SecureRandom.random_number(MAX_INTEGER)
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
Utils.make_sharable_constantans(self)
|
54
|
+
|
55
|
+
private_constant(
|
56
|
+
:PATTERN_WITH_CROCKFORD_BASE32_SUBSET,
|
57
|
+
:STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET,
|
58
|
+
:SCANNING_PATTERN,
|
59
|
+
:TIME_FORMAT_IN_INSPECT,
|
60
|
+
:RANDOM_INTEGER_GENERATOR
|
61
|
+
)
|
62
|
+
|
54
63
|
private_class_method(:new)
|
55
64
|
|
56
65
|
# @param [Integer, Time] moment
|
57
66
|
# @param [Integer] entropy
|
58
67
|
# @return [ULID]
|
59
|
-
|
60
|
-
|
68
|
+
# @raise [OverflowError] if the given value is larger than the ULID limit
|
69
|
+
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
70
|
+
def self.generate(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy)
|
71
|
+
milliseconds = Utils.milliseconds_from_moment(moment)
|
72
|
+
base32_encoded = Utils.encode_base32(milliseconds: milliseconds, entropy: entropy)
|
73
|
+
new(
|
74
|
+
milliseconds: milliseconds,
|
75
|
+
entropy: entropy,
|
76
|
+
integer: base32_encoded.to_i(32),
|
77
|
+
encoded: CrockfordBase32.from_base32(base32_encoded).freeze
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Almost same as [.generate] except directly returning String without needless object creation
|
82
|
+
#
|
83
|
+
# @param [Integer, Time] moment
|
84
|
+
# @param [Integer] entropy
|
85
|
+
# @return [String]
|
86
|
+
def self.encode(moment: Utils.current_milliseconds, entropy: Utils.reasonable_entropy)
|
87
|
+
base32_encoded = Utils.encode_base32(milliseconds: Utils.milliseconds_from_moment(moment), entropy: entropy)
|
88
|
+
CrockfordBase32.from_base32(base32_encoded)
|
61
89
|
end
|
62
90
|
|
63
91
|
# Short hand of `ULID.generate(moment: time)`
|
@@ -66,7 +94,7 @@ class ULID
|
|
66
94
|
def self.at(time)
|
67
95
|
raise(ArgumentError, 'ULID.at takes only `Time` instance') unless Time === time
|
68
96
|
|
69
|
-
|
97
|
+
generate(moment: time)
|
70
98
|
end
|
71
99
|
|
72
100
|
# @param [Time, Integer] moment
|
@@ -81,10 +109,6 @@ class ULID
|
|
81
109
|
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
82
110
|
end
|
83
111
|
|
84
|
-
RANDOM_INTEGER_GENERATOR = -> {
|
85
|
-
SecureRandom.random_number(MAX_INTEGER)
|
86
|
-
}.freeze
|
87
|
-
|
88
112
|
# @param [Range<Time>, Range<nil>, Range[ULID], nil] period
|
89
113
|
# @overload sample(number, period: nil)
|
90
114
|
# @param [Integer] number
|
@@ -157,16 +181,21 @@ class ULID
|
|
157
181
|
raise(OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}") unless integer <= MAX_INTEGER
|
158
182
|
raise(ArgumentError, "integer should not be negative: given: #{integer}") if integer.negative?
|
159
183
|
|
160
|
-
|
161
|
-
|
162
|
-
|
184
|
+
base32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
|
185
|
+
base32encoded_timestamp = base32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
|
186
|
+
base32encoded_randomness = base32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
|
163
187
|
|
164
|
-
raise(UnexpectedError) unless
|
188
|
+
raise(UnexpectedError) unless base32encoded_timestamp && base32encoded_randomness
|
165
189
|
|
166
|
-
milliseconds =
|
167
|
-
entropy =
|
190
|
+
milliseconds = base32encoded_timestamp.to_i(32)
|
191
|
+
entropy = base32encoded_randomness.to_i(32)
|
168
192
|
|
169
|
-
new(
|
193
|
+
new(
|
194
|
+
milliseconds: milliseconds,
|
195
|
+
entropy: entropy,
|
196
|
+
integer: integer,
|
197
|
+
encoded: CrockfordBase32.from_base32(base32encoded).freeze
|
198
|
+
)
|
170
199
|
end
|
171
200
|
|
172
201
|
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
@@ -222,38 +251,6 @@ class ULID
|
|
222
251
|
time.floor(3)
|
223
252
|
end
|
224
253
|
|
225
|
-
# @api private
|
226
|
-
# @return [Integer]
|
227
|
-
def self.current_milliseconds
|
228
|
-
milliseconds_from_time(Time.now)
|
229
|
-
end
|
230
|
-
|
231
|
-
# @api private
|
232
|
-
# @param [Time] time
|
233
|
-
# @return [Integer]
|
234
|
-
private_class_method def self.milliseconds_from_time(time)
|
235
|
-
(time.to_r * 1000).to_i
|
236
|
-
end
|
237
|
-
|
238
|
-
# @api private
|
239
|
-
# @param [Time, Integer] moment
|
240
|
-
# @return [Integer]
|
241
|
-
def self.milliseconds_from_moment(moment)
|
242
|
-
case moment
|
243
|
-
when Integer
|
244
|
-
moment
|
245
|
-
when Time
|
246
|
-
milliseconds_from_time(moment)
|
247
|
-
else
|
248
|
-
raise(ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`')
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
# @return [Integer]
|
253
|
-
private_class_method def self.reasonable_entropy
|
254
|
-
SecureRandom.random_number(MAX_ENTROPY)
|
255
|
-
end
|
256
|
-
|
257
254
|
# @param [String, #to_str] string
|
258
255
|
# @return [ULID]
|
259
256
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
@@ -279,6 +276,26 @@ class ULID
|
|
279
276
|
parse(normalized_in_crockford)
|
280
277
|
end
|
281
278
|
|
279
|
+
# Almost same as `ULID.parse(string).to_time` except directly returning Time instance without needless object creation
|
280
|
+
#
|
281
|
+
# @param [String, #to_str] string
|
282
|
+
# @param [String, Integer, nil] in
|
283
|
+
# @return [Time]
|
284
|
+
# @raise [ParserError] if the given format is not correct for ULID specs
|
285
|
+
def self.decode_time(string, in: 'UTC')
|
286
|
+
in_for_time_at = binding.local_variable_get(:in) # Needed because `in` is a reserved word.
|
287
|
+
string = String.try_convert(string)
|
288
|
+
raise(ArgumentError, 'ULID.decode_time takes only strings') unless string
|
289
|
+
|
290
|
+
unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
|
291
|
+
raise(ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`")
|
292
|
+
end
|
293
|
+
|
294
|
+
timestamp = string.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError)
|
295
|
+
|
296
|
+
Time.at(0, CrockfordBase32.decode(timestamp), :millisecond, in: in_for_time_at)
|
297
|
+
end
|
298
|
+
|
282
299
|
# @param [String, #to_str] string
|
283
300
|
# @return [String]
|
284
301
|
# @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
|
@@ -303,7 +320,7 @@ class ULID
|
|
303
320
|
# @param [String, #to_str] string
|
304
321
|
# @return [Boolean]
|
305
322
|
def self.valid_as_variant_format?(string)
|
306
|
-
|
323
|
+
parse_variant_format(string)
|
307
324
|
rescue Exception
|
308
325
|
false
|
309
326
|
else
|
@@ -335,80 +352,20 @@ class ULID
|
|
335
352
|
if ULID === converted
|
336
353
|
converted
|
337
354
|
else
|
338
|
-
object_class_name = safe_get_class_name(object)
|
339
|
-
converted_class_name = safe_get_class_name(converted)
|
355
|
+
object_class_name = Utils.safe_get_class_name(object)
|
356
|
+
converted_class_name = Utils.safe_get_class_name(converted)
|
340
357
|
raise(TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})")
|
341
358
|
end
|
342
359
|
end
|
343
360
|
end
|
344
361
|
|
345
|
-
# @param [BasicObject] object
|
346
|
-
# @return [String]
|
347
|
-
private_class_method def self.safe_get_class_name(object)
|
348
|
-
fallback = 'UnknownObject'
|
349
|
-
|
350
|
-
# This class getter implementation used https://github.com/rspec/rspec-support/blob/4ad8392d0787a66f9c351d9cf6c7618e18b3d0f2/lib/rspec/support.rb#L83-L89 as a reference, thank you!
|
351
|
-
# ref: https://twitter.com/_kachick/status/1400064896759304196
|
352
|
-
klass = (
|
353
|
-
begin
|
354
|
-
object.class
|
355
|
-
rescue NoMethodError
|
356
|
-
# steep can't correctly handle singleton class assign. See https://github.com/soutaro/steep/pull/586 for further detail
|
357
|
-
# So this annotation is hack for the type infer.
|
358
|
-
# @type var object: BasicObject
|
359
|
-
# @type var singleton_class: untyped
|
360
|
-
singleton_class = class << object; self; end
|
361
|
-
(Class === singleton_class) ? singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) } : fallback
|
362
|
-
end
|
363
|
-
)
|
364
|
-
|
365
|
-
begin
|
366
|
-
name = String.try_convert(klass.name)
|
367
|
-
rescue Exception
|
368
|
-
fallback
|
369
|
-
else
|
370
|
-
name || fallback
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
# @api private
|
375
|
-
# @param [Integer] milliseconds
|
376
|
-
# @param [Integer] entropy
|
377
|
-
# @return [ULID]
|
378
|
-
# @raise [OverflowError] if the given value is larger than the ULID limit
|
379
|
-
# @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
|
380
|
-
def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
|
381
|
-
raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
|
382
|
-
raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
|
383
|
-
raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
|
384
|
-
raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
|
385
|
-
|
386
|
-
n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
387
|
-
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
388
|
-
integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)
|
389
|
-
|
390
|
-
new(milliseconds: milliseconds, entropy: entropy, integer: integer)
|
391
|
-
end
|
392
|
-
|
393
|
-
# @dynamic milliseconds, entropy
|
394
362
|
attr_reader(:milliseconds, :entropy)
|
395
363
|
|
396
|
-
# @api private
|
397
|
-
# @param [Integer] milliseconds
|
398
|
-
# @param [Integer] entropy
|
399
|
-
# @param [Integer] integer
|
400
|
-
# @return [void]
|
401
|
-
def initialize(milliseconds:, entropy:, integer:)
|
402
|
-
# All arguments check should be done with each constructors, not here
|
403
|
-
@integer = integer
|
404
|
-
@milliseconds = milliseconds
|
405
|
-
@entropy = entropy
|
406
|
-
end
|
407
|
-
|
408
364
|
# @return [String]
|
409
|
-
def
|
410
|
-
@
|
365
|
+
def encode
|
366
|
+
@encoded
|
411
367
|
end
|
368
|
+
alias_method(:to_s, :encode)
|
412
369
|
|
413
370
|
# @return [Integer]
|
414
371
|
def to_i
|
@@ -427,14 +384,13 @@ class ULID
|
|
427
384
|
|
428
385
|
# @return [String]
|
429
386
|
def inspect
|
430
|
-
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{
|
387
|
+
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{@encoded})".freeze
|
431
388
|
end
|
432
389
|
|
433
390
|
# @return [Boolean]
|
434
391
|
def eql?(other)
|
435
392
|
equal?(other) || (ULID === other && @integer == other.to_i)
|
436
393
|
end
|
437
|
-
# @dynamic ==
|
438
394
|
alias_method(:==, :eql?)
|
439
395
|
|
440
396
|
# Return `true` for same value of ULID, variant formats of strings, same Time in ULID precision(msec).
|
@@ -449,11 +405,9 @@ class ULID
|
|
449
405
|
@integer == other.to_i
|
450
406
|
when String
|
451
407
|
begin
|
452
|
-
|
408
|
+
to_i == ULID.parse_variant_format(other).to_i
|
453
409
|
rescue Exception
|
454
410
|
false
|
455
|
-
else
|
456
|
-
to_s == normalized
|
457
411
|
end
|
458
412
|
when Time
|
459
413
|
to_time == ULID.floor(other)
|
@@ -488,12 +442,12 @@ class ULID
|
|
488
442
|
|
489
443
|
# @return [String]
|
490
444
|
def timestamp
|
491
|
-
@timestamp ||= (
|
445
|
+
@timestamp ||= (@encoded.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
|
492
446
|
end
|
493
447
|
|
494
448
|
# @return [String]
|
495
449
|
def randomness
|
496
|
-
@randomness ||= (
|
450
|
+
@randomness ||= (@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
|
497
451
|
end
|
498
452
|
|
499
453
|
# @note Providing for rough operations. The keys and values is not fixed.
|
@@ -519,7 +473,6 @@ class ULID
|
|
519
473
|
ULID.from_integer(succ_int)
|
520
474
|
end
|
521
475
|
end
|
522
|
-
# @dynamic next
|
523
476
|
alias_method(:next, :succ)
|
524
477
|
|
525
478
|
# @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
|
@@ -543,18 +496,21 @@ class ULID
|
|
543
496
|
super
|
544
497
|
end
|
545
498
|
|
546
|
-
# @api private
|
547
499
|
# @return [Integer]
|
548
500
|
def marshal_dump
|
549
501
|
@integer
|
550
502
|
end
|
551
503
|
|
552
|
-
# @api private
|
553
504
|
# @param [Integer] integer
|
554
505
|
# @return [void]
|
555
506
|
def marshal_load(integer)
|
556
507
|
unmarshaled = ULID.from_integer(integer)
|
557
|
-
initialize(
|
508
|
+
initialize(
|
509
|
+
integer: unmarshaled.to_i,
|
510
|
+
milliseconds: unmarshaled.milliseconds,
|
511
|
+
entropy: unmarshaled.entropy,
|
512
|
+
encoded: unmarshaled.to_s
|
513
|
+
)
|
558
514
|
end
|
559
515
|
|
560
516
|
# @return [self]
|
@@ -576,20 +532,31 @@ class ULID
|
|
576
532
|
|
577
533
|
private
|
578
534
|
|
535
|
+
# @param [Integer] milliseconds
|
536
|
+
# @param [Integer] entropy
|
537
|
+
# @param [Integer] integer
|
538
|
+
# @param [String] encoded
|
539
|
+
# @return [void]
|
540
|
+
def initialize(milliseconds:, entropy:, integer:, encoded:)
|
541
|
+
# All arguments check should be done with each constructors, not here
|
542
|
+
@integer = integer
|
543
|
+
@encoded = encoded
|
544
|
+
@milliseconds = milliseconds
|
545
|
+
@entropy = entropy
|
546
|
+
end
|
547
|
+
|
579
548
|
# @return [void]
|
580
549
|
def cache_all_instance_variables
|
581
550
|
inspect
|
582
551
|
timestamp
|
583
552
|
randomness
|
584
553
|
end
|
585
|
-
end
|
586
554
|
|
587
|
-
|
588
|
-
|
589
|
-
require_relative('ulid/monotonic_generator')
|
590
|
-
require_relative('ulid/ractor_unshareable_constants')
|
555
|
+
MIN = parse('00000000000000000000000000').freeze
|
556
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
591
557
|
|
592
|
-
|
593
|
-
|
594
|
-
|
558
|
+
Utils.make_sharable_value(MIN)
|
559
|
+
Utils.make_sharable_value(MAX)
|
560
|
+
|
561
|
+
private_constant(:MIN, :MAX)
|
595
562
|
end
|
data/sig/ulid.rbs
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
class ULID < Object
|
2
2
|
VERSION: String
|
3
|
-
CROCKFORD_BASE32_ENCODING_STRING: String
|
4
3
|
TIMESTAMP_ENCODED_LENGTH: 10
|
5
4
|
RANDOMNESS_ENCODED_LENGTH: 16
|
6
5
|
ENCODED_LENGTH: 26
|
@@ -20,8 +19,8 @@ class ULID < Object
|
|
20
19
|
MAX: ULID
|
21
20
|
include Comparable
|
22
21
|
|
23
|
-
|
24
|
-
type moment = Time |
|
22
|
+
type milliseconds = Integer
|
23
|
+
type moment = Time | milliseconds
|
25
24
|
|
26
25
|
class Error < StandardError
|
27
26
|
end
|
@@ -35,25 +34,42 @@ class ULID < Object
|
|
35
34
|
class UnexpectedError < Error
|
36
35
|
end
|
37
36
|
|
38
|
-
module
|
39
|
-
|
40
|
-
|
37
|
+
# Private module
|
38
|
+
module Utils
|
39
|
+
def self.encode_base32: (milliseconds: milliseconds, entropy: Integer) -> String
|
40
|
+
|
41
|
+
def self.current_milliseconds: -> milliseconds
|
42
|
+
|
43
|
+
def self.milliseconds_from_moment: (moment moment) -> milliseconds
|
44
|
+
|
45
|
+
def self.reasonable_entropy: -> Integer
|
46
|
+
|
47
|
+
def self.milliseconds_from_time: (Time time) -> milliseconds
|
41
48
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
def self.safe_get_class_name: (untyped object) -> String
|
50
|
+
|
51
|
+
def self.make_sharable_value: (untyped object) -> void
|
52
|
+
|
53
|
+
def self.make_sharable_constantans: (Module) -> void
|
54
|
+
|
55
|
+
def self.ractor_can_make_shareable_time?: -> bool
|
56
|
+
end
|
57
|
+
|
58
|
+
# Private module
|
59
|
+
module CrockfordBase32
|
60
|
+
ENCODING_STRING: String
|
61
|
+
CROCKFORD_TR_PATTERN: String
|
62
|
+
BASE32_TR_PATTERN: String
|
63
|
+
VARIANT_TR_PATTERN: String
|
64
|
+
NORMALIZED_TR_PATTERN: String
|
48
65
|
|
49
|
-
# A private API. Should not be used in your code.
|
50
66
|
def self.encode: (Integer integer) -> String
|
51
67
|
|
52
|
-
# A private API. Should not be used in your code.
|
53
68
|
def self.decode: (String string) -> Integer
|
54
69
|
|
55
|
-
# A private API. Should not be used in your code.
|
56
70
|
def self.normalize: (String string) -> String
|
71
|
+
|
72
|
+
def self.from_base32: (String base32encoded) -> String
|
57
73
|
end
|
58
74
|
|
59
75
|
class MonotonicGenerator
|
@@ -71,13 +87,13 @@ class ULID < Object
|
|
71
87
|
# ```
|
72
88
|
attr_reader prev: ULID | nil
|
73
89
|
|
74
|
-
# A private API. Should not be used in your code.
|
75
|
-
def initialize: -> void
|
76
|
-
|
77
90
|
# See [How to keep `Sortable` even if in same timestamp](https://github.com/kachick/ruby-ulid#how-to-keep-sortable-even-if-in-same-timestamp)
|
78
91
|
# The `Thread-safety` is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255)
|
79
92
|
def generate: (?moment: moment) -> ULID
|
80
93
|
|
94
|
+
# Just providing similar api as `ULID.generate` and `ULID.encode` relation. No performance benefit exists in monotonic generator's one.
|
95
|
+
def encode: (?moment: moment) -> String
|
96
|
+
|
81
97
|
# Returned value is `basically not` Thread-safety
|
82
98
|
# If you want to keep Thread-safety, keep to call {#generate} only in same {#synchronize} block
|
83
99
|
#
|
@@ -91,6 +107,12 @@ class ULID < Object
|
|
91
107
|
def inspect: -> String
|
92
108
|
alias to_s inspect
|
93
109
|
def freeze: -> void
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def initialize: -> void
|
114
|
+
|
115
|
+
def prev=: (ULID?) -> void
|
94
116
|
end
|
95
117
|
|
96
118
|
interface _ToULID
|
@@ -102,8 +124,8 @@ class ULID < Object
|
|
102
124
|
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
|
103
125
|
type period = Range[Time] | Range[nil] | Range[ULID]
|
104
126
|
|
105
|
-
@string: String?
|
106
127
|
@integer: Integer
|
128
|
+
@encoded: String
|
107
129
|
@timestamp: String?
|
108
130
|
@randomness: String?
|
109
131
|
@inspect: String?
|
@@ -145,6 +167,13 @@ class ULID < Object
|
|
145
167
|
#
|
146
168
|
def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
|
147
169
|
|
170
|
+
# Retuns encoded and normalzied String.
|
171
|
+
# It has same arguments signatures as `.generate`. So can be used for just ID creation usecases without needless object creation.
|
172
|
+
#
|
173
|
+
# NOTE: Difference of ULID#encode, returned String is NOT frozen.
|
174
|
+
#
|
175
|
+
def self.encode: (?moment: moment, ?entropy: Integer) -> String
|
176
|
+
|
148
177
|
# Shorthand of `ULID.generate(moment: Time)`
|
149
178
|
# See also [ULID.generate](https://kachick.github.io/ruby-ulid/ULID.html#generate-class_method)
|
150
179
|
#
|
@@ -159,12 +188,6 @@ class ULID < Object
|
|
159
188
|
# ```
|
160
189
|
def self.at: (Time time) -> ULID
|
161
190
|
|
162
|
-
# A private API. Should not be used in your code.
|
163
|
-
def self.current_milliseconds: -> Integer
|
164
|
-
|
165
|
-
# A private API. Should not be used in your code.
|
166
|
-
def self.milliseconds_from_moment: (moment moment) -> Integer
|
167
|
-
|
168
191
|
# `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
|
169
192
|
#
|
170
193
|
# ```ruby
|
@@ -205,7 +228,7 @@ class ULID < Object
|
|
205
228
|
# ```
|
206
229
|
def self.floor: (Time time) -> Time
|
207
230
|
|
208
|
-
#
|
231
|
+
# Return ULID instance from encoded String.
|
209
232
|
#
|
210
233
|
# ```ruby
|
211
234
|
# ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
|
@@ -213,6 +236,17 @@ class ULID < Object
|
|
213
236
|
# ```
|
214
237
|
def self.parse: (_ToStr string) -> ULID
|
215
238
|
|
239
|
+
# Return Time instance from encoded String.
|
240
|
+
# See also `ULID.encode` for similar purpose.
|
241
|
+
#
|
242
|
+
# NOTE: Difference of ULID#to_time, returned Time is NOT frozen.
|
243
|
+
#
|
244
|
+
# ```ruby
|
245
|
+
# time = ULID.decode_time('01ARZ3NDEKTSV4RRFFQ69G5FAV')
|
246
|
+
# #=> 2016-07-30 23:54:10.259 UTC
|
247
|
+
# ```
|
248
|
+
def self.decode_time: (_ToStr string, ?in: String | Integer | nil) -> Time
|
249
|
+
|
216
250
|
# Get ULID instance from unnormalized String that encoded in Crockford's base32.
|
217
251
|
#
|
218
252
|
# http://www.crockford.com/base32.html
|
@@ -415,7 +449,7 @@ class ULID < Object
|
|
415
449
|
# ```
|
416
450
|
def self.scan: (_ToStr string) -> Enumerator[self, singleton(ULID)]
|
417
451
|
| (_ToStr string) { (ULID ulid) -> void } -> singleton(ULID)
|
418
|
-
|
452
|
+
|
419
453
|
def self.try_convert: (_ToULID) -> ULID
|
420
454
|
| (untyped) -> nil
|
421
455
|
|
@@ -430,9 +464,12 @@ class ULID < Object
|
|
430
464
|
# ```ruby
|
431
465
|
# ulid = ULID.generate
|
432
466
|
# #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
433
|
-
# ulid.
|
467
|
+
# ulid.encode #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
468
|
+
# ulid.encode.frozen? #=> true
|
469
|
+
# ulid.encode.equal?(ulid.to_s) #=> true
|
434
470
|
# ```
|
435
|
-
def
|
471
|
+
def encode: -> String
|
472
|
+
alias to_s encode
|
436
473
|
|
437
474
|
# ```ruby
|
438
475
|
# ulid = ULID.generate
|
@@ -563,10 +600,8 @@ class ULID < Object
|
|
563
600
|
def pred: -> ULID?
|
564
601
|
def freeze: -> self
|
565
602
|
|
566
|
-
# A private API. Should not be used in your code.
|
567
603
|
def marshal_dump: -> Integer
|
568
604
|
|
569
|
-
# A private API. Should not be used in your code.
|
570
605
|
def marshal_load: (Integer integer) -> void
|
571
606
|
|
572
607
|
# Returns `self`
|
@@ -581,21 +616,7 @@ class ULID < Object
|
|
581
616
|
|
582
617
|
private
|
583
618
|
|
584
|
-
|
585
|
-
def self.new: (milliseconds: Integer, entropy: Integer, integer: Integer) -> ULID
|
586
|
-
|
587
|
-
# A private API. Should not be used in your code.
|
588
|
-
def self.reasonable_entropy: -> Integer
|
589
|
-
|
590
|
-
# A private API. Should not be used in your code.
|
591
|
-
def self.milliseconds_from_time: (Time time) -> Integer
|
592
|
-
|
593
|
-
# A private API. Should not be used in your code.
|
594
|
-
def self.safe_get_class_name: (untyped object) -> String
|
595
|
-
|
596
|
-
# A private API. Should not be used in your code.
|
597
|
-
def initialize: (milliseconds: Integer, entropy: Integer, integer: Integer) -> void
|
619
|
+
def initialize: (milliseconds: milliseconds, entropy: Integer, integer: Integer, encoded: String) -> void
|
598
620
|
|
599
|
-
# A private API. Should not be used in your code.
|
600
621
|
def cache_all_instance_variables: -> void
|
601
622
|
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.
|
4
|
+
version: 0.7.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-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: " generator, optional monotonicity, parser and tools for ULID (RBS
|
14
14
|
included)\n"
|
@@ -23,8 +23,9 @@ files:
|
|
23
23
|
- lib/ruby-ulid.rb
|
24
24
|
- lib/ulid.rb
|
25
25
|
- lib/ulid/crockford_base32.rb
|
26
|
+
- lib/ulid/errors.rb
|
26
27
|
- lib/ulid/monotonic_generator.rb
|
27
|
-
- lib/ulid/
|
28
|
+
- lib/ulid/utils.rb
|
28
29
|
- lib/ulid/uuid.rb
|
29
30
|
- lib/ulid/version.rb
|
30
31
|
- sig/ulid.rbs
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# coding: us-ascii
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
class ULID
|
5
|
-
min = parse('00000000000000000000000000').freeze
|
6
|
-
max = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
7
|
-
|
8
|
-
ractor_can_make_shareable_time = RUBY_VERSION >= '3.1'
|
9
|
-
|
10
|
-
MIN = ractor_can_make_shareable_time ? Ractor.make_shareable(min) : min
|
11
|
-
MAX = ractor_can_make_shareable_time ? Ractor.make_shareable(max) : max
|
12
|
-
end
|