ruby-ulid 0.0.8 → 0.0.13

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: a1179379859a437f04ff904c941372fffc4aa3027d550ae50c67f744e4899d9a
4
- data.tar.gz: 71781ee8481373bb7229f10f88a1703b2653c11a4f18cbc7b50eb4adbec58908
3
+ metadata.gz: 9e2d0489b211160618fc0ae6ecd30cbbbbaa411683162def97930ca95a860696
4
+ data.tar.gz: 0063c982795d2a42e2f6bac7b236d831753c685b36f4d047e2999dbe7368c6de
5
5
  SHA512:
6
- metadata.gz: 6d48e489221851290a486ffe3a39847196e900bb54751fa7d0c808f7580c8838d1a6382b203da1b90ad4f74dd9e998239e0a6c996b06d99881c39489ff607697
7
- data.tar.gz: fbef689db966901800b24e18820ee6744aeeaeb2476f14349b74d634ed1e5d359a4bff4abb3eb0eee94e4d0183f5db1420719d7e94afd3e803d21d62510087d7
6
+ metadata.gz: 825dab6b7e01f4e1dee5fcd03031ef27645de6a1b56e8392fd784a55c860268af5b84ae4ac3697cdf3eba630dbae197da28cde4b8fbef043c8d0ff48b587f992
7
+ data.tar.gz: f2039e401c77cf15bda81d10587085b7fa167c253a19a66bc0b14ee5fda71162325b7a278f2ebb185d53cbb3fd01563ccb4c6875ffbaebf7fc676a2a24c73767
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Kenichi Kamiya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # ruby-ulid
2
+
3
+ A handy `ULID` library
4
+
5
+ The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
6
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
7
+ Also providing rbs signature files.
8
+
9
+ ---
10
+
11
+ ![ULIDlogo](https://raw.githubusercontent.com/kachick/ruby-ulid/main/logo.png)
12
+
13
+ ![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test.yml/badge.svg?branch=main)
14
+ [![Gem Version](https://badge.fury.io/rb/ruby-ulid.png)](http://badge.fury.io/rb/ruby-ulid)
15
+
16
+ ## Universally Unique Lexicographically Sortable Identifier
17
+
18
+ UUID can be suboptimal for many uses-cases because:
19
+
20
+ - It isn't the most character efficient way of encoding 128 bits of randomness
21
+ - UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address
22
+ - UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures
23
+ - UUID v4 provides no other information than randomness which can cause fragmentation in many data structures
24
+
25
+ Instead, herein is proposed ULID:
26
+
27
+ - 128-bit compatibility with UUID
28
+ - 1.21e+24 unique ULIDs per millisecond
29
+ - Lexicographically sortable!
30
+ - Canonically encoded as a 26 character string, as opposed to the 36 character UUID
31
+ - Uses Crockford's base32 for better efficiency and readability (5 bits per character)
32
+ - Case insensitive
33
+ - No special characters (URL safe)
34
+ - Monotonic sort order (correctly detects and handles the same millisecond)
35
+
36
+ ## Install
37
+
38
+ Require Ruby 2.6 or later
39
+
40
+ ```console
41
+ $ gem install ruby-ulid
42
+ #=> Installed
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ The generated `ULID` is an object not just a string.
48
+ It means easily get the timestamps and binary formats.
49
+
50
+ ```ruby
51
+ require 'ulid'
52
+
53
+ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
54
+ ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
55
+ ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
56
+ ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
57
+ ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
58
+ ```
59
+
60
+ You can get the objects from exists encoded ULIDs
61
+
62
+ ```ruby
63
+ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
64
+ ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
65
+ ```
66
+
67
+ ULIDs are sortable when they are generated in different timestamp with milliseconds precision
68
+
69
+ ```ruby
70
+ ulids = 1000.times.map do
71
+ sleep(0.001)
72
+ ULID.generate
73
+ end
74
+ ulids.sort == ulids #=> true
75
+ ulids.uniq(&:to_time).size #=> 1000
76
+ ```
77
+
78
+ `ULID.generate` can take fixed `Time` instance
79
+
80
+ ```ruby
81
+ time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
82
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
83
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
84
+
85
+ ulids = 1000.times.map do |n|
86
+ ULID.generate(moment: time + n)
87
+ end
88
+ ulids.sort == ulids #=> true
89
+ ```
90
+
91
+ The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
92
+
93
+ ```ruby
94
+ ulids = 10000.times.map do
95
+ ULID.generate
96
+ end
97
+ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment)
98
+ ulids.sort == ulids #=> false
99
+ ```
100
+
101
+ If you want to prefer `sortable` rather than the `randomness`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
102
+ (Though it starts with new random value when changed the timestamp)
103
+
104
+ ```ruby
105
+ monotonic_generator = ULID::MonotonicGenerator.new
106
+ monotonic_ulids = 10000.times.map do
107
+ monotonic_generator.generate
108
+ end
109
+ sample_ulids_by_the_time = monotonic_ulids.uniq(&:to_time)
110
+ sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
111
+
112
+ # In same milliseconds creation, it just increments the end of randomness part
113
+ monotonic_ulids.take(5) #=>
114
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
115
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
116
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
117
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
118
+ # ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
119
+
120
+ # When the milliseconds is updated, it starts with new randomness
121
+ sample_ulids_by_the_time.take(5) #=>
122
+ # [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
123
+ # ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
124
+ # ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K),
125
+ # ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
126
+ # ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
127
+
128
+ monotonic_ulids.sort == monotonic_ulids #=> true
129
+ ```
130
+
131
+ When filtering ULIDs by `Time`, we should consider to handle the precision.
132
+ So this gem provides `ULID.range` to generate `Range[ULID]` from given `Range[Time]`
133
+
134
+ ```ruby
135
+ # Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
136
+ include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
137
+ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
138
+
139
+ # So you can use the generated range objects as below
140
+ ulids.grep(include_end)
141
+ ulids.grep(exclude_end)
142
+ #=> I hope the results should be actually you want!
143
+ ```
144
+
145
+ For rough operations, `ULID.scan` might be useful.
146
+
147
+ ```ruby
148
+ json =<<'EOD'
149
+ {
150
+ "id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3",
151
+ "author": {
152
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
153
+ "name": "kachick"
154
+ },
155
+ "title": "My awesome blog post",
156
+ "comments": [
157
+ {
158
+ "id": "01F4GNCNC3CH0BCRZBPPDEKBKS",
159
+ "commenter": {
160
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
161
+ "name": "kachick"
162
+ }
163
+ },
164
+ {
165
+ "id": "01F4GNCXAMXQ1SGBH5XCR6ZH0M",
166
+ "commenter": {
167
+ "id": "01F4GND4RYYSKNAADHQ9BNXAWJ",
168
+ "name": "pankona"
169
+ }
170
+ }
171
+ ]
172
+ }
173
+ EOD
174
+
175
+ ULID.scan(json).to_a
176
+ #=>
177
+ [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
178
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
179
+ ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
180
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
181
+ ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
182
+ ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
183
+ ```
184
+
185
+ `ULID.min` and `ULID.max` return termination values for ULID spec.
186
+
187
+ ```ruby
188
+ ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
189
+ ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
190
+
191
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
192
+ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
193
+ ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
194
+ ```
195
+
196
+ `ULID#next` and `ULID#succ` returns next(successor) ULID
197
+
198
+ ```ruby
199
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
200
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000"
201
+ ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
202
+ ```
203
+
204
+ `ULID#pred` returns predecessor ULID
205
+
206
+ ```ruby
207
+ ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
208
+ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ"
209
+ ULID.parse('00000000000000000000000000').pred #=> nil
210
+ ```
211
+
212
+ UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
213
+
214
+ ```ruby
215
+ ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
216
+ #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
217
+ ```
218
+
219
+ ## References
220
+
221
+ - [Repository](https://github.com/kachick/ruby-ulid)
222
+ - [API documents](https://kachick.github.io/ruby-ulid/)
223
+ - [ulid/spec](https://github.com/ulid/spec)
224
+ - [Another choices are UUIDv6, UUIDv7, UUIDv8. But they are still in draft state](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
225
+ - Current parser/validator/matcher implementation aims `strict`, It might be changed in [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57).
data/Steepfile ADDED
@@ -0,0 +1,7 @@
1
+ target :lib do
2
+ signature 'sig'
3
+
4
+ check 'lib'
5
+
6
+ library 'securerandom'
7
+ end
data/lib/ulid.rb CHANGED
@@ -4,7 +4,6 @@
4
4
 
5
5
  require 'securerandom'
6
6
  require 'integer/base'
7
- require_relative 'ulid/version'
8
7
 
9
8
  # @see https://github.com/ulid/spec
10
9
  # @!attribute [r] milliseconds
@@ -18,72 +17,47 @@ class ULID
18
17
  class OverflowError < Error; end
19
18
  class ParserError < Error; end
20
19
 
20
+ encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
21
21
  # Crockford's Base32. Excluded I, L, O, U.
22
22
  # @see https://www.crockford.com/base32.html
23
- ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.chars.map(&:freeze).freeze
23
+ ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
24
24
 
25
- TIME_PART_LENGTH = 10
25
+ TIMESTAMP_PART_LENGTH = 10
26
26
  RANDOMNESS_PART_LENGTH = 16
27
- ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
- TIME_OCTETS_LENGTH = 6
27
+ ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
+ TIMESTAMP_OCTETS_LENGTH = 6
29
29
  RANDOMNESS_OCTETS_LENGTH = 10
30
- OCTETS_LENGTH = TIME_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
30
+ OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
31
31
  MAX_MILLISECONDS = 281474976710655
32
32
  MAX_ENTROPY = 1208925819614629174706175
33
+ MAX_INTEGER = 340282366920938463463374607431768211455
34
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
35
+ STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
+
37
+ # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
38
+ 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
33
39
 
34
40
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
35
41
  # @see https://bugs.ruby-lang.org/issues/15958
36
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
37
43
 
38
- class MonotonicGenerator
39
- attr_accessor :latest_milliseconds, :latest_entropy
40
-
41
- def initialize
42
- reset
43
- end
44
-
45
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
46
- # @return [ULID]
47
- def generate
48
- milliseconds = ULID.current_milliseconds
49
- reasonable_entropy = ULID.reasonable_entropy
50
-
51
- @latest_milliseconds ||= milliseconds
52
- @latest_entropy ||= reasonable_entropy
53
- if @latest_milliseconds != milliseconds
54
- @latest_milliseconds = milliseconds
55
- @latest_entropy = reasonable_entropy
56
- else
57
- @latest_entropy += 1
58
- end
59
-
60
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
61
- end
62
-
63
- # @return [self]
64
- def reset
65
- @latest_milliseconds = nil
66
- @latest_entropy = nil
67
- self
68
- end
69
-
70
- # @raise [TypeError] always raises exception and does not freeze self
71
- # @return [void]
72
- def freeze
73
- raise TypeError, "cannot freeze #{self.class}"
74
- end
44
+ # @param [Integer, Time] moment
45
+ # @param [Integer] entropy
46
+ # @return [ULID]
47
+ def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
48
+ new milliseconds: milliseconds_from_moment(moment), entropy: entropy
75
49
  end
76
50
 
77
- MONOTONIC_GENERATOR = MonotonicGenerator.new
78
-
79
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT
51
+ # @param [Integer, Time] moment
52
+ # @return [ULID]
53
+ def self.min(moment: 0)
54
+ generate(moment: moment, entropy: 0)
55
+ end
80
56
 
81
57
  # @param [Integer, Time] moment
82
- # @param [Integer] entropy
83
58
  # @return [ULID]
84
- def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
85
- milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
86
- new milliseconds: milliseconds, entropy: entropy
59
+ def self.max(moment: MAX_MILLISECONDS)
60
+ generate(moment: moment, entropy: MAX_ENTROPY)
87
61
  end
88
62
 
89
63
  # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
@@ -100,17 +74,113 @@ class ULID
100
74
  MONOTONIC_GENERATOR.generate
101
75
  end
102
76
 
77
+ # @param [String, #to_str] string
78
+ # @return [Enumerator]
79
+ # @yieldparam [ULID] ulid
80
+ # @yieldreturn [self]
81
+ def self.scan(string)
82
+ string = string.to_str
83
+ return to_enum(__callee__, string) unless block_given?
84
+ string.scan(PATTERN) do |pair|
85
+ yield parse(pair.join)
86
+ end
87
+ self
88
+ end
89
+
90
+ # @param [String, #to_str] uuid
91
+ # @return [ULID]
92
+ # @raise [ParserError] if the given format is not correct for UUIDv4 specs
93
+ def self.from_uuidv4(uuid)
94
+ begin
95
+ uuid = uuid.to_str
96
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
97
+ raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
98
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
99
+ from_integer(normalized.to_i(16))
100
+ rescue => err
101
+ raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
102
+ end
103
+ end
104
+
105
+ # @param [Integer, #to_int] integer
106
+ # @return [ULID]
107
+ # @raise [OverflowError] if the given integer is larger than the ULID limit
108
+ # @raise [ArgumentError] if the given integer is negative number
109
+ # @todo Need optimized for performance
110
+ def self.from_integer(integer)
111
+ integer = integer.to_int
112
+ raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
113
+ raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
114
+
115
+ octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
116
+ time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
117
+ randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
118
+ milliseconds = inverse_of_digits(time_octets)
119
+ entropy = inverse_of_digits(randomness_octets)
120
+
121
+ new milliseconds: milliseconds, entropy: entropy
122
+ end
123
+
124
+ # @param [Range<Time>] time_range
125
+ # @return [Range<ULID>]
126
+ def self.range(time_range)
127
+ raise ArgumentError, 'ULID.range takes only Range[Time]' unless time_range.kind_of?(Range)
128
+ begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
129
+
130
+ case begin_time
131
+ when Time
132
+ begin_ulid = min(moment: begin_time)
133
+ when nil
134
+ begin_ulid = min
135
+ else
136
+ raise ArgumentError, 'ULID.range takes only Range[Time]'
137
+ end
138
+
139
+ case end_time
140
+ when Time
141
+ if exclude_end
142
+ end_ulid = min(moment: end_time)
143
+ else
144
+ end_ulid = max(moment: end_time)
145
+ end
146
+ when nil
147
+ # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
148
+ end_ulid = max
149
+ exclude_end = false
150
+ else
151
+ raise ArgumentError, 'ULID.range takes only Range[Time]'
152
+ end
153
+
154
+ Range.new(begin_ulid, end_ulid, exclude_end)
155
+ end
156
+
157
+ # @param [Time] time
158
+ # @return [Time]
159
+ def self.floor(time)
160
+ if RUBY_VERSION >= '2.7'
161
+ time.floor(3)
162
+ else
163
+ Time.at(0, milliseconds_from_time(time), :millisecond)
164
+ end
165
+ end
166
+
103
167
  # @return [Integer]
104
168
  def self.current_milliseconds
105
- time_to_milliseconds(Time.now)
169
+ milliseconds_from_time(Time.now)
106
170
  end
107
171
 
108
172
  # @param [Time] time
109
173
  # @return [Integer]
110
- def self.time_to_milliseconds(time)
174
+ def self.milliseconds_from_time(time)
111
175
  (time.to_r * 1000).to_i
112
176
  end
113
177
 
178
+ # @param [Time, Integer] moment
179
+ # @return [Integer]
180
+ def self.milliseconds_from_moment(moment)
181
+ moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
182
+ end
183
+
114
184
  # @return [Integer]
115
185
  def self.reasonable_entropy
116
186
  SecureRandom.random_number(MAX_ENTROPY)
@@ -126,8 +196,8 @@ class ULID
126
196
  unless string.size == ENCODED_ID_LENGTH
127
197
  raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
128
198
  end
129
- timestamp = string.slice(0, TIME_PART_LENGTH)
130
- randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
199
+ timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
200
+ randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
131
201
  milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
132
202
  entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
133
203
  rescue => err
@@ -146,8 +216,34 @@ class ULID
146
216
  true
147
217
  end
148
218
 
219
+ # @api private
220
+ # @param [Integer] integer
221
+ # @param [Integer] length
222
+ # @return [Array<Integer>]
223
+ def self.octets_from_integer(integer, length:)
224
+ digits = integer.digits(256)
225
+ (length - digits.size).times do
226
+ digits.push 0
227
+ end
228
+ digits.reverse!
229
+ end
230
+
231
+ # @api private
232
+ # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
233
+ # @param [Array<Integer>] reversed_digits
234
+ # @return [Integer]
235
+ def self.inverse_of_digits(reversed_digits)
236
+ base = 256
237
+ num = 0
238
+ reversed_digits.each do |digit|
239
+ num = (num * base) + digit
240
+ end
241
+ num
242
+ end
243
+
149
244
  attr_reader :milliseconds, :entropy
150
245
 
246
+ # @api private
151
247
  # @param [Integer] milliseconds
152
248
  # @param [Integer] entropy
153
249
  # @return [void]
@@ -172,7 +268,7 @@ class ULID
172
268
 
173
269
  # @return [Integer]
174
270
  def to_i
175
- @integer ||= inverse_of_digits(octets)
271
+ @integer ||= self.class.inverse_of_digits(octets)
176
272
  end
177
273
  alias_method :hash, :to_i
178
274
 
@@ -210,63 +306,90 @@ class ULID
210
306
 
211
307
  # @return [Time]
212
308
  def to_time
213
- @time ||= Time.at(0, @milliseconds, :millisecond).utc
309
+ @time ||= begin
310
+ if RUBY_VERSION >= '2.7'
311
+ Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
312
+ else
313
+ Time.at(0, @milliseconds, :millisecond).utc.freeze
314
+ end
315
+ end
214
316
  end
215
317
 
216
318
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
217
319
  def octets
218
- @octets ||= (time_octets + randomness_octets).freeze
320
+ @octets ||= (timestamp_octets + randomness_octets).freeze
219
321
  end
220
322
 
221
323
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
222
- def time_octets
223
- @time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
324
+ def timestamp_octets
325
+ @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
224
326
  end
225
327
 
226
328
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
227
329
  def randomness_octets
228
- @randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
330
+ @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
229
331
  end
230
332
 
231
- # @raise [OverflowError] if the next entropy part is larger than the ULID limit
232
- # @return [ULID]
333
+ # @return [String]
334
+ def timestamp
335
+ @timestamp ||= matchdata[:timestamp].freeze
336
+ end
337
+
338
+ # @return [String]
339
+ def randomness
340
+ @randomness ||= matchdata[:randomness].freeze
341
+ end
342
+
343
+ # @return [Regexp]
344
+ def pattern
345
+ @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
346
+ end
347
+
348
+ # @return [Regexp]
349
+ def strict_pattern
350
+ @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
351
+ end
352
+
353
+ # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
233
354
  def next
234
- @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
355
+ next_int = to_i.next
356
+ return nil if next_int > MAX_INTEGER
357
+ @next ||= self.class.from_integer(next_int)
235
358
  end
236
359
  alias_method :succ, :next
237
360
 
361
+ # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
362
+ def pred
363
+ pre_int = to_i.pred
364
+ return nil if pre_int.negative?
365
+ @pred ||= self.class.from_integer(pre_int)
366
+ end
367
+
238
368
  # @return [self]
239
369
  def freeze
240
370
  # Evaluate all caching
241
371
  inspect
242
372
  octets
243
- succ
244
373
  to_i
374
+ succ
375
+ pred
376
+ strict_pattern
245
377
  super
246
378
  end
247
379
 
248
380
  private
249
381
 
250
- # @param [Integer] integer
251
- # @param [Integer] length
252
- # @return [Array<Integer>]
253
- def octets_from_integer(integer, length:)
254
- digits = integer.digits(256)
255
- (length - digits.size).times do
256
- digits.push 0
257
- end
258
- digits.reverse!
382
+ # @return [MatchData]
383
+ def matchdata
384
+ @matchdata ||= STRICT_PATTERN.match(to_str).freeze
259
385
  end
386
+ end
260
387
 
261
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
262
- # @param [Array<Integer>] reversed_digits
263
- # @return [Integer]
264
- def inverse_of_digits(reversed_digits)
265
- base = 256
266
- num = 0
267
- reversed_digits.each do |digit|
268
- num = (num * base) + digit
269
- end
270
- num
271
- end
388
+ require_relative 'ulid/version'
389
+ require_relative 'ulid/monotonic_generator'
390
+
391
+ class ULID
392
+ MONOTONIC_GENERATOR = MonotonicGenerator.new
393
+
394
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
272
395
  end
@@ -0,0 +1,46 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ class ULID
6
+ class MonotonicGenerator
7
+ # @api private
8
+ attr_accessor :latest_milliseconds, :latest_entropy
9
+
10
+ def initialize
11
+ reset
12
+ end
13
+
14
+ # @param [Time, Integer] moment
15
+ # @return [ULID]
16
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
17
+ # @raise [ArgumentError] if the given moment(milliseconds) is negative number
18
+ def generate(moment: ULID.current_milliseconds)
19
+ milliseconds = ULID.milliseconds_from_moment(moment)
20
+ raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
21
+
22
+ if @latest_milliseconds < milliseconds
23
+ @latest_milliseconds = milliseconds
24
+ @latest_entropy = ULID.reasonable_entropy
25
+ else
26
+ @latest_entropy += 1
27
+ end
28
+
29
+ ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
30
+ end
31
+
32
+ # @api private
33
+ # @return [void]
34
+ def reset
35
+ @latest_milliseconds = 0
36
+ @latest_entropy = ULID.reasonable_entropy
37
+ nil
38
+ end
39
+
40
+ # @raise [TypeError] always raises exception and does not freeze self
41
+ # @return [void]
42
+ def freeze
43
+ raise TypeError, "cannot freeze #{self.class}"
44
+ end
45
+ end
46
+ end
data/lib/ulid/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  class ULID
5
- VERSION = '0.0.8'
5
+ VERSION = '0.0.13'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -2,18 +2,24 @@
2
2
  class ULID
3
3
  VERSION: String
4
4
  ENCODING_CHARS: Array[String]
5
- TIME_PART_LENGTH: 10
5
+ TIMESTAMP_PART_LENGTH: 10
6
6
  RANDOMNESS_PART_LENGTH: 16
7
7
  ENCODED_ID_LENGTH: 26
8
- TIME_OCTETS_LENGTH: 6
8
+ TIMESTAMP_OCTETS_LENGTH: 6
9
9
  RANDOMNESS_OCTETS_LENGTH: 10
10
10
  OCTETS_LENGTH: 16
11
11
  MAX_MILLISECONDS: 281474976710655
12
12
  MAX_ENTROPY: 1208925819614629174706175
13
+ MAX_INTEGER: 340282366920938463463374607431768211455
13
14
  TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
15
+ PATTERN: Regexp
16
+ STRICT_PATTERN: Regexp
17
+ UUIDV4_PATTERN: Regexp
14
18
  MONOTONIC_GENERATOR: MonotonicGenerator
15
19
  include Comparable
16
20
 
21
+ type moment = Time | Integer
22
+
17
23
  class Error < StandardError
18
24
  end
19
25
 
@@ -24,34 +30,52 @@ class ULID
24
30
  end
25
31
 
26
32
  class MonotonicGenerator
27
- attr_accessor latest_milliseconds: Integer?
28
- attr_accessor latest_entropy: Integer?
33
+ attr_accessor latest_milliseconds: Integer
34
+ attr_accessor latest_entropy: Integer
29
35
  def initialize: -> void
30
- def generate: -> ULID
36
+ def generate: (?moment: moment) -> ULID
31
37
  def reset: -> void
32
38
  def freeze: -> void
33
39
  end
34
40
 
35
41
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
36
- type time_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
42
+ type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
37
43
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
38
44
 
39
- @string: String
40
- @integer: Integer
41
- @octets: octets
42
- @time_octets: time_octets
43
- @randomness_octets: randomness_octets
44
- @inspect: String
45
- @time: Time
46
- @next: ULID
45
+ @milliseconds: Integer
46
+ @entropy: Integer
47
+ @string: String?
48
+ @integer: Integer?
49
+ @octets: octets?
50
+ @timestamp_octets: timestamp_octets?
51
+ @randomness_octets: randomness_octets?
52
+ @timestamp: String?
53
+ @randomness: String?
54
+ @inspect: String?
55
+ @time: Time?
56
+ @next: ULID?
57
+ @pattern: Regexp?
58
+ @strict_pattern: Regexp?
59
+ @matchdata: MatchData?
47
60
 
48
- def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
61
+ def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
49
62
  def self.monotonic_generate: -> ULID
50
63
  def self.current_milliseconds: -> Integer
51
- def self.time_to_milliseconds: (Time time) -> Integer
64
+ def self.milliseconds_from_time: (Time time) -> Integer
65
+ def self.milliseconds_from_moment: (moment moment) -> Integer
66
+ def self.range: (Range[Time] time_range) -> Range[ULID]
67
+ def self.floor: (Time time) -> Time
52
68
  def self.reasonable_entropy: -> Integer
53
69
  def self.parse: (String string) -> ULID
70
+ def self.from_uuidv4: (String uuid) -> ULID
71
+ def self.from_integer: (Integer integer) -> ULID
72
+ def self.min: (?moment: moment) -> ULID
73
+ def self.max: (?moment: moment) -> ULID
54
74
  def self.valid?: (untyped string) -> bool
75
+ def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
76
+ | (String string) { (ULID ulid) -> void } -> singleton(ULID)
77
+ def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
78
+ def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
55
79
  attr_reader milliseconds: Integer
56
80
  attr_reader entropy: Integer
57
81
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
@@ -66,14 +90,18 @@ class ULID
66
90
  alias == eql?
67
91
  def ===: (untyped other) -> bool
68
92
  def to_time: -> Time
93
+ def timestamp: -> String
94
+ def randomness: -> String
95
+ def pattern: -> Regexp
96
+ def strict_pattern: -> Regexp
69
97
  def octets: -> octets
70
- def time_octets: -> time_octets
98
+ def timestamp_octets: -> timestamp_octets
71
99
  def randomness_octets: -> randomness_octets
72
- def next: -> ULID
100
+ def next: -> ULID?
73
101
  alias succ next
102
+ def pred: -> ULID?
74
103
  def freeze: -> self
75
104
 
76
105
  private
77
- def octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
78
- def inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
106
+ def matchdata: -> MatchData
79
107
  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.0.8
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Kamiya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-29 00:00:00.000000000 Z
11
+ date: 2021-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.2.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rbs
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.2.0
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.2.0
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: benchmark-ips
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -70,18 +84,21 @@ dependencies:
70
84
  - - "<"
71
85
  - !ruby/object:Gem::Version
72
86
  version: '2'
73
- description: |2
74
- ULID(Universally Unique Lexicographically Sortable Identifier) is defined on https://github.com/ulid/spec.
75
- It has useful specs for actual applications.
76
- This gem aims to provide the generator, monotonic generator, parser and handy manipulation methods for the ID.
77
- Also having rbs signature files.
87
+ description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
88
+ useful specs for applications (e.g. `Database key`). \n This gem aims to provide
89
+ the generator, monotonic generator, parser and handy manipulation features around
90
+ the ULID.\n Also providing `rbs` signature files.\n"
78
91
  email:
79
92
  - kachick1+ruby@gmail.com
80
93
  executables: []
81
94
  extensions: []
82
95
  extra_rdoc_files: []
83
96
  files:
97
+ - LICENSE
98
+ - README.md
99
+ - Steepfile
84
100
  - lib/ulid.rb
101
+ - lib/ulid/monotonic_generator.rb
85
102
  - lib/ulid/version.rb
86
103
  - sig/ulid.rbs
87
104
  homepage: https://github.com/kachick/ruby-ulid
@@ -99,14 +116,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
116
  requirements:
100
117
  - - ">="
101
118
  - !ruby/object:Gem::Version
102
- version: '2.5'
119
+ version: 2.6.0
103
120
  required_rubygems_version: !ruby/object:Gem::Requirement
104
121
  requirements:
105
122
  - - ">="
106
123
  - !ruby/object:Gem::Version
107
124
  version: '0'
108
125
  requirements: []
109
- rubygems_version: 3.2.15
126
+ rubygems_version: 3.1.4
110
127
  signing_key:
111
128
  specification_version: 4
112
129
  summary: A handy ULID library