ruby-ulid 0.5.0 → 0.7.0

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