ruby-ulid 0.5.0 → 0.7.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: 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