ruby-ulid 0.0.8 → 0.0.13

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: 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