ruby-ulid 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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