ruby-ulid 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/
|
3
|
+
[![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml?query=branch%3Amain)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/ruby-ulid.svg)](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
|
+
- ![rbs overview](./assets/ulid-rbs-overview.png?raw=true.png)
|
432
|
+
- ![rbs mix](./assets/ulid-rbs-mix.png?raw=true.png)
|
433
|
+
- ![rbs ng-to_str](./assets/ulid-rbs-ng-to_str.png?raw=true.png)
|
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
|