ruby-ulid 0.6.1 → 0.8.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 +74 -106
- data/lib/ulid/crockford_base32.rb +17 -16
- data/lib/ulid/errors.rb +1 -0
- data/lib/ulid/monotonic_generator.rb +23 -16
- data/lib/ulid/utils.rb +99 -0
- data/lib/ulid/uuid.rb +62 -30
- data/lib/ulid/version.rb +1 -1
- data/lib/ulid.rb +115 -194
- data/sig/ulid.rbs +162 -117
- metadata +8 -8
- 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: 323b750a4e11157bd492b31d74833df2042192775c8627917880ef386dee4c1c
|
4
|
+
data.tar.gz: 2dc8a91cbed7b473d6f9e28480dd227ab3469828ab0833f48a754111bd6e926c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e78a5289863c1300a3f4c186b72a0f1d97cd709ce6043b69729d851f5e6ffcedac001e3bb691397d761ee497d609b5292c300db7f2ec3f8cf5be0ee5c095af80
|
7
|
+
data.tar.gz: e19197709d7a896a79c6ebdc40fa919c2e7876284fa1dbf8895498ad7b80e989634cdd3e651362db5cf9f7c627fe17439ccc21e75c8e6598b002da6b19858aa0
|
data/README.md
CHANGED
@@ -1,13 +1,14 @@
|
|
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
|
-
|
10
|
-
This gem aims to provide the generator, optional monotonicity, parser and other
|
8
|
+
[ulid/spec](https://github.com/ulid/spec) defines some useful features.\
|
9
|
+
In particular, it has uniqueness, randomness, extractable timestamps, and sortability.\
|
10
|
+
This gem aims to provide the generator, optional monotonicity, parser, and other manipulations around ULID.\
|
11
|
+
[RBS](https://github.com/ruby/rbs) definitions are also included.
|
11
12
|
|
12
13
|
---
|
13
14
|
|
@@ -37,31 +38,22 @@ Instead, herein is proposed ULID:
|
|
37
38
|
|
38
39
|
### Install
|
39
40
|
|
40
|
-
|
41
|
+
Tested only in the last 2 Rubies. So you need Ruby 3.1 or higher.
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
```console
|
45
|
-
$ gem install ruby-ulid
|
46
|
-
Should be installed!
|
47
|
-
```
|
48
|
-
|
49
|
-
Add this line in your Gemfile.
|
43
|
+
Add this line to your `Gemfile`.
|
50
44
|
|
51
45
|
```ruby
|
52
|
-
gem('ruby-ulid', '~> 0.
|
46
|
+
gem('ruby-ulid', '~> 0.8.0')
|
53
47
|
```
|
54
48
|
|
55
|
-
|
49
|
+
And load it.
|
56
50
|
|
57
51
|
```ruby
|
58
52
|
require 'ulid'
|
59
|
-
|
60
|
-
ULID::VERSION
|
61
|
-
# => "0.6.1"
|
62
53
|
```
|
63
54
|
|
64
|
-
NOTE: This README
|
55
|
+
NOTE: This README contains information about the development version.\
|
56
|
+
If you would like to see released version's one. [Look at the ref](https://github.com/kachick/ruby-ulid/tree/v0.8.0).
|
65
57
|
|
66
58
|
### Generator and Parser
|
67
59
|
|
@@ -77,7 +69,7 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
|
|
77
69
|
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
78
70
|
```
|
79
71
|
|
80
|
-
It
|
72
|
+
It has inspector methods.
|
81
73
|
|
82
74
|
```ruby
|
83
75
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
@@ -99,9 +91,9 @@ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT
|
|
99
91
|
ULID.at(time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB002W5BGWWKN76N22H6)
|
100
92
|
```
|
101
93
|
|
102
|
-
Also `ULID.encode` and `ULID.decode_time` can be used to get primitive values for most usecases.
|
94
|
+
Also `ULID.encode` and `ULID.decode_time` can be used to get primitive values for most usecases.
|
103
95
|
|
104
|
-
`ULID.encode` returns [normalized](#variants-of-format) String without ULID object creation
|
96
|
+
`ULID.encode` returns [normalized](#variants-of-format) String without ULID object creation.\
|
105
97
|
It can take same arguments as `ULID.generate`.
|
106
98
|
|
107
99
|
```ruby
|
@@ -116,17 +108,17 @@ ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1') #=> 2000-01-01 00:00:00 UTC
|
|
116
108
|
ULID.decode_time('00VHNCZB00SYG7RCEXZC9DA4E1', in: '+09:00') #=> 2000-01-01 09:00:00 +0900
|
117
109
|
```
|
118
110
|
|
119
|
-
This project does not prioritize the speed. However it actually works faster than others! :zap:
|
111
|
+
This project does not prioritize on the speed. However it actually works faster than others! :zap:
|
120
112
|
|
121
|
-
Snapshot on 0.
|
113
|
+
Snapshot on v0.8.0 with Ruby 3.2.1 is below
|
122
114
|
|
123
|
-
|
124
|
-
|
125
|
-
|
115
|
+
- Generator is 1.9x faster than - [ulid gem - v1.4.0](https://github.com/rafaelsales/ulid)
|
116
|
+
- Generator is 2.0x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby)
|
117
|
+
- Parser is 3.1x faster than - [ulid-ruby gem - v1.0.2](https://github.com/abachman/ulid-ruby)
|
126
118
|
|
127
119
|
You can see further detail at [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
|
128
120
|
|
129
|
-
### Sortable
|
121
|
+
### Sortable by timestamp
|
130
122
|
|
131
123
|
ULIDs are sortable when they are generated in different timestamp with milliseconds precision.
|
132
124
|
|
@@ -139,7 +131,7 @@ ulids.uniq(&:to_time).size #=> 1000
|
|
139
131
|
ulids.sort == ulids #=> true
|
140
132
|
```
|
141
133
|
|
142
|
-
|
134
|
+
The basic generator prefers `randomness`, the results in the same milliseconds are not sortable.
|
143
135
|
|
144
136
|
```ruby
|
145
137
|
ulids = 10000.times.map do
|
@@ -151,8 +143,9 @@ ulids.sort == ulids #=> false
|
|
151
143
|
|
152
144
|
### How to keep `Sortable` even if in same timestamp
|
153
145
|
|
154
|
-
If you
|
155
|
-
|
146
|
+
If you prefer `sortability`, you can use `MonotonicGenerator` instead.\
|
147
|
+
It is referred to as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) in the spec.\
|
148
|
+
(Although it starts with a new random value when the timestamp is changed)
|
156
149
|
|
157
150
|
```ruby
|
158
151
|
monotonic_generator = ULID::MonotonicGenerator.new
|
@@ -163,20 +156,16 @@ sample_ulids_by_the_time = ulids.uniq(&:to_time)
|
|
163
156
|
sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
|
164
157
|
|
165
158
|
# In same milliseconds creation, it just increments the end of randomness part
|
166
|
-
ulids.take(
|
159
|
+
ulids.take(3) #=>
|
167
160
|
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
168
161
|
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
|
169
|
-
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6)
|
170
|
-
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
|
171
|
-
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
|
162
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6)]
|
172
163
|
|
173
164
|
# When the milliseconds is updated, it starts with new randomness
|
174
|
-
sample_ulids_by_the_time.take(
|
165
|
+
sample_ulids_by_the_time.take(3) #=>
|
175
166
|
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
176
167
|
# ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
|
177
|
-
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K)
|
178
|
-
# ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
|
179
|
-
# ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
|
168
|
+
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K)]
|
180
169
|
|
181
170
|
ulids.sort == ulids #=> true
|
182
171
|
```
|
@@ -195,7 +184,7 @@ ulids.grep(one_of_the_above)
|
|
195
184
|
ulids.grep_v(one_of_the_above)
|
196
185
|
```
|
197
186
|
|
198
|
-
When want to filter ULIDs with `Time`, we should consider to handle the precision
|
187
|
+
When want to filter ULIDs with `Time`, we should consider to handle the precision.\
|
199
188
|
So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
|
200
189
|
|
201
190
|
```ruby
|
@@ -265,18 +254,6 @@ ULID.scan(json).to_a
|
|
265
254
|
# ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
266
255
|
```
|
267
256
|
|
268
|
-
`ULID#patterns` is a util for text based operations.
|
269
|
-
The results and spec are not fixed. Should not be used except snippets/console operation.
|
270
|
-
|
271
|
-
```ruby
|
272
|
-
ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
|
273
|
-
#=> returns like a fallowing Hash
|
274
|
-
{
|
275
|
-
named_captures: /(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)/i,
|
276
|
-
strict_named_captures: /\A(?<timestamp>01F4GNBXW1)(?<randomness>AM2KWW52PVT3ZY9X)\z/i
|
277
|
-
}
|
278
|
-
```
|
279
|
-
|
280
257
|
#### Get boundary ULIDs
|
281
258
|
|
282
259
|
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
@@ -294,7 +271,7 @@ ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
|
294
271
|
|
295
272
|
#### As element in Enumerable
|
296
273
|
|
297
|
-
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
274
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID.\
|
298
275
|
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
299
276
|
|
300
277
|
NOTE: However basically `Range[ULID]#each` should not be used. Incrementing 128 bits IDs are not reasonable operation in most cases.
|
@@ -324,13 +301,11 @@ ULID.sample #=> ULID(2545-07-26 06:51:20.085 UTC: 0GGKQ45GMNMZR6N8A8GFG0ZXST)
|
|
324
301
|
ULID.sample #=> ULID(5098-07-26 21:31:06.946 UTC: 2SSBNGGYA272J7BMDCG4Z6EEM5)
|
325
302
|
ULID.sample(0) #=> []
|
326
303
|
ULID.sample(1) #=> [ULID(2241-04-16 03:31:18.440 UTC: 07S52YWZ98AZ8T565MD9VRYMQH)]
|
327
|
-
ULID.sample(
|
304
|
+
ULID.sample(3)
|
328
305
|
#=>
|
329
306
|
#[ULID(5701-04-29 12:41:19.647 UTC: 3B2YH2DV0ZYDDATGTYSKMM1CMT),
|
330
307
|
# ULID(2816-08-01 01:21:46.612 UTC: 0R9GT6RZKMK3RG02Q2HAFVKEY2),
|
331
|
-
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0)
|
332
|
-
# ULID(2741-09-02 16:24:18.803 UTC: 0P4Q4V34KKAJW46QW47WQB5463),
|
333
|
-
# ULID(2665-03-16 14:50:22.724 UTC: 0KYFW9DWM4CEGFNTAC6YFAVVJ6)]
|
308
|
+
# ULID(10408-10-05 17:06:27.848 UTC: 7J6CPTEEC86Y24EQ4F1Y93YYN0)]
|
334
309
|
```
|
335
310
|
|
336
311
|
You can specify a range object for the timestamp restriction, see also `ULID.range`.
|
@@ -338,37 +313,31 @@ You can specify a range object for the timestamp restriction, see also `ULID.ran
|
|
338
313
|
```ruby
|
339
314
|
ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
|
340
315
|
ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
|
341
|
-
ulids = ULID.sample(
|
342
|
-
ulids.uniq.size #=> 1000
|
343
|
-
ulids.take(5)
|
316
|
+
ulids = ULID.sample(3, period: ulid1..ulid2)
|
344
317
|
#=>
|
345
318
|
#[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
|
346
319
|
# ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
|
347
|
-
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW)
|
348
|
-
|
349
|
-
# ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3)]
|
350
|
-
ULID.sample(5, period: ulid1.to_time..ulid2.to_time)
|
320
|
+
# ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW)]
|
321
|
+
ULID.sample(3, period: ulid1.to_time..ulid2.to_time)
|
351
322
|
#=>
|
352
323
|
# [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
|
353
324
|
# ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
|
354
|
-
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW)
|
355
|
-
# ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
|
356
|
-
# ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G)]
|
325
|
+
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW)]
|
357
326
|
```
|
358
327
|
|
359
328
|
#### Variants of format
|
360
329
|
|
361
330
|
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.
|
362
331
|
|
363
|
-
>Case insensitive
|
332
|
+
> Case insensitive
|
364
333
|
|
365
|
-
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase
|
334
|
+
I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase.\
|
366
335
|
However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
|
367
336
|
|
368
|
-
>Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
337
|
+
> Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
369
338
|
|
370
|
-
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0
|
371
|
-
And accepts freestyle inserting `Hyphens (-)
|
339
|
+
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.\
|
340
|
+
And accepts freestyle inserting `Hyphens (-)`.\
|
372
341
|
To consider this patterns or not is different in each implementations.
|
373
342
|
|
374
343
|
I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57).
|
@@ -385,37 +354,40 @@ ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true
|
|
385
354
|
ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
|
386
355
|
```
|
387
356
|
|
388
|
-
####
|
389
|
-
|
390
|
-
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
391
|
-
The imported timestamp is meaningless. So ULID's benefit will lost.
|
392
|
-
|
393
|
-
```ruby
|
394
|
-
# Currently experimental feature, so needed to load the extension.
|
395
|
-
require 'ulid/uuid'
|
396
|
-
|
397
|
-
# Basically reversible
|
398
|
-
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
399
|
-
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
357
|
+
#### UUID
|
400
358
|
|
401
|
-
|
402
|
-
uuid_v4s.uniq.size == 10000 #=> Probably `true`
|
359
|
+
Both ULID and UUID are 128-bit IDs. But with different specs. Especially UUID has some versions probably UUIDv4.
|
403
360
|
|
404
|
-
|
405
|
-
|
361
|
+
All UUIDv4s can be converted to ULID, but this will not have the correct "timestamp".\
|
362
|
+
Most ULIDs cannot be converted to UUIDv4 while maintaining reversibility, because UUIDv4 requires version and variants in the fields.
|
406
363
|
|
407
|
-
|
364
|
+
See also [ulid/spec#64](https://github.com/ulid/spec/issues/64) for further detail.
|
408
365
|
|
409
|
-
|
410
|
-
ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
366
|
+
For now, this gem provides 4 methods for UUIDs.
|
411
367
|
|
412
|
-
|
413
|
-
|
414
|
-
reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
|
368
|
+
- Reversibility is preferred: `ULID.from_uuidish`, `ULID.to_uuidish`
|
369
|
+
- Prefer UUIDv4 specification: `ULID.from_uuidv4`, `ULID.to_uuidv4`
|
415
370
|
|
416
|
-
|
417
|
-
|
418
|
-
|
371
|
+
```ruby
|
372
|
+
# All UUIDv4 IDs can be reversible even if converted to ULID
|
373
|
+
uuid = SecureRandom.uuid
|
374
|
+
ULID.from_uuidish(uuid) == ULID.from_uuidv4(uuid) #=> true
|
375
|
+
ULID.from_uuidish(uuid).to_uuidish == ULID.from_uuidv4(uuid).to_uuidv4 #=> true
|
376
|
+
|
377
|
+
# But most ULIDs cannot be converted to UUIDv4
|
378
|
+
ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA')
|
379
|
+
ulid.to_uuidv4 #=> ULID::IrreversibleUUIDError
|
380
|
+
# So 2 ways to get substitute strings that might satisfy the use case
|
381
|
+
ulid.to_uuidv4(force: true) #=> "0179145f-07ca-4b3c-af33-4c3c3149254a" this cannot be reverse to source ULID
|
382
|
+
ulid == ULID.from_uuidv4(ulid.to_uuidv4(force: true)) #=> false
|
383
|
+
ulid.to_uuidish #=> "0179145f-07ca-bb3c-af33-4c3c3149254a" does not satisfy UUIDv4 spec
|
384
|
+
ulid == ULID.from_uuidish(ulid.to_uuidish) #=> true
|
385
|
+
|
386
|
+
# Seeing boundary IDs makes it easier to understand
|
387
|
+
ULID.min.to_uuidish #=> "00000000-0000-0000-0000-000000000000"
|
388
|
+
ULID.min.to_uuidv4(force: true) #=> "00000000-0000-4000-8000-000000000000"
|
389
|
+
ULID.max.to_uuidish #=> "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
390
|
+
ULID.max.to_uuidv4(force: true) #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
419
391
|
```
|
420
392
|
|
421
393
|
## Migration from other gems
|
@@ -424,13 +396,8 @@ See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-
|
|
424
396
|
|
425
397
|
## RBS
|
426
398
|
|
427
|
-
Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
|
428
|
-
|
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).
|
430
|
-
|
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)
|
399
|
+
- Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
|
400
|
+
- See the overview in [our wiki page for RBS](https://github.com/kachick/ruby-ulid/wiki/RBS)
|
434
401
|
|
435
402
|
## References
|
436
403
|
|
@@ -440,5 +407,6 @@ I have checked the behavior with [ruby/rbs@2.6.0](https://github.com/ruby/rbs) +
|
|
440
407
|
|
441
408
|
## Note
|
442
409
|
|
443
|
-
- [UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-
|
444
|
-
|
410
|
+
- [UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-02.html) is another choice for sortable and randomness ID.
|
411
|
+
\
|
412
|
+
However they remain in draft state. Our tracker is: [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
@@ -1,11 +1,12 @@
|
|
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
|
-
# @see https://www.crockford.com/base32.html
|
9
|
+
# @see https://www.crockford.com/base32.html and https://www.rfc-editor.org/rfc/rfc4648
|
9
10
|
#
|
10
11
|
# This module supporting only `subset of original crockford for actual use-case` in ULID context.
|
11
12
|
# Original decoding spec allows other characters.
|
@@ -37,7 +38,7 @@ class ULID
|
|
37
38
|
}.freeze
|
38
39
|
|
39
40
|
# Excluded I, L, O, U, - from Base32
|
40
|
-
|
41
|
+
base32hex_to_crockford = {
|
41
42
|
'I' => 'J',
|
42
43
|
'J' => 'K',
|
43
44
|
'K' => 'M',
|
@@ -53,8 +54,8 @@ class ULID
|
|
53
54
|
'U' => 'Y',
|
54
55
|
'V' => 'Z'
|
55
56
|
}.freeze
|
56
|
-
|
57
|
-
CROCKFORD_TR_PATTERN =
|
57
|
+
BASE32HEX_TR_PATTERN = base32hex_to_crockford.keys.join.freeze
|
58
|
+
CROCKFORD_TR_PATTERN = base32hex_to_crockford.values.join.freeze
|
58
59
|
ENCODING_STRING = "#{same_definitions.values.join}#{CROCKFORD_TR_PATTERN}".freeze
|
59
60
|
|
60
61
|
variant_to_normarized = {
|
@@ -68,36 +69,36 @@ class ULID
|
|
68
69
|
VARIANT_TR_PATTERN = variant_to_normarized.keys.join.freeze
|
69
70
|
NORMALIZED_TR_PATTERN = variant_to_normarized.values.join.freeze
|
70
71
|
|
72
|
+
Utils.make_sharable_constants(self)
|
73
|
+
|
71
74
|
# @note Avoid to depend regex as possible. `tr(string, string)` is almost 2x Faster than `gsub(regex, hash)` in Ruby 3.1
|
72
75
|
|
73
|
-
# @api private
|
74
76
|
# @param [String] string
|
75
77
|
# @return [Integer]
|
76
78
|
def self.decode(string)
|
77
|
-
|
78
|
-
|
79
|
+
base32hex = string.upcase.tr(CROCKFORD_TR_PATTERN, BASE32HEX_TR_PATTERN)
|
80
|
+
Integer(base32hex, 32, exception: true)
|
79
81
|
end
|
80
82
|
|
81
|
-
# @api private
|
82
83
|
# @param [Integer] integer
|
83
84
|
# @return [String]
|
84
85
|
def self.encode(integer)
|
85
|
-
|
86
|
-
|
86
|
+
base32hex = integer.to_s(32)
|
87
|
+
from_base32hex(base32hex).rjust(ENCODED_LENGTH, '0')
|
87
88
|
end
|
88
89
|
|
89
|
-
# @api private
|
90
90
|
# @param [String] string
|
91
91
|
# @return [String]
|
92
92
|
def self.normalize(string)
|
93
93
|
string.delete('-').tr(VARIANT_TR_PATTERN, NORMALIZED_TR_PATTERN)
|
94
94
|
end
|
95
95
|
|
96
|
-
# @
|
97
|
-
# @param [String] n32encoded
|
96
|
+
# @param [String] base32hex
|
98
97
|
# @return [String]
|
99
|
-
def self.
|
100
|
-
|
98
|
+
def self.from_base32hex(base32hex)
|
99
|
+
base32hex.upcase.tr(BASE32HEX_TR_PATTERN, CROCKFORD_TR_PATTERN)
|
101
100
|
end
|
102
101
|
end
|
102
|
+
|
103
|
+
private_constant(:CrockfordBase32)
|
103
104
|
end
|
data/lib/ulid/errors.rb
CHANGED
@@ -4,6 +4,9 @@
|
|
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.
|
@@ -11,18 +14,19 @@ class ULID
|
|
11
14
|
include(MonitorMixin)
|
12
15
|
|
13
16
|
# @return [ULID, nil]
|
14
|
-
|
17
|
+
attr_accessor(:last)
|
18
|
+
private(:last=)
|
15
19
|
|
16
20
|
undef_method(:instance_variable_set)
|
17
21
|
|
18
22
|
def initialize
|
19
23
|
super
|
20
|
-
@
|
24
|
+
@last = nil
|
21
25
|
end
|
22
26
|
|
23
27
|
# @return [String]
|
24
28
|
def inspect
|
25
|
-
"ULID::MonotonicGenerator(
|
29
|
+
"ULID::MonotonicGenerator(last: #{@last.inspect})"
|
26
30
|
end
|
27
31
|
alias_method(:to_s, :inspect)
|
28
32
|
|
@@ -31,27 +35,27 @@ class ULID
|
|
31
35
|
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
32
36
|
# @raise [UnexpectedError] if the generated ULID is an invalid value in monotonicity spec.
|
33
37
|
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
34
|
-
def generate(moment:
|
38
|
+
def generate(moment: Utils.current_milliseconds)
|
35
39
|
synchronize do
|
36
|
-
|
37
|
-
unless
|
38
|
-
ret = ULID.generate(moment:
|
39
|
-
@
|
40
|
+
prev = @last
|
41
|
+
unless prev
|
42
|
+
ret = ULID.generate(moment:)
|
43
|
+
@last = ret
|
40
44
|
return ret
|
41
45
|
end
|
42
46
|
|
43
|
-
milliseconds =
|
47
|
+
milliseconds = Utils.milliseconds_from_moment(moment)
|
44
48
|
|
45
49
|
ulid = (
|
46
|
-
if
|
50
|
+
if prev.milliseconds < milliseconds
|
47
51
|
ULID.generate(moment: milliseconds)
|
48
52
|
else
|
49
|
-
ULID.
|
53
|
+
ULID.generate(moment: prev.milliseconds, entropy: prev.entropy.succ)
|
50
54
|
end
|
51
55
|
)
|
52
56
|
|
53
|
-
unless ulid >
|
54
|
-
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{
|
57
|
+
unless ulid > prev
|
58
|
+
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{prev.inspect}"
|
55
59
|
additional_information = (
|
56
60
|
if Thread.list == [Thread.main]
|
57
61
|
'# NOTE: looks single thread only exist'
|
@@ -63,15 +67,18 @@ class ULID
|
|
63
67
|
raise(UnexpectedError, base_message + additional_information)
|
64
68
|
end
|
65
69
|
|
66
|
-
@
|
70
|
+
@last = ulid
|
67
71
|
ulid
|
68
72
|
end
|
69
73
|
end
|
70
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
|
71
78
|
# @param [Time, Integer] moment
|
72
79
|
# @return [String]
|
73
|
-
def encode(moment:
|
74
|
-
generate(moment:
|
80
|
+
def encode(moment: Utils.current_milliseconds)
|
81
|
+
generate(moment:).encode
|
75
82
|
end
|
76
83
|
|
77
84
|
undef_method(:freeze)
|
data/lib/ulid/utils.rb
ADDED
@@ -0,0 +1,99 @@
|
|
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_base32hex(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
|
+
base32hex_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
|
56
|
+
base32hex_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
57
|
+
"#{base32hex_timestamp}#{base32hex_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
|
+
# @note Call before Module#private_constant
|
90
|
+
def self.make_sharable_constants(mod)
|
91
|
+
mod.constants.each do |const_name|
|
92
|
+
value = mod.const_get(const_name)
|
93
|
+
Ractor.make_shareable(value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private_constant(:Utils)
|
99
|
+
end
|