ruby-ulid 0.2.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cea320f797b834d5a0f246db9920a9453a6d229728b428590e2f99dd275c8fe9
4
- data.tar.gz: e703ca1896a7ee4155b1362cffdfaf9499e4ae32073cd440c50750d4e9cc9c1a
3
+ metadata.gz: 4d7356366d0184815a725d1947f4f685b3abba0320ae60800b5a015dc7c649e9
4
+ data.tar.gz: 04b55b6d92dbf02865a7803be352f5cd385f3417c061269f2ad197474202154d
5
5
  SHA512:
6
- metadata.gz: b9d0436d98095dd0084675d87a6605848f6c7ee14d65724ce5bd9229945ffc17a3b7e1efaf9a295baeaeac11782c9fc63bc901149cfa58d96046e334d5d19067
7
- data.tar.gz: f7e016a4ca3d859f8469b5281d9f5581ffe6c0fdde737c554c627f7862461701844e46e4513ce09748e1cdf3fa4b8da2607a8d3267f74c64ae4a257ae54d6e78
6
+ metadata.gz: aa826d204cbdc1e6850fbeab278dc72e5b683926db9af7ed477ec2e74837d32829dbfb468c1156d6dcf5be604f9fdeb8fd0a282999b47a627884152b2a2cd5d4
7
+ data.tar.gz: 0b1208e76af50f9952c8c4835d1410c7e1fd114313cc71d39a143c9bc04ac4f1a35ba49be88a49406606c441e4f4fa7cad5fc938d74fcecde0d612319a6890b3
data/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # ruby-ulid
2
2
 
3
+ [![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/badge.svg?branch=main)](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/?branch=main)
4
+ [![Gem Version](https://badge.fury.io/rb/ruby-ulid.svg)](http://badge.fury.io/rb/ruby-ulid)
5
+
3
6
  ## Overview
4
7
 
5
- The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
6
- This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
7
- Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
8
+ [ulid/spec](https://github.com/ulid/spec) is useful.
9
+ Especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
10
+ This gem aims to provide the generator, optional monotonicity, parser and other manipulation features around ULID with included [RBS](https://github.com/ruby/rbs).
8
11
 
9
12
  ---
10
13
 
11
- ![ULIDlogo](https://raw.githubusercontent.com/kachick/ruby-ulid/main/logo.png)
12
-
13
- [![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/badge.svg?branch=main)](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/?branch=main)
14
- [![Gem Version](https://badge.fury.io/rb/ruby-ulid.svg)](http://badge.fury.io/rb/ruby-ulid)
15
- [![Visual Studio Code](https://img.shields.io/badge/Visual%20Studio%20Code-0078d7.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white)](https://github.dev/kachick/ruby-ulid)
14
+ ![ULIDlogo](./assets/logo.png)
16
15
 
17
16
  ## Universally Unique Lexicographically Sortable Identifier
18
17
 
@@ -47,21 +46,43 @@ $ gem install ruby-ulid
47
46
  Should be installed!
48
47
  ```
49
48
 
50
- Add this line to your application/library's `Gemfile` is needed in basic use-case
49
+ Add this line in your Gemfile.
51
50
 
52
51
  ```ruby
53
- gem 'ruby-ulid', '~> 0.2.2'
52
+ gem('ruby-ulid', '~> 0.5.0')
54
53
  ```
55
54
 
56
- ### Generator and Parser
57
-
58
- The generated `ULID` is an object not just a string.
59
- It means easily get the timestamps and binary formats.
55
+ ### How to use
60
56
 
61
57
  ```ruby
62
58
  require 'ulid'
63
59
 
60
+ ULID::VERSION
61
+ # => "0.5.0"
62
+ ```
63
+
64
+ ### Basic Generator
65
+
66
+ The generated `ULID` is an object not just a string.
67
+
68
+ ```ruby
64
69
  ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
70
+ ```
71
+
72
+ ### Parser
73
+
74
+ Get the objects from exists encoded ULIDs.
75
+
76
+ ```ruby
77
+ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
78
+ ```
79
+
80
+ ### ULID object
81
+
82
+ Extract timestamps and binary formats.
83
+
84
+ ```ruby
85
+ ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
65
86
  ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
66
87
  ulid.milliseconds #=> 1619544442826
67
88
  ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
@@ -71,16 +92,9 @@ ulid.to_i #=> 1957909092946624190749577070267409738
71
92
  ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
72
93
  ```
73
94
 
74
- You can get the objects from exists encoded ULIDs
75
-
76
- ```ruby
77
- ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
78
- ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
79
- ```
80
-
81
95
  ### Sortable with the timestamp
82
96
 
83
- ULIDs are sortable when they are generated in different timestamp with milliseconds precision
97
+ ULIDs are sortable when they are generated in different timestamp with milliseconds precision.
84
98
 
85
99
  ```ruby
86
100
  ulids = 1000.times.map do
@@ -91,7 +105,7 @@ ulids.uniq(&:to_time).size #=> 1000
91
105
  ulids.sort == ulids #=> true
92
106
  ```
93
107
 
94
- `ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`
108
+ `ULID.generate` can take fixed `Time` instance. The shorthand is `ULID.at`.
95
109
 
96
110
  ```ruby
97
111
  time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
@@ -105,7 +119,7 @@ end
105
119
  ulids.sort == ulids #=> true
106
120
  ```
107
121
 
108
- The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
122
+ Basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
109
123
 
110
124
  ```ruby
111
125
  ulids = 10000.times.map do
@@ -147,11 +161,11 @@ sample_ulids_by_the_time.take(5) #=>
147
161
  ulids.sort == ulids #=> true
148
162
  ```
149
163
 
150
- Same generator does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255)
164
+ Same generator does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255).
151
165
 
152
166
  ### Filtering IDs with `Time`
153
167
 
154
- `ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
168
+ `ULID` can be element of the `Range`. If they were generated with monotonic generator, ID based filtering is easy and reliable.
155
169
 
156
170
  ```ruby
157
171
  include_end = ulid1..ulid2
@@ -187,7 +201,9 @@ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.12
187
201
  ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
188
202
  ```
189
203
 
190
- ### Scanner for string (e.g. `JSON`)
204
+ ### Tools
205
+
206
+ #### Scanner for string (e.g. `JSON`)
191
207
 
192
208
  For rough operations, `ULID.scan` might be useful.
193
209
 
@@ -230,7 +246,7 @@ ULID.scan(json).to_a
230
246
  ```
231
247
 
232
248
  `ULID#patterns` is a util for text based operations.
233
- The results and spec are not fixed. Should not be used except snippets/console operation
249
+ The results and spec are not fixed. Should not be used except snippets/console operation.
234
250
 
235
251
  ```ruby
236
252
  ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
@@ -241,7 +257,7 @@ ULID.parse('01F4GNBXW1AM2KWW52PVT3ZY9X').patterns
241
257
  }
242
258
  ```
243
259
 
244
- ### Some methods to help manipulations
260
+ #### Get boundary ULIDs
245
261
 
246
262
  `ULID.min` and `ULID.max` return termination values for ULID spec.
247
263
 
@@ -256,10 +272,12 @@ ULID.min(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
256
272
  ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
257
273
  ```
258
274
 
275
+ #### As element in Enumerable
276
+
259
277
  `ULID#next` and `ULID#succ` returns next(successor) ULID.
260
278
  Especially `ULID#succ` makes it possible `Range[ULID]#each`.
261
279
 
262
- NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
280
+ NOTE: However basically `Range[ULID]#each` should not be used. Iincrementing 128 bits IDs are not reasonable operation in most cases.
263
281
 
264
282
  ```ruby
265
283
  ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
@@ -267,7 +285,7 @@ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM00000000000000
267
285
  ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
268
286
  ```
269
287
 
270
- `ULID#pred` returns predecessor ULID
288
+ `ULID#pred` returns predecessor ULID.
271
289
 
272
290
  ```ruby
273
291
  ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
@@ -275,6 +293,8 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
275
293
  ULID.parse('00000000000000000000000000').pred #=> nil
276
294
  ```
277
295
 
296
+ #### Test helpers
297
+
278
298
  `ULID.sample` returns random ULIDs.
279
299
 
280
300
  Basically ignores generating time.
@@ -300,61 +320,52 @@ ulid1 = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') #=> ULID(2021-04-27 17:27:22.82
300
320
  ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
301
321
  ulids = ULID.sample(1000, period: ulid1..ulid2)
302
322
  ulids.uniq.size #=> 1000
303
- ulids.take(10)
323
+ ulids.take(5)
304
324
  #=>
305
325
  #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
306
326
  # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
307
327
  # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
308
328
  # ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
309
- # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
310
- # ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
311
- # ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
312
- # ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
313
- # ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
314
- # ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
315
- ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
329
+ # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3)]
330
+ ULID.sample(5, period: ulid1.to_time..ulid2.to_time)
316
331
  #=>
317
332
  # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
318
333
  # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
319
334
  # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
320
335
  # ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
321
- # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
322
- # ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
323
- # ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
324
- # ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
325
- # ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
326
- # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
336
+ # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G)]
327
337
  ```
328
338
 
329
- ### ULID specification ambiguity around orthographical variants of the format
339
+ #### Variants of format
330
340
 
331
341
  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.
332
342
 
343
+ >Case insensitive
344
+
345
+ I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase.
346
+ However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
347
+
333
348
  >Uses Crockford's base32 for better efficiency and readability (5 bits per character)
334
349
 
335
350
  The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.
336
351
  And accepts freestyle inserting `Hyphens (-)`.
337
352
  To consider this patterns or not is different in each implementations.
338
353
 
339
- Current parser/validator/matcher aims to cover `subset of Crockford's base32`.
340
- I have suggested it would be clarified in [ulid/spec#57](https://github.com/ulid/spec/pull/57).
341
-
342
- >Case insensitive
354
+ I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57).
343
355
 
344
- I can understand it might be considered in actual use-case.
345
- But it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
356
+ This gem provides some methods to handle the nasty possibilities.
346
357
 
347
- Be that as it may, this gem provides API for handling the nasty possibilities.
348
-
349
- `ULID.normalize` and `ULID.normalized?`
358
+ `ULID.normalize`, `ULID.normalized?`, `ULID.valid_as_variant_format?` and `ULID.parse_variant_format`
350
359
 
351
360
  ```ruby
352
- ULID.normalize('-olarz3-noekisv4rrff-q6ig5fav--') #=> "01ARZ3N0EK1SV4RRFFQ61G5FAV"
353
- ULID.normalized?('-olarz3-noekisv4rrff-q6ig5fav--') #=> false
354
- ULID.normalized?('01ARZ3N0EK1SV4RRFFQ61G5FAV') #=> true
361
+ ULID.normalize('01g70y0y7g-z1xwdarexergsddd') #=> "01G70Y0Y7GZ1XWDAREXERGSDDD"
362
+ ULID.normalized?('01g70y0y7g-z1xwdarexergsddd') #=> false
363
+ ULID.normalized?('01G70Y0Y7GZ1XWDAREXERGSDDD') #=> true
364
+ ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true
365
+ ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
355
366
  ```
356
367
 
357
- ### UUIDv4 converter for migration use-cases
368
+ #### UUIDv4 converter (experimental)
358
369
 
359
370
  `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
360
371
  The imported timestamp is meaningless. So ULID's benefit will lost.
@@ -387,56 +398,19 @@ ULID.min == reversed_min #=> false
387
398
  ULID.max == reversed_max #=> false
388
399
  ```
389
400
 
390
- ## How to migrate from other gems
391
-
392
- As far as I know, major prior arts are below
393
-
394
- ### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
395
-
396
- It is just providing basic `String` generator only.
397
- So you can replace the code as below
398
-
399
- ```diff
400
- -ULID.generate
401
- +ULID.generate.to_s
402
- ```
403
-
404
- NOTE: In version before `1.3.0`, timestamps might not be correct value.
405
-
406
- 1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
407
- 1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
408
- 1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
409
-
410
- ### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
411
-
412
- It is providing basic generator(except monotonic generator) and parser.
413
- Major methods can be replaced as below.
414
-
415
- ```diff
416
- -ULID.generate
417
- +ULID.generate.to_s
418
- -ULID.at(time)
419
- +ULID.at(time).to_s
420
- -ULID.time(string)
421
- +ULID.parse(string).to_time
422
- -ULID.min_ulid_at(time)
423
- +ULID.min(time).to_s
424
- -ULID.max_ulid_at(time)
425
- +ULID.max(time).to_s
426
- ```
401
+ ## Migration from other gems
427
402
 
428
- NOTE: In version before `1.0.2`, timestamps might not be correct value.
403
+ See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-migration).
429
404
 
430
- 1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
431
- 1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
432
- 1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
433
- 1. [Released in 1.0.2](https://github.com/abachman/ulid-ruby/compare/v1.0.0...v1.0.2)
405
+ ## RBS
434
406
 
435
- ### Compare performance with them
407
+ Try at [examples/rbs_sandbox](https://github.com/kachick/ruby-ulid/tree/main/examples/rbs_sandbox).
436
408
 
437
- See [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
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) + [soutaro/steep-vscode](https://github.com/soutaro/steep-vscode).
438
410
 
439
- The results are not something to be proud of.
411
+ * ![rbs overview](./assets/ulid-rbs-overview.png?raw=true.png)
412
+ * ![rbs mix](./assets/ulid-rbs-mix.png?raw=true.png)
413
+ * ![rbs ng-to_str](./assets/ulid-rbs-ng-to_str.png?raw=true.png)
440
414
 
441
415
  ## References
442
416
 
@@ -446,4 +420,5 @@ The results are not something to be proud of.
446
420
 
447
421
  ## Note
448
422
 
449
- - Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
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.
424
+ However they are stayed in draft state. ref: [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
data/lib/ruby-ulid.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # coding: us-ascii
2
2
  # frozen_string_literal: true
3
+ # shareable_constant_value: literal
3
4
 
4
5
  # Copyright (C) 2021 Kenichi Kamiya
5
6
 
6
- require_relative 'ulid'
7
+ require_relative('ulid')
@@ -1,5 +1,6 @@
1
1
  # coding: us-ascii
2
2
  # frozen_string_literal: true
3
+ # shareable_constant_value: literal
3
4
 
4
5
  # Copyright (C) 2021 Kenichi Kamiya
5
6
 
@@ -14,10 +15,10 @@ class ULID
14
15
  # * https://github.com/kachick/ruby-ulid/issues/57
15
16
  # * https://github.com/kachick/ruby-ulid/issues/78
16
17
  module CrockfordBase32
17
- class SetupError < ScriptError; end
18
+ class SetupError < UnexpectedError; end
18
19
 
19
20
  n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
20
- raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
21
+ raise(SetupError, 'obvious bug exists in the mapping algorithm') unless n32_chars.size == 32
21
22
 
22
23
  n32_char_by_number = {}
23
24
  n32_chars.each_with_index do |char, index|
@@ -48,7 +49,7 @@ class ULID
48
49
  map[encoding_char] = char_32
49
50
  end
50
51
  end.freeze
51
- raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
52
+ raise(SetupError, 'obvious bug exists in the mapping algorithm') unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
52
53
 
53
54
  CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
54
55
 
@@ -1,19 +1,23 @@
1
1
  # coding: us-ascii
2
2
  # frozen_string_literal: true
3
+ # shareable_constant_value: literal
3
4
 
4
5
  # Copyright (C) 2021 Kenichi Kamiya
5
6
 
6
7
  class ULID
7
8
  class MonotonicGenerator
8
- include MonitorMixin
9
+ # @note When use https://github.com/ko1/ractor-tvar might realize Ractor based thread safe monotonic generator.
10
+ # However it is a C extention, I'm pending to use it for now.
11
+ include(MonitorMixin)
9
12
 
13
+ # @dynamic prev
10
14
  # @return [ULID, nil]
11
- attr_reader :prev
15
+ attr_reader(:prev)
12
16
 
13
- undef_method :instance_variable_set
17
+ undef_method(:instance_variable_set)
14
18
 
15
19
  def initialize
16
- super()
20
+ super
17
21
  @prev = nil
18
22
  end
19
23
 
@@ -21,7 +25,8 @@ class ULID
21
25
  def inspect
22
26
  "ULID::MonotonicGenerator(prev: #{@prev.inspect})"
23
27
  end
24
- alias_method :to_s, :inspect
28
+ # @dynamic to_s
29
+ alias_method(:to_s, :inspect)
25
30
 
26
31
  # @param [Time, Integer] moment
27
32
  # @return [ULID]
@@ -30,23 +35,25 @@ class ULID
30
35
  # Basically will not happen. Just means this feature prefers error rather than invalid value.
31
36
  def generate(moment: ULID.current_milliseconds)
32
37
  synchronize do
33
- unless @prev
34
- @prev = ULID.generate(moment: moment)
35
- return @prev
38
+ prev_ulid = @prev
39
+ unless prev_ulid
40
+ ret = ULID.generate(moment: moment)
41
+ @prev = ret
42
+ return ret
36
43
  end
37
44
 
38
45
  milliseconds = ULID.milliseconds_from_moment(moment)
39
46
 
40
47
  ulid = (
41
- if @prev.milliseconds < milliseconds
48
+ if prev_ulid.milliseconds < milliseconds
42
49
  ULID.generate(moment: milliseconds)
43
50
  else
44
- ULID.from_milliseconds_and_entropy(milliseconds: @prev.milliseconds, entropy: @prev.entropy.succ)
51
+ ULID.from_milliseconds_and_entropy(milliseconds: prev_ulid.milliseconds, entropy: prev_ulid.entropy.succ)
45
52
  end
46
53
  )
47
54
 
48
- unless ulid > @prev
49
- base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{@prev.inspect}"
55
+ unless ulid > prev_ulid
56
+ base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{prev_ulid.inspect}"
50
57
  additional_information = (
51
58
  if Thread.list == [Thread.main]
52
59
  '# NOTE: looks single thread only exist'
@@ -55,7 +62,7 @@ class ULID
55
62
  end
56
63
  )
57
64
 
58
- raise UnexpectedError, base_message + additional_information
65
+ raise(UnexpectedError, base_message + additional_information)
59
66
  end
60
67
 
61
68
  @prev = ulid
@@ -63,12 +70,12 @@ class ULID
63
70
  end
64
71
  end
65
72
 
66
- undef_method :freeze
73
+ undef_method(:freeze)
67
74
 
68
75
  # @raise [TypeError] always raises exception and does not freeze self
69
76
  # @return [void]
70
77
  def freeze
71
- raise TypeError, "cannot freeze #{self.class}"
78
+ raise(TypeError, "cannot freeze #{self.class}")
72
79
  end
73
80
  end
74
81
  end
@@ -0,0 +1,12 @@
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
data/lib/ulid/uuid.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # coding: us-ascii
2
2
  # frozen_string_literal: true
3
+ # shareable_constant_value: literal
3
4
 
4
5
  # Copyright (C) 2021 Kenichi Kamiya
5
6
 
@@ -10,18 +11,18 @@
10
11
  class ULID
11
12
  # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
12
13
  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
13
- private_constant :UUIDV4_PATTERN
14
+ private_constant(:UUIDV4_PATTERN)
14
15
 
15
16
  # @param [String, #to_str] uuid
16
17
  # @return [ULID]
17
18
  # @raise [ParserError] if the given format is not correct for UUIDv4 specs
18
19
  def self.from_uuidv4(uuid)
19
20
  uuid = String.try_convert(uuid)
20
- raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid
21
+ raise(ArgumentError, 'ULID.from_uuidv4 takes only strings') unless uuid
21
22
 
22
23
  prefix_trimmed = uuid.delete_prefix('urn:uuid:')
23
24
  unless UUIDV4_PATTERN.match?(prefix_trimmed)
24
- raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
25
+ raise(ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`")
25
26
  end
26
27
 
27
28
  normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
@@ -32,8 +33,11 @@ class ULID
32
33
  def to_uuidv4
33
34
  # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
34
35
  array = octets.pack('C*').unpack('NnnnnN')
35
- array[2] = (array[2] & 0x0fff) | 0x4000
36
- array[3] = (array[3] & 0x3fff) | 0x8000
36
+ ref2, ref3 = array[2], array[3]
37
+ raise unless Integer === ref2 && Integer === ref3
38
+
39
+ array[2] = (ref2 & 0x0fff) | 0x4000
40
+ array[3] = (ref3 & 0x3fff) | 0x8000
37
41
  ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
38
42
  end
39
43
  end
data/lib/ulid/version.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # coding: us-ascii
2
2
  # frozen_string_literal: true
3
+ # shareable_constant_value: literal
3
4
 
4
5
  class ULID
5
- VERSION = '0.2.2'
6
+ VERSION = '0.5.0'
6
7
  end
data/lib/ulid.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # coding: us-ascii
2
2
  # frozen_string_literal: true
3
+ # shareable_constant_value: experimental_everything
3
4
 
4
5
  # Copyright (C) 2021 Kenichi Kamiya
5
6
 
6
- require 'securerandom'
7
+ require('securerandom')
7
8
 
8
9
  # @see https://github.com/ulid/spec
9
10
  # @!attribute [r] milliseconds
@@ -11,7 +12,7 @@ require 'securerandom'
11
12
  # @!attribute [r] entropy
12
13
  # @return [Integer]
13
14
  class ULID
14
- include Comparable
15
+ include(Comparable)
15
16
 
16
17
  class Error < StandardError; end
17
18
  class OverflowError < Error; end
@@ -25,10 +26,12 @@ class ULID
25
26
 
26
27
  TIMESTAMP_ENCODED_LENGTH = 10
27
28
  RANDOMNESS_ENCODED_LENGTH = 16
28
- ENCODED_LENGTH = TIMESTAMP_ENCODED_LENGTH + RANDOMNESS_ENCODED_LENGTH
29
+ ENCODED_LENGTH = 26
30
+
29
31
  TIMESTAMP_OCTETS_LENGTH = 6
30
32
  RANDOMNESS_OCTETS_LENGTH = 10
31
- OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
33
+ OCTETS_LENGTH = 16
34
+
32
35
  MAX_MILLISECONDS = 281474976710655
33
36
  MAX_ENTROPY = 1208925819614629174706175
34
37
  MAX_INTEGER = 340282366920938463463374607431768211455
@@ -40,14 +43,15 @@ class ULID
40
43
  STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET = /\A#{PATTERN_WITH_CROCKFORD_BASE32_SUBSET.source}\z/i.freeze
41
44
 
42
45
  # Optimized for `ULID.scan`, might be changed the definition with gathered `ULID.scan` spec changed.
43
- # This can't contain `\b` for considering UTF-8 (e.g. Japanese), so intentional `false negative` definition.
44
- SCANNING_PATTERN = /[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}/i.freeze
46
+ SCANNING_PATTERN = /\b[0-7][#{CROCKFORD_BASE32_ENCODING_STRING}]{#{TIMESTAMP_ENCODED_LENGTH - 1}}[#{CROCKFORD_BASE32_ENCODING_STRING}]{#{RANDOMNESS_ENCODED_LENGTH}}\b/i.freeze
45
47
 
46
- # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
48
+ # Similar as Time#inspect since Ruby 2.7, however it is NOT same.
49
+ # Time#inspect trancates needless digits. Keeping full milliseconds with "%3N" will fit for ULID.
47
50
  # @see https://bugs.ruby-lang.org/issues/15958
51
+ # @see https://github.com/ruby/ruby/blob/744d17ff6c33b09334508e8110007ea2a82252f5/time.c#L4026-L4078
48
52
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
49
53
 
50
- private_class_method :new
54
+ private_class_method(:new)
51
55
 
52
56
  # @param [Integer, Time] moment
53
57
  # @param [Integer] entropy
@@ -60,7 +64,7 @@ class ULID
60
64
  # @param [Time] time
61
65
  # @return [ULID]
62
66
  def self.at(time)
63
- raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
67
+ raise(ArgumentError, 'ULID.at takes only `Time` instance') unless Time === time
64
68
 
65
69
  from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
66
70
  end
@@ -79,7 +83,7 @@ class ULID
79
83
 
80
84
  RANDOM_INTEGER_GENERATOR = -> {
81
85
  SecureRandom.random_number(MAX_INTEGER)
82
- }
86
+ }.freeze
83
87
 
84
88
  # @param [Range<Time>, Range<nil>, Range[ULID], nil] period
85
89
  # @overload sample(number, period: nil)
@@ -92,14 +96,14 @@ class ULID
92
96
  # * Do not ensure the uniqueness
93
97
  # * Do not take random generator for the arguments
94
98
  # * Raising error instead of truncating elements for the given number
95
- def self.sample(*args, period: nil)
99
+ def self.sample(number=nil, period: nil)
96
100
  int_generator = (
97
101
  if period
98
102
  ulid_range = range(period)
99
103
  min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
100
104
 
101
105
  possibilities = (max - min) + (exclude_end ? 0 : 1)
102
- raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
106
+ raise(ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities") unless possibilities.positive?
103
107
 
104
108
  -> {
105
109
  SecureRandom.random_number(possibilities) + min
@@ -109,24 +113,21 @@ class ULID
109
113
  end
110
114
  )
111
115
 
112
- case args.size
113
- when 0
116
+ case number
117
+ when nil
114
118
  from_integer(int_generator.call)
115
- when 1
116
- number = args.first
117
- raise ArgumentError, 'accepts no argument or integer only' unless Integer === number
118
-
119
+ when Integer
119
120
  if number > MAX_INTEGER || number.negative?
120
- raise ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative"
121
+ raise(ArgumentError, "given number `#{number}` is larger than ULID limit `#{MAX_INTEGER}` or negative")
121
122
  end
122
123
 
123
- if period && (number > possibilities)
124
- raise ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`"
124
+ if period && possibilities && (number > possibilities)
125
+ raise(ArgumentError, "given number `#{number}` is larger than given possibilities `#{possibilities}`")
125
126
  end
126
127
 
127
128
  Array.new(number) { from_integer(int_generator.call) }
128
129
  else
129
- raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
130
+ raise(ArgumentError, 'accepts no argument or integer only')
130
131
  end
131
132
  end
132
133
 
@@ -136,11 +137,13 @@ class ULID
136
137
  # @yieldreturn [self]
137
138
  def self.scan(string)
138
139
  string = String.try_convert(string)
139
- raise ArgumentError, 'ULID.scan takes only strings' unless string
140
- return to_enum(__callee__, string) unless block_given?
140
+ raise(ArgumentError, 'ULID.scan takes only strings') unless string
141
+ return to_enum(:scan, string) unless block_given?
141
142
 
142
143
  string.scan(SCANNING_PATTERN) do |matched|
143
- yield parse(matched)
144
+ if String === matched
145
+ yield(parse(matched))
146
+ end
144
147
  end
145
148
  self
146
149
  end
@@ -150,14 +153,16 @@ class ULID
150
153
  # @raise [OverflowError] if the given integer is larger than the ULID limit
151
154
  # @raise [ArgumentError] if the given integer is negative number
152
155
  def self.from_integer(integer)
153
- raise ArgumentError, 'ULID.from_integer takes only `Integer`' unless Integer === integer
154
- raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
155
- raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
156
+ raise(ArgumentError, 'ULID.from_integer takes only `Integer`') unless Integer === integer
157
+ raise(OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}") unless integer <= MAX_INTEGER
158
+ raise(ArgumentError, "integer should not be negative: given: #{integer}") if integer.negative?
156
159
 
157
160
  n32encoded = integer.to_s(32).rjust(ENCODED_LENGTH, '0')
158
161
  n32encoded_timestamp = n32encoded.slice(0, TIMESTAMP_ENCODED_LENGTH)
159
162
  n32encoded_randomness = n32encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH)
160
163
 
164
+ raise(UnexpectedError) unless n32encoded_timestamp && n32encoded_randomness
165
+
161
166
  milliseconds = n32encoded_timestamp.to_i(32)
162
167
  entropy = n32encoded_randomness.to_i(32)
163
168
 
@@ -168,37 +173,43 @@ class ULID
168
173
  # @return [Range<ULID>]
169
174
  # @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
170
175
  def self.range(period)
171
- raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period
176
+ raise(ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`') unless Range === period
172
177
 
173
178
  begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
174
- return period if self === begin_element && self === end_element
175
-
176
- case begin_element
177
- when Time
178
- begin_ulid = min(begin_element)
179
- when nil
180
- begin_ulid = MIN
181
- when self
182
- begin_ulid = begin_element
183
- else
184
- raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
185
- end
179
+ new_begin, new_end = false, false
180
+
181
+ begin_ulid = (
182
+ case begin_element
183
+ when Time
184
+ new_begin = true
185
+ min(begin_element)
186
+ when nil
187
+ MIN
188
+ when self
189
+ begin_element
190
+ else
191
+ raise(ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}")
192
+ end
193
+ )
186
194
 
187
- case end_element
188
- when Time
189
- end_ulid = exclude_end ? min(end_element) : max(end_element)
190
- when nil
191
- # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
192
- end_ulid = MAX
193
- exclude_end = false
194
- when self
195
- end_ulid = end_element
196
- else
197
- raise ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}"
198
- end
195
+ end_ulid = (
196
+ case end_element
197
+ when Time
198
+ new_end = true
199
+ exclude_end ? min(end_element) : max(end_element)
200
+ when nil
201
+ exclude_end = false
202
+ # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
203
+ MAX
204
+ when self
205
+ end_element
206
+ else
207
+ raise(ArgumentError, "ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`, given: #{period.inspect}")
208
+ end
209
+ )
199
210
 
200
- begin_ulid.freeze
201
- end_ulid.freeze
211
+ begin_ulid.freeze if new_begin
212
+ end_ulid.freeze if new_end
202
213
 
203
214
  Range.new(begin_ulid, end_ulid, exclude_end)
204
215
  end
@@ -206,7 +217,7 @@ class ULID
206
217
  # @param [Time] time
207
218
  # @return [Time]
208
219
  def self.floor(time)
209
- raise ArgumentError, 'ULID.floor takes only `Time` instance' unless Time === time
220
+ raise(ArgumentError, 'ULID.floor takes only `Time` instance') unless Time === time
210
221
 
211
222
  time.floor(3)
212
223
  end
@@ -234,7 +245,7 @@ class ULID
234
245
  when Time
235
246
  milliseconds_from_time(moment)
236
247
  else
237
- raise ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`'
248
+ raise(ArgumentError, '`moment` should be a `Time` or `Integer as milliseconds`')
238
249
  end
239
250
  end
240
251
 
@@ -248,39 +259,67 @@ class ULID
248
259
  # @raise [ParserError] if the given format is not correct for ULID specs
249
260
  def self.parse(string)
250
261
  string = String.try_convert(string)
251
- raise ArgumentError, 'ULID.parse takes only strings' unless string
262
+ raise(ArgumentError, 'ULID.parse takes only strings') unless string
252
263
 
253
264
  unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
254
- raise ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`"
265
+ raise(ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`")
255
266
  end
256
267
 
257
268
  from_integer(CrockfordBase32.decode(string))
258
269
  end
259
270
 
271
+ # @param [String, #to_str] string
272
+ # @return [ULID]
273
+ # @raise [ParserError] if the given format is not correct for ULID specs
274
+ def self.parse_variant_format(string)
275
+ string = String.try_convert(string)
276
+ raise(ArgumentError, 'ULID.parse_variant_format takes only strings') unless string
277
+
278
+ normalized_in_crockford = CrockfordBase32.normalize(string)
279
+ parse(normalized_in_crockford)
280
+ end
281
+
260
282
  # @param [String, #to_str] string
261
283
  # @return [String]
262
284
  # @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
263
285
  def self.normalize(string)
264
286
  string = String.try_convert(string)
265
- raise ArgumentError, 'ULID.normalize takes only strings' unless string
287
+ raise(ArgumentError, 'ULID.normalize takes only strings') unless string
266
288
 
267
- normalized_in_crockford = CrockfordBase32.normalize(string)
268
289
  # Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
269
- parse(normalized_in_crockford).to_s
290
+ parse_variant_format(string).to_s
270
291
  end
271
292
 
293
+ # @param [String, #to_str] string
272
294
  # @return [Boolean]
273
- def self.normalized?(object)
274
- normalized = normalize(object)
295
+ def self.normalized?(string)
296
+ normalized = normalize(string)
275
297
  rescue Exception
276
298
  false
277
299
  else
278
- normalized == object
300
+ normalized == string
279
301
  end
280
302
 
303
+ # @param [String, #to_str] string
281
304
  # @return [Boolean]
282
- def self.valid?(object)
283
- string = String.try_convert(object)
305
+ def self.valid_as_variant_format?(string)
306
+ normalize(string)
307
+ rescue Exception
308
+ false
309
+ else
310
+ true
311
+ end
312
+
313
+ # @deprecated Use [.valid_as_variant_format?] or [.normalized?] instead
314
+ #
315
+ # Returns `true` if it is normalized string.
316
+ # Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs.
317
+ #
318
+ # @return [Boolean]
319
+ def self.valid?(string)
320
+ warn_kwargs = (RUBY_VERSION >= '3.0') ? { category: :deprecated } : {}
321
+ Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.', **warn_kwargs)
322
+ string = String.try_convert(string)
284
323
  string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
285
324
  end
286
325
 
@@ -298,7 +337,7 @@ class ULID
298
337
  else
299
338
  object_class_name = safe_get_class_name(object)
300
339
  converted_class_name = safe_get_class_name(converted)
301
- raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
340
+ raise(TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})")
302
341
  end
303
342
  end
304
343
  end
@@ -314,8 +353,12 @@ class ULID
314
353
  begin
315
354
  object.class
316
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
317
360
  singleton_class = class << object; self; end
318
- singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) }
361
+ (Class === singleton_class) ? singleton_class.ancestors.detect { |ancestor| !ancestor.equal?(singleton_class) } : fallback
319
362
  end
320
363
  )
321
364
 
@@ -335,10 +378,10 @@ class ULID
335
378
  # @raise [OverflowError] if the given value is larger than the ULID limit
336
379
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
337
380
  def self.from_milliseconds_and_entropy(milliseconds:, entropy:)
338
- raise ArgumentError, 'milliseconds and entropy should be an `Integer`' unless Integer === milliseconds && Integer === entropy
339
- raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
340
- raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
341
- raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
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?
342
385
 
343
386
  n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
344
387
  n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
@@ -347,7 +390,8 @@ class ULID
347
390
  new(milliseconds: milliseconds, entropy: entropy, integer: integer)
348
391
  end
349
392
 
350
- attr_reader :milliseconds, :entropy
393
+ # @dynamic milliseconds, entropy
394
+ attr_reader(:milliseconds, :entropy)
351
395
 
352
396
  # @api private
353
397
  # @param [Integer] milliseconds
@@ -376,7 +420,7 @@ class ULID
376
420
  [ULID, @integer].hash
377
421
  end
378
422
 
379
- # @return [Integer, nil]
423
+ # @return [-1, 0, 1, nil]
380
424
  def <=>(other)
381
425
  (ULID === other) ? (@integer <=> other.to_i) : nil
382
426
  end
@@ -390,15 +434,29 @@ class ULID
390
434
  def eql?(other)
391
435
  equal?(other) || (ULID === other && @integer == other.to_i)
392
436
  end
393
- alias_method :==, :eql?
437
+ # @dynamic ==
438
+ alias_method(:==, :eql?)
394
439
 
440
+ # Return `true` for same value of ULID, variant formats of strings, same Time in ULID precision(msec).
441
+ # Do not consider integer, octets and partial strings, then returns `false`.
442
+ #
395
443
  # @return [Boolean]
444
+ # @see .normalize
445
+ # @see .floor
396
446
  def ===(other)
397
447
  case other
398
448
  when ULID
399
449
  @integer == other.to_i
400
450
  when String
401
- to_s == other.upcase
451
+ begin
452
+ normalized = ULID.normalize(other)
453
+ rescue Exception
454
+ false
455
+ else
456
+ to_s == normalized
457
+ end
458
+ when Time
459
+ to_time == ULID.floor(other)
402
460
  else
403
461
  false
404
462
  end
@@ -420,22 +478,22 @@ class ULID
420
478
 
421
479
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
422
480
  def timestamp_octets
423
- octets.slice(0, TIMESTAMP_OCTETS_LENGTH)
481
+ octets.slice(0, TIMESTAMP_OCTETS_LENGTH) || raise(UnexpectedError)
424
482
  end
425
483
 
426
484
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
427
485
  def randomness_octets
428
- octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH)
486
+ octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH) || raise(UnexpectedError)
429
487
  end
430
488
 
431
489
  # @return [String]
432
490
  def timestamp
433
- @timestamp ||= to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze
491
+ @timestamp ||= (to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
434
492
  end
435
493
 
436
494
  # @return [String]
437
495
  def randomness
438
- @randomness ||= to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze
496
+ @randomness ||= (to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
439
497
  end
440
498
 
441
499
  # @note Providing for rough operations. The keys and values is not fixed.
@@ -461,7 +519,8 @@ class ULID
461
519
  ULID.from_integer(succ_int)
462
520
  end
463
521
  end
464
- alias_method :next, :succ
522
+ # @dynamic next
523
+ alias_method(:next, :succ)
465
524
 
466
525
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
467
526
  def pred
@@ -513,7 +572,7 @@ class ULID
513
572
  self
514
573
  end
515
574
 
516
- undef_method :instance_variable_set
575
+ undef_method(:instance_variable_set)
517
576
 
518
577
  private
519
578
 
@@ -525,13 +584,12 @@ class ULID
525
584
  end
526
585
  end
527
586
 
528
- require_relative 'ulid/version'
529
- require_relative 'ulid/crockford_base32'
530
- require_relative 'ulid/monotonic_generator'
587
+ require_relative('ulid/version')
588
+ require_relative('ulid/crockford_base32')
589
+ require_relative('ulid/monotonic_generator')
590
+ require_relative('ulid/ractor_unshareable_constants')
531
591
 
532
592
  class ULID
533
- MIN = parse('00000000000000000000000000').freeze
534
- MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
535
-
536
- private_constant :TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR
593
+ # Do not write as `ULID.private_constant` for avoiding YARD warnings `[warn]: in YARD::Handlers::Ruby::PrivateConstantHandler: Undocumentable private constants:`
594
+ private_constant(:TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR, :CROCKFORD_BASE32_ENCODING_STRING)
537
595
  end
data/sig/ulid.rbs CHANGED
@@ -36,7 +36,7 @@ class ULID < Object
36
36
  end
37
37
 
38
38
  module CrockfordBase32
39
- class SetupError < ScriptError
39
+ class SetupError < UnexpectedError
40
40
  end
41
41
 
42
42
  N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
@@ -97,9 +97,9 @@ class ULID < Object
97
97
  def to_ulid: () -> ULID
98
98
  end
99
99
 
100
- type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
101
- type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
102
- type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
100
+ type full_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
101
+ type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
102
+ type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
103
103
  type period = Range[Time] | Range[nil] | Range[ULID]
104
104
 
105
105
  @string: String?
@@ -213,6 +213,21 @@ class ULID < Object
213
213
  # ```
214
214
  def self.parse: (_ToStr string) -> ULID
215
215
 
216
+ # Get ULID instance from unnormalized String that encoded in Crockford's base32.
217
+ #
218
+ # http://www.crockford.com/base32.html
219
+ #
220
+ # * Ignore Hyphens (-)
221
+ # * Mapping 0 O o => 0, 1 I i L l => 1
222
+ #
223
+ # ```ruby
224
+ # ulid = ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD')
225
+ # #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D)
226
+ # ```
227
+ #
228
+ # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
229
+ def self.parse_variant_format: (_ToStr string) -> ULID
230
+
216
231
  # ```ruby
217
232
  # # Currently experimental feature, so needed to load the extension.
218
233
  # require 'ulid/uuid'
@@ -292,41 +307,30 @@ class ULID < Object
292
307
  # ulid2 = ULID.parse('01F4PTVCSN9ZPFKYTY2DDJVRK4') #=> ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4)
293
308
  # ulids = ULID.sample(1000, period: ulid1..ulid2)
294
309
  # ulids.uniq.size #=> 1000
295
- # ulids.take(10)
310
+ # ulids.take(5)
296
311
  # #=>
297
312
  # #[ULID(2021-05-02 06:57:19.954 UTC: 01F4NXW02JNB8H0J0TK48JD39X),
298
313
  # # ULID(2021-05-02 07:06:07.458 UTC: 01F4NYC372GVP7NS0YAYQGT4VZ),
299
314
  # # ULID(2021-05-01 06:16:35.791 UTC: 01F4K94P6F6P68K0H64WRDSFKW),
300
315
  # # ULID(2021-04-27 22:17:37.844 UTC: 01F4APHGSMFJZQTGXKZBFFBPJP),
301
- # # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3),
302
- # # ULID(2021-04-30 07:18:54.307 UTC: 01F4GTA2332AS2VPHC4FMKC7R5),
303
- # # ULID(2021-05-02 12:26:03.480 UTC: 01F4PGNXARG554Y3HYVBDW4T9S),
304
- # # ULID(2021-04-29 09:52:15.107 UTC: 01F4EGP483ZX2747FQPWQNPPMW),
305
- # # ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
306
- # # ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
316
+ # # ULID(2021-04-28 20:17:55.357 UTC: 01F4D231MXQJXAR8G2JZHEJNH3)]
307
317
  # ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
308
318
  # #=>
309
319
  # # [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
310
320
  # # ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
311
321
  # # ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
312
322
  # # ULID(2021-05-01 03:06:09.130 UTC: 01F4JY7ZBABCBMX16XH2Q4JW4W),
313
- # # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G),
314
- # # ULID(2021-04-29 17:14:14.116 UTC: 01F4F9ZDQ449BE8BBZFEHYQWG2),
315
- # # ULID(2021-04-30 16:18:08.205 UTC: 01F4HS5DPD1HWDVJNJ6YKJXKSK),
316
- # # ULID(2021-04-30 10:31:33.602 UTC: 01F4H5ATF2A1CSQF0XV5NKZ288),
317
- # # ULID(2021-04-28 16:49:06.484 UTC: 01F4CP4PDM214Q6H3KJP7DYJRR),
318
- # # ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
323
+ # # ULID(2021-04-29 21:38:58.109 UTC: 01F4FS45DX4049JEQK4W6TER6G)]
319
324
  # ```
320
325
  def self.sample: (?period: period) -> ULID
321
- | (Integer number, ?period: period) -> Array[ULID]
322
- def self.valid?: (untyped) -> bool
326
+ | (Integer number, ?period: period?) -> Array[ULID]
323
327
 
324
328
  # Returns normalized string
325
329
  #
326
330
  # ```ruby
331
+ # ULID.normalize('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> "01G70Y0Y7GZ1XWDAREXERGSDDD"
327
332
  # ULID.normalize('-olarz3-noekisv4rrff-q6ig5fav--') #=> "01ARZ3N0EK1SV4RRFFQ61G5FAV"
328
- # ULID.normalized?('-olarz3-noekisv4rrff-q6ig5fav--') #=> false
329
- # ULID.normalized?('01ARZ3N0EK1SV4RRFFQ61G5FAV') #=> true
333
+ # ULID.normalize('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> ULID::ParserError
330
334
  # ```
331
335
  #
332
336
  # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
@@ -335,13 +339,40 @@ class ULID < Object
335
339
  # Returns `true` if it is normalized string
336
340
  #
337
341
  # ```ruby
338
- # ULID.normalize('-olarz3-noekisv4rrff-q6ig5fav--') #=> "01ARZ3N0EK1SV4RRFFQ61G5FAV"
339
- # ULID.normalized?('-olarz3-noekisv4rrff-q6ig5fav--') #=> false
340
- # ULID.normalized?('01ARZ3N0EK1SV4RRFFQ61G5FAV') #=> true
342
+ # ULID.normalized?('01G70Y0Y7GZ1XWDAREXERGSDDD') #=> true
343
+ # ULID.normalized?('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> false
344
+ # ULID.normalized?(ULID.generate.to_s.downcase) #=> false
345
+ # ULID.normalized?('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> false (Not raising ULID::ParserError)
346
+ # ```
347
+ #
348
+ # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
349
+ def self.normalized?: (_ToStr string) -> bool
350
+ | (untyped) -> false
351
+
352
+ # Returns `true` if it is valid in ULID format variants
353
+ #
354
+ # ```ruby
355
+ # ULID.valid_as_variant_format?(ULID.generate.to_s.downcase) #=> true
356
+ # ULID.valid_as_variant_format?('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> true
357
+ # ULID.valid_as_variant_format?('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> false
341
358
  # ```
342
359
  #
343
360
  # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
344
- def self.normalized?: (untyped) -> bool
361
+ def self.valid_as_variant_format?: (_ToStr string) -> bool
362
+ | (untyped) -> false
363
+
364
+ # DEPRECATED Use valid_as_variant_format? instead
365
+ #
366
+ # Returns `true` if it is normalized string.
367
+ # Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs.
368
+ #
369
+ # ```ruby
370
+ # ULID.valid?(ULID.generate.to_s.downcase) #=> true
371
+ # ```
372
+ #
373
+ # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3)
374
+ def self.valid?: (_ToStr string) -> bool
375
+ | (untyped) -> false
345
376
 
346
377
  # Returns parsed ULIDs from given String for rough operations.
347
378
  #
@@ -409,7 +440,9 @@ class ULID < Object
409
440
  # ulid.to_i #=> 1957909092946624190749577070267409738
410
441
  # ```
411
442
  def to_i: -> Integer
412
- alias hash to_i
443
+
444
+ # Returns integer for making as a hash key use-case
445
+ def hash: -> Integer
413
446
 
414
447
  # Basically same as String based sort.
415
448
  #
@@ -419,7 +452,12 @@ class ULID < Object
419
452
  # #=> true
420
453
  # ```
421
454
  #
422
- # To be precise, this sorting unaffected with `case sensitive or not` and might handle [ulid/spec#57](https://github.com/ulid/spec/pull/57) in future. So preferable than `lexicographically sortable` in actual case.
455
+ # To be precise, this sorting unaffected with `case sensitive or not` and might handle [ulid/spec#57](https://github.com/ulid/spec/pull/57) in future.
456
+ # So preferable than `lexicographically sortable` in actual case.
457
+ #
458
+ # This returns -1 | 0 | 1 for ULIDs. However defined as returning Integer. It is caused on ruby/rbs current definition.
459
+ # https://github.com/ruby/ruby/blob/cd34f56d450f2310cceaf4c5f34d23eddfda58e8/numeric.c#L4646-L4660
460
+ # https://github.com/ruby/rbs/blob/14abbbae8885a09a2ed82de2ef31d67a9c0a108d/core/integer.rbs#L461-L462
423
461
  #
424
462
  def <=>: (ULID other) -> Integer
425
463
  | (untyped other) -> nil
@@ -434,9 +472,28 @@ class ULID < Object
434
472
  # ULID.parse('4NNB20D9C1ME2NGMTX51ERZJX0') == ULID.parse('4nnb20d9c1me2ngmtx51erzjx0')
435
473
  # #=> true
436
474
  # ```
437
- def eql?: (untyped other) -> bool
475
+ def eql?: (ULID other) -> bool
476
+ | (untyped other) -> false
438
477
  alias == eql?
439
- def ===: (untyped other) -> bool
478
+
479
+ # Return `true` for same value of ULID, variant formats of strings, same Time in ULID precision(msec).
480
+ # Do not consider integer, octets and partial strings, then returns `false`.
481
+ #
482
+ # ```ruby
483
+ # ulid = ULID.parse('01G6Z7Q4RSH97E6QHAC7VK19G2')
484
+ # ulid === ULID.parse(ulid.to_s)
485
+ # #=> true
486
+ # ulid === ulid.to_s.downcase
487
+ # #=> true
488
+ # ulid === ulid.to_time
489
+ # #=> true
490
+ # ulid === ulid.to_i
491
+ # #=> false
492
+ # ulid === ulid.next
493
+ # #=> false
494
+ # ```
495
+ def ===: (ULID | String | Time other) -> bool
496
+ | (untyped other) -> false
440
497
 
441
498
  # ```ruby
442
499
  # ulid = ULID.generate
@@ -462,7 +519,7 @@ class ULID < Object
462
519
  # Intentionally avoiding to use `Record type` ref: https://github.com/ruby/rbs/blob/4fb4c33b2325d1a73d79ff7aaeb49f21cec1e0e5/docs/syntax.md#record-type
463
520
  # Because the returning values are not fixed.
464
521
  def patterns: -> Hash[Symbol, Regexp | String]
465
- def octets: -> octets
522
+ def octets: -> full_octets
466
523
  def timestamp_octets: -> timestamp_octets
467
524
  def randomness_octets: -> randomness_octets
468
525
 
@@ -525,7 +582,7 @@ class ULID < Object
525
582
  private
526
583
 
527
584
  # A private API. Should not be used in your code.
528
- def self.new: (milliseconds: Integer, entropy: Integer, integer: Integer) -> self
585
+ def self.new: (milliseconds: Integer, entropy: Integer, integer: Integer) -> ULID
529
586
 
530
587
  # A private API. Should not be used in your code.
531
588
  def self.reasonable_entropy: -> Integer
metadata CHANGED
@@ -1,19 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-ulid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.5.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-05-26 00:00:00.000000000 Z
11
+ date: 2022-07-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: |2
14
- The ULID(Universally Unique Lexicographically Sortable Identifier) has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
15
- This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
16
- Also providing `ruby/rbs` signature files.
13
+ description: " generator, optional monotonicity, parser and tools for ULID (RBS
14
+ included)\n"
17
15
  email:
18
16
  - kachick1+ruby@gmail.com
19
17
  executables: []
@@ -26,6 +24,7 @@ files:
26
24
  - lib/ulid.rb
27
25
  - lib/ulid/crockford_base32.rb
28
26
  - lib/ulid/monotonic_generator.rb
27
+ - lib/ulid/ractor_unshareable_constants.rb
29
28
  - lib/ulid/uuid.rb
30
29
  - lib/ulid/version.rb
31
30
  - sig/ulid.rbs
@@ -46,7 +45,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
46
45
  requirements:
47
46
  - - ">="
48
47
  - !ruby/object:Gem::Version
49
- version: 2.7.0
48
+ version: 2.7.2
50
49
  required_rubygems_version: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - ">="