ruby-ulid 0.2.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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
  - - ">="