ruby-ulid 0.5.0 → 0.6.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: 82f48c45c124521403b04938ec4498a85bf292645357d75f32173cc2b9613618
4
+ data.tar.gz: 9c6269cbef649b57df980d120116687cdcc33e505cb45585b4566f4b16f3cdf2
5
5
  SHA512:
6
- metadata.gz: aa826d204cbdc1e6850fbeab278dc72e5b683926db9af7ed477ec2e74837d32829dbfb468c1156d6dcf5be604f9fdeb8fd0a282999b47a627884152b2a2cd5d4
7
- data.tar.gz: 0b1208e76af50f9952c8c4835d1410c7e1fd114313cc71d39a143c9bc04ac4f1a35ba49be88a49406606c441e4f4fa7cad5fc938d74fcecde0d612319a6890b3
6
+ metadata.gz: 817d5589c2967d8d1b787ce20c147483cd12769eba22a0489ade4002285e1edc31e06f9e5d8dd590d3cf2342d2875c388818e55b95cbd1d72921b1848287483f
7
+ data.tar.gz: 6bcacebe7955bb247127ddc64df1fec2cc776954e7c86530750c200da5420fd4cbb73d5fd2f0888cb37ff5f097987dd12f372ee591adccb8b35f35e03e4f0538
data/README.md CHANGED
@@ -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.6.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.6.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.6.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 the speed. However it actually works faster than others! :zap:
120
+
121
+ Snapshot on 0.6.0 is below
122
+
123
+ * Generator is 1.4x faster than - [ulid gem](https://github.com/rafaelsales/ulid)
124
+ * Generator is 1.7x 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
@@ -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
 
@@ -277,7 +297,7 @@ ULID.max(time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
277
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"
@@ -15,63 +15,62 @@ class ULID
15
15
  # * https://github.com/kachick/ruby-ulid/issues/57
16
16
  # * https://github.com/kachick/ruby-ulid/issues/78
17
17
  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
18
+ # Excluded I, L, O, U, - from Base32
19
+ base32_to_crockford = {
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',
38
+ 'I' => 'J',
39
+ 'J' => 'K',
40
+ 'K' => 'M',
41
+ 'L' => 'N',
42
+ 'M' => 'P',
43
+ 'N' => 'Q',
44
+ 'O' => 'R',
45
+ 'P' => 'S',
46
+ 'Q' => 'T',
47
+ 'R' => 'V',
48
+ 'S' => 'W',
49
+ 'T' => 'X',
50
+ 'U' => 'Y',
51
+ 'V' => 'Z'
44
52
  }.freeze
53
+ BASE32_TR_PATTERN = base32_to_crockford.keys.join.freeze
54
+ ENCODING_STRING = CROCKFORD_BASE32_TR_PATTERN = base32_to_crockford.values.freeze.join.freeze
45
55
 
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
58
-
59
- STANDARD_BY_VARIANT = {
56
+ normarized_by_variant = {
60
57
  'L' => '1',
61
58
  'l' => '1',
62
59
  'I' => '1',
63
60
  'i' => '1',
64
61
  'O' => '0',
65
- 'o' => '0',
66
- '-' => ''
62
+ 'o' => '0'
67
63
  }.freeze
68
- VARIANT_PATTERN = /[#{STANDARD_BY_VARIANT.keys.join}]/.freeze
64
+ VARIANT_TR_PATTERN = normarized_by_variant.keys.join.freeze
65
+ NORMALIZED_TR_PATTERN = normarized_by_variant.values.join.freeze
66
+
67
+ # @note Avoid to depend regex as possible. `tr(string, string)` is almost 2x Faster than `gsub(regex, hash)` in Ruby 3.1
69
68
 
70
69
  # @api private
71
70
  # @param [String] string
72
71
  # @return [Integer]
73
72
  def self.decode(string)
74
- n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
73
+ n32encoded = string.upcase.tr(CROCKFORD_BASE32_TR_PATTERN, BASE32_TR_PATTERN)
75
74
  n32encoded.to_i(32)
76
75
  end
77
76
 
@@ -80,14 +79,21 @@ class ULID
80
79
  # @return [String]
81
80
  def self.encode(integer)
82
81
  n32encoded = integer.to_s(32)
83
- n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
82
+ from_n32(n32encoded).rjust(ENCODED_LENGTH, '0')
84
83
  end
85
84
 
86
85
  # @api private
87
86
  # @param [String] string
88
87
  # @return [String]
89
88
  def self.normalize(string)
90
- string.gsub(VARIANT_PATTERN, STANDARD_BY_VARIANT)
89
+ string.delete('-').tr(VARIANT_TR_PATTERN, NORMALIZED_TR_PATTERN)
90
+ end
91
+
92
+ # @api private
93
+ # @param [String] n32encoded
94
+ # @return [String]
95
+ def self.from_n32(n32encoded)
96
+ n32encoded.upcase.tr(BASE32_TR_PATTERN, CROCKFORD_BASE32_TR_PATTERN)
91
97
  end
92
98
  end
93
99
  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
@@ -10,7 +10,6 @@ class ULID
10
10
  # However it is a C extention, I'm pending to use it for now.
11
11
  include(MonitorMixin)
12
12
 
13
- # @dynamic prev
14
13
  # @return [ULID, nil]
15
14
  attr_reader(:prev)
16
15
 
@@ -25,7 +24,6 @@ class ULID
25
24
  def inspect
26
25
  "ULID::MonotonicGenerator(prev: #{@prev.inspect})"
27
26
  end
28
- # @dynamic to_s
29
27
  alias_method(:to_s, :inspect)
30
28
 
31
29
  # @param [Time, Integer] moment
@@ -70,6 +68,12 @@ class ULID
70
68
  end
71
69
  end
72
70
 
71
+ # @param [Time, Integer] moment
72
+ # @return [String]
73
+ def encode(moment: ULID.current_milliseconds)
74
+ generate(moment: moment).encode
75
+ end
76
+
73
77
  undef_method(:freeze)
74
78
 
75
79
  # @raise [TypeError] always raises exception and does not freeze self
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.6.0'
7
7
  end
data/lib/ulid.rb CHANGED
@@ -6,6 +6,11 @@
6
6
 
7
7
  require('securerandom')
8
8
 
9
+ require_relative('ulid/version')
10
+ require_relative('ulid/errors')
11
+ require_relative('ulid/crockford_base32')
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.
@@ -60,6 +55,16 @@ class ULID
60
55
  from_milliseconds_and_entropy(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
61
56
  end
62
57
 
58
+ # Almost same as [.generate] except directly returning String without needless object creation
59
+ #
60
+ # @param [Integer, Time] moment
61
+ # @param [Integer] entropy
62
+ # @return [String]
63
+ def self.encode(moment: current_milliseconds, entropy: reasonable_entropy)
64
+ n32_encoded = encode_n32(milliseconds: milliseconds_from_moment(moment), entropy: entropy)
65
+ CrockfordBase32.from_n32(n32_encoded)
66
+ end
67
+
63
68
  # Short hand of `ULID.generate(moment: time)`
64
69
  # @param [Time] time
65
70
  # @return [ULID]
@@ -166,7 +171,12 @@ class ULID
166
171
  milliseconds = n32encoded_timestamp.to_i(32)
167
172
  entropy = n32encoded_randomness.to_i(32)
168
173
 
169
- new(milliseconds: milliseconds, entropy: entropy, integer: integer)
174
+ new(
175
+ milliseconds: milliseconds,
176
+ entropy: entropy,
177
+ integer: integer,
178
+ encoded: CrockfordBase32.from_n32(n32encoded).freeze
179
+ )
170
180
  end
171
181
 
172
182
  # @param [Range<Time>, Range<nil>, Range[ULID]] period
@@ -254,6 +264,17 @@ class ULID
254
264
  SecureRandom.random_number(MAX_ENTROPY)
255
265
  end
256
266
 
267
+ private_class_method def self.encode_n32(milliseconds:, entropy:)
268
+ raise(ArgumentError, 'milliseconds and entropy should be an `Integer`') unless Integer === milliseconds && Integer === entropy
269
+ raise(OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}") unless milliseconds <= MAX_MILLISECONDS
270
+ raise(OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}") unless entropy <= MAX_ENTROPY
271
+ raise(ArgumentError, 'milliseconds and entropy should not be negative') if milliseconds.negative? || entropy.negative?
272
+
273
+ n32encoded_timestamp = milliseconds.to_s(32).rjust(TIMESTAMP_ENCODED_LENGTH, '0')
274
+ n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
275
+ "#{n32encoded_timestamp}#{n32encoded_randomness}"
276
+ end
277
+
257
278
  # @param [String, #to_str] string
258
279
  # @return [ULID]
259
280
  # @raise [ParserError] if the given format is not correct for ULID specs
@@ -279,6 +300,25 @@ class ULID
279
300
  parse(normalized_in_crockford)
280
301
  end
281
302
 
303
+ # Almost same as `ULID.parse(string).to_time` except directly returning Time instance without needless object creation
304
+ #
305
+ # @param [String, #to_str] string
306
+ # @return [Time]
307
+ # @raise [ParserError] if the given format is not correct for ULID specs
308
+ def self.decode_time(string, in: 'UTC')
309
+ in_for_time_at = binding.local_variable_get(:in) # Needed because `in` is a reserved word.
310
+ string = String.try_convert(string)
311
+ raise(ArgumentError, 'ULID.decode_time takes only strings') unless string
312
+
313
+ unless STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string)
314
+ raise(ParserError, "given `#{string}` does not match to `#{STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.inspect}`")
315
+ end
316
+
317
+ timestamp = string.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError)
318
+
319
+ Time.at(0, CrockfordBase32.decode(timestamp), :millisecond, in: in_for_time_at)
320
+ end
321
+
282
322
  # @param [String, #to_str] string
283
323
  # @return [String]
284
324
  # @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
@@ -303,7 +343,7 @@ class ULID
303
343
  # @param [String, #to_str] string
304
344
  # @return [Boolean]
305
345
  def self.valid_as_variant_format?(string)
306
- normalize(string)
346
+ parse_variant_format(string)
307
347
  rescue Exception
308
348
  false
309
349
  else
@@ -378,37 +418,36 @@ class ULID
378
418
  # @raise [OverflowError] if the given value is larger than the ULID limit
379
419
  # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
380
420
  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)
421
+ n32_encoded = encode_n32(milliseconds: milliseconds, entropy: entropy)
422
+ new(
423
+ milliseconds: milliseconds,
424
+ entropy: entropy,
425
+ integer: n32_encoded.to_i(32),
426
+ encoded: CrockfordBase32.from_n32(n32_encoded).upcase.freeze
427
+ )
391
428
  end
392
429
 
393
- # @dynamic milliseconds, entropy
394
430
  attr_reader(:milliseconds, :entropy)
395
431
 
396
432
  # @api private
397
433
  # @param [Integer] milliseconds
398
434
  # @param [Integer] entropy
399
435
  # @param [Integer] integer
436
+ # @param [String] encoded
400
437
  # @return [void]
401
- def initialize(milliseconds:, entropy:, integer:)
438
+ def initialize(milliseconds:, entropy:, integer:, encoded:)
402
439
  # All arguments check should be done with each constructors, not here
403
440
  @integer = integer
441
+ @encoded = encoded
404
442
  @milliseconds = milliseconds
405
443
  @entropy = entropy
406
444
  end
407
445
 
408
446
  # @return [String]
409
- def to_s
410
- @string ||= CrockfordBase32.encode(@integer).freeze
447
+ def encode
448
+ @encoded
411
449
  end
450
+ alias_method(:to_s, :encode)
412
451
 
413
452
  # @return [Integer]
414
453
  def to_i
@@ -427,14 +466,13 @@ class ULID
427
466
 
428
467
  # @return [String]
429
468
  def inspect
430
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
469
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{@encoded})".freeze
431
470
  end
432
471
 
433
472
  # @return [Boolean]
434
473
  def eql?(other)
435
474
  equal?(other) || (ULID === other && @integer == other.to_i)
436
475
  end
437
- # @dynamic ==
438
476
  alias_method(:==, :eql?)
439
477
 
440
478
  # Return `true` for same value of ULID, variant formats of strings, same Time in ULID precision(msec).
@@ -449,11 +487,9 @@ class ULID
449
487
  @integer == other.to_i
450
488
  when String
451
489
  begin
452
- normalized = ULID.normalize(other)
490
+ to_i == ULID.parse_variant_format(other).to_i
453
491
  rescue Exception
454
492
  false
455
- else
456
- to_s == normalized
457
493
  end
458
494
  when Time
459
495
  to_time == ULID.floor(other)
@@ -488,12 +524,12 @@ class ULID
488
524
 
489
525
  # @return [String]
490
526
  def timestamp
491
- @timestamp ||= (to_s.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
527
+ @timestamp ||= (@encoded.slice(0, TIMESTAMP_ENCODED_LENGTH).freeze || raise(UnexpectedError))
492
528
  end
493
529
 
494
530
  # @return [String]
495
531
  def randomness
496
- @randomness ||= (to_s.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
532
+ @randomness ||= (@encoded.slice(TIMESTAMP_ENCODED_LENGTH, RANDOMNESS_ENCODED_LENGTH).freeze || raise(UnexpectedError))
497
533
  end
498
534
 
499
535
  # @note Providing for rough operations. The keys and values is not fixed.
@@ -519,7 +555,6 @@ class ULID
519
555
  ULID.from_integer(succ_int)
520
556
  end
521
557
  end
522
- # @dynamic next
523
558
  alias_method(:next, :succ)
524
559
 
525
560
  # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
@@ -554,7 +589,7 @@ class ULID
554
589
  # @return [void]
555
590
  def marshal_load(integer)
556
591
  unmarshaled = ULID.from_integer(integer)
557
- initialize(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy)
592
+ initialize(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy, encoded: unmarshaled.to_s)
558
593
  end
559
594
 
560
595
  # @return [self]
@@ -584,12 +619,9 @@ class ULID
584
619
  end
585
620
  end
586
621
 
587
- require_relative('ulid/version')
588
- require_relative('ulid/crockford_base32')
589
- require_relative('ulid/monotonic_generator')
590
622
  require_relative('ulid/ractor_unshareable_constants')
591
623
 
592
624
  class ULID
593
625
  # 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)
626
+ private_constant(:TIME_FORMAT_IN_INSPECT, :MIN, :MAX, :RANDOM_INTEGER_GENERATOR)
595
627
  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
@@ -39,12 +38,11 @@ class ULID < Object
39
38
  class SetupError < UnexpectedError
40
39
  end
41
40
 
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
41
+ ENCODING_STRING: String
42
+ CROCKFORD_BASE32_TR_PATTERN: String
43
+ BASE32_TR_PATTERN: String
44
+ VARIANT_TR_PATTERN: String
45
+ NORMALIZED_TR_PATTERN: String
48
46
 
49
47
  # A private API. Should not be used in your code.
50
48
  def self.encode: (Integer integer) -> String
@@ -54,6 +52,9 @@ class ULID < Object
54
52
 
55
53
  # A private API. Should not be used in your code.
56
54
  def self.normalize: (String string) -> String
55
+
56
+ # A private API. Should not be used in your code.
57
+ def self.from_n32: (String n32encoded) -> String
57
58
  end
58
59
 
59
60
  class MonotonicGenerator
@@ -78,6 +79,9 @@ class ULID < Object
78
79
  # The `Thread-safety` is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255)
79
80
  def generate: (?moment: moment) -> ULID
80
81
 
82
+ # Just providing similar api as `ULID.generate` and `ULID.encode` relation. No performance benefit exists in monotonic generator's one.
83
+ def encode: (?moment: moment) -> String
84
+
81
85
  # Returned value is `basically not` Thread-safety
82
86
  # If you want to keep Thread-safety, keep to call {#generate} only in same {#synchronize} block
83
87
  #
@@ -102,8 +106,8 @@ class ULID < Object
102
106
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer] | Array[Integer]
103
107
  type period = Range[Time] | Range[nil] | Range[ULID]
104
108
 
105
- @string: String?
106
109
  @integer: Integer
110
+ @encoded: String
107
111
  @timestamp: String?
108
112
  @randomness: String?
109
113
  @inspect: String?
@@ -145,6 +149,16 @@ class ULID < Object
145
149
  #
146
150
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
147
151
 
152
+ # Retuns encoded and normalzied String.
153
+ # It has same arguments signatures as `.generate`. So can be used for just ID creation usecases without needless object creation.
154
+ #
155
+ # NOTE: Difference of ULID#encode, returned String is NOT frozen.
156
+ #
157
+ def self.encode: (?moment: moment, ?entropy: Integer) -> String
158
+
159
+ # A private API. Should not be used in your code.
160
+ def self.encode_n32: (milliseconds: Integer, entropy: Integer) -> String
161
+
148
162
  # Shorthand of `ULID.generate(moment: Time)`
149
163
  # See also [ULID.generate](https://kachick.github.io/ruby-ulid/ULID.html#generate-class_method)
150
164
  #
@@ -205,7 +219,7 @@ class ULID < Object
205
219
  # ```
206
220
  def self.floor: (Time time) -> Time
207
221
 
208
- # Get ULID instance from encoded String.
222
+ # Return ULID instance from encoded String.
209
223
  #
210
224
  # ```ruby
211
225
  # ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
@@ -213,6 +227,17 @@ class ULID < Object
213
227
  # ```
214
228
  def self.parse: (_ToStr string) -> ULID
215
229
 
230
+ # Return Time instance from encoded String.
231
+ # See also `ULID.encode` for similar purpose.
232
+ #
233
+ # NOTE: Difference of ULID#to_time, returned Time is NOT frozen.
234
+ #
235
+ # ```ruby
236
+ # time = ULID.decode_time('01ARZ3NDEKTSV4RRFFQ69G5FAV')
237
+ # #=> 2016-07-30 23:54:10.259 UTC
238
+ # ```
239
+ def self.decode_time: (_ToStr string, ?in: String | Integer | nil) -> Time
240
+
216
241
  # Get ULID instance from unnormalized String that encoded in Crockford's base32.
217
242
  #
218
243
  # http://www.crockford.com/base32.html
@@ -430,9 +455,12 @@ class ULID < Object
430
455
  # ```ruby
431
456
  # ulid = ULID.generate
432
457
  # #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
433
- # ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
458
+ # ulid.encode #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
459
+ # ulid.encode.frozen? #=> true
460
+ # ulid.encode.equal?(ulid.to_s) #=> true
434
461
  # ```
435
- def to_s: -> String
462
+ def encode: -> String
463
+ alias to_s encode
436
464
 
437
465
  # ```ruby
438
466
  # ulid = ULID.generate
@@ -581,9 +609,6 @@ class ULID < Object
581
609
 
582
610
  private
583
611
 
584
- # A private API. Should not be used in your code.
585
- def self.new: (milliseconds: Integer, entropy: Integer, integer: Integer) -> ULID
586
-
587
612
  # A private API. Should not be used in your code.
588
613
  def self.reasonable_entropy: -> Integer
589
614
 
@@ -594,7 +619,7 @@ class ULID < Object
594
619
  def self.safe_get_class_name: (untyped object) -> String
595
620
 
596
621
  # A private API. Should not be used in your code.
597
- def initialize: (milliseconds: Integer, entropy: Integer, integer: Integer) -> void
622
+ def initialize: (milliseconds: Integer, entropy: Integer, integer: Integer, encoded: String) -> void
598
623
 
599
624
  # A private API. Should not be used in your code.
600
625
  def cache_all_instance_variables: -> void
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.6.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-07-18 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,6 +23,7 @@ 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
28
  - lib/ulid/ractor_unshareable_constants.rb
28
29
  - lib/ulid/uuid.rb