ruby-ulid 0.0.10 → 0.0.15

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: ce2bb6c517e9fee4f758577e57d2c5eddfd16db9ee3c0d17ad41db8d9f1de383
4
- data.tar.gz: 1cfb027a652e2035445483bdff2511c2837441e2e664286d60682fec6769cd17
3
+ metadata.gz: 6a620e80f82e91c778329ccf7da8cca96bda4b12e047610b9543768c5ec85d22
4
+ data.tar.gz: ea28469d63348758f52e1460b5330ce177ad2281e63060800a0669054758f3ee
5
5
  SHA512:
6
- metadata.gz: ea00703256bc218a1e022dd732da22b37b9caeec30bdbdb4df945f9f185d408f0b746c36b36377dba2d4acaae5d2cf0b4db8b574ecc9da0e6e4b9b0af5c2404f
7
- data.tar.gz: 6cd088ae1d539d8cb1d2690eb4248be413e527a87db7eb67b3c1adfc167c285cf2b90bb5701e82208586996649d56d1ef6c7c30396d1fbf0fc68220487711a54
6
+ metadata.gz: acb07ae797c3a06a6626d34e15dc1056a30586e8bc151a3773770259d2f4047d0708593e702fbd67a7609758165f20a15f208fede068717a5555733535ed4bde
7
+ data.tar.gz: 57c9819d86f2a0f002cf3b9a7b76b3a36bc412fa874d50136ec296f1217fb905fa19feb52de2c9d5250547da384580cf882121f0e331054cd665bd113d1f10e3
data/README.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  A handy `ULID` library
4
4
 
5
- The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
6
- Formal name is `Universally Unique Lexicographically Sortable Identifier`.
7
- It has useful specs for actual applications.
8
- This gem aims to provide the generator, monotonic generator, parser and handy manipulation methods for the ID.
9
- Also having rbs signature files.
5
+ The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
6
+ This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
7
+ Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
10
8
 
11
9
  ---
12
10
 
@@ -30,13 +28,15 @@ Instead, herein is proposed ULID:
30
28
  - 1.21e+24 unique ULIDs per millisecond
31
29
  - Lexicographically sortable!
32
30
  - Canonically encoded as a 26 character string, as opposed to the 36 character UUID
33
- - Uses Crockford's base32 for better efficiency and readability (5 bits per character)
31
+ - Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) # See also exists issues in [Note](#note)
34
32
  - Case insensitive
35
33
  - No special characters (URL safe)
36
34
  - Monotonic sort order (correctly detects and handles the same millisecond)
37
35
 
38
36
  ## Install
39
37
 
38
+ Require Ruby 2.6 or later
39
+
40
40
  ```console
41
41
  $ gem install ruby-ulid
42
42
  #=> Installed
@@ -57,13 +57,14 @@ ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 7
57
57
  ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
58
58
  ```
59
59
 
60
- You can parse from exists IDs
60
+ You can get the objects from exists encoded ULIDs
61
61
 
62
62
  ```ruby
63
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
64
65
  ```
65
66
 
66
- ULID is sortable when they are generated different timestamp in milliseconds precision
67
+ ULIDs are sortable when they are generated in different timestamp with milliseconds precision
67
68
 
68
69
  ```ruby
69
70
  ulids = 1000.times.map do
@@ -74,29 +75,71 @@ ulids.sort == ulids #=> true
74
75
  ulids.uniq(&:to_time).size #=> 1000
75
76
  ```
76
77
 
77
- Providing monotonic generator for same milliseconds use-cases. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
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.
78
92
 
79
93
  ```ruby
80
94
  ulids = 10000.times.map do
81
95
  ULID.generate
82
96
  end
83
- ulids.uniq(&:to_time).size #=> 35 (the number will be changed by every creation)
97
+ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment)
84
98
  ulids.sort == ulids #=> false
99
+ ```
85
100
 
101
+ If you want to ensure `sortable`, 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)
86
103
 
104
+ ```ruby
87
105
  monotonic_generator = ULID::MonotonicGenerator.new
88
- monotonic_ulids = 10000.times.map do
106
+ ulids = 10000.times.map do
89
107
  monotonic_generator.generate
90
108
  end
91
- monotonic_ulids.uniq(&:to_time).size #=> 34 (the number will be changed by every creation)
92
- monotonic_ulids.sort == monotonic_ulids #=> true
109
+ sample_ulids_by_the_time = 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
+ 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
+ ulids.sort == ulids #=> true
93
129
  ```
94
130
 
95
- Providing converter for UUIDv4. (Of course the timestamp will be useless one.)
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]`
96
133
 
97
134
  ```ruby
98
- ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
99
- #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
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!
100
143
  ```
101
144
 
102
145
  For rough operations, `ULID.scan` might be useful.
@@ -150,50 +193,59 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
150
193
  ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
151
194
  ```
152
195
 
153
- ## Development
154
-
155
- At first, you should install development dependencies
196
+ `ULID#next` and `ULID#succ` returns next(successor) ULID
156
197
 
157
- ```console
158
- $ git clone git@github.com:kachick/ruby-ulid.git
159
- $ cd ./ruby-ulid
160
- $ bundle install
161
- # Executing first time might take longtime, because development mode dependent active_support via steep
198
+ ```ruby
199
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
200
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000"
201
+ ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
162
202
  ```
163
203
 
164
- Play with the behaviors in REPL.
204
+ `ULID#pred` returns predecessor ULID
165
205
 
166
- ```console
167
- $ ./bin/console
168
- # Starting up IRB with loading developing ULID library
169
- irb(main):001:0>
206
+ ```ruby
207
+ ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
208
+ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ"
209
+ ULID.parse('00000000000000000000000000').pred #=> nil
170
210
  ```
171
211
 
212
+ ### UUIDv4 converter for migration use-cases
213
+
214
+ `ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
215
+ The imported timestamp is meaningless. So ULID's benefit will lost
216
+
172
217
  ```ruby
173
- # On IRB, you can check behaviors even if it is undocumented
174
- ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
175
- ls ULID
176
- ```
218
+ # Basically reversible
219
+ ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
220
+ ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
177
221
 
178
- If you try to add/change/fix features, please update tests and ensure they are not broken.
222
+ uuid_v4s = 10000.times.map { SecureRandom.uuid }
223
+ uuid_v4s.uniq.size == 10000 #=> Probably `true`
179
224
 
180
- ```console
181
- $ bundle exec rake test
182
- $ echo $?
183
- 0
184
- ```
225
+ ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
226
+ ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
185
227
 
186
- If you try to improve any performance issue, please add benchmarking and check the result of before and after.
228
+ # NOTE: Some boundary values are not reversible. See below.
187
229
 
188
- ```console
189
- $ bundle exec ruby benchmark/the_added_file.rb
190
- # Showing the results
230
+ ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
231
+ ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
232
+
233
+ # These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
234
+ reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
235
+ reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
236
+
237
+ # But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
238
+ ULID.min == reversed_min #=> false
239
+ ULID.max == reversed_max #=> false
191
240
  ```
192
241
 
193
- ## Documents
242
+ ## References
194
243
 
195
- - [API documents generated by YARD](https://kachick.github.io/ruby-ulid/)
244
+ - [Repository](https://github.com/kachick/ruby-ulid)
245
+ - [API documents](https://kachick.github.io/ruby-ulid/)
246
+ - [ulid/spec](https://github.com/ulid/spec)
196
247
 
197
- ## Author
248
+ ## Note
198
249
 
199
- Kenichi Kamiya - [@kachick](https://github.com/kachick)
250
+ - Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (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)
251
+ - Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
data/lib/ulid.rb CHANGED
@@ -35,7 +35,7 @@ class ULID
35
35
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
36
36
 
37
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
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.freeze
39
39
 
40
40
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
41
41
  # @see https://bugs.ruby-lang.org/issues/15958
@@ -45,20 +45,19 @@ class ULID
45
45
  # @param [Integer] entropy
46
46
  # @return [ULID]
47
47
  def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
48
- milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
49
- new milliseconds: milliseconds, entropy: entropy
48
+ new milliseconds: milliseconds_from_moment(moment), entropy: entropy
50
49
  end
51
50
 
52
51
  # @param [Integer, Time] moment
53
52
  # @return [ULID]
54
53
  def self.min(moment: 0)
55
- generate(moment: moment, entropy: 0)
54
+ 0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
56
55
  end
57
56
 
58
57
  # @param [Integer, Time] moment
59
58
  # @return [ULID]
60
59
  def self.max(moment: MAX_MILLISECONDS)
61
- generate(moment: moment, entropy: MAX_ENTROPY)
60
+ MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
62
61
  end
63
62
 
64
63
  # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
@@ -107,6 +106,7 @@ class ULID
107
106
  # @return [ULID]
108
107
  # @raise [OverflowError] if the given integer is larger than the ULID limit
109
108
  # @raise [ArgumentError] if the given integer is negative number
109
+ # @todo Need optimized for performance
110
110
  def self.from_integer(integer)
111
111
  integer = integer.to_int
112
112
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
@@ -121,17 +121,70 @@ class ULID
121
121
  new milliseconds: milliseconds, entropy: entropy
122
122
  end
123
123
 
124
+ # @param [Range<Time>, Range<nil>] time_range
125
+ # @return [Range<ULID>]
126
+ # @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
127
+ def self.range(time_range)
128
+ raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
129
+ begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
130
+
131
+ case begin_time
132
+ when Time
133
+ begin_ulid = min(moment: begin_time)
134
+ when nil
135
+ begin_ulid = min
136
+ else
137
+ raise argument_error_for_range_building(time_range)
138
+ end
139
+
140
+ case end_time
141
+ when Time
142
+ if exclude_end
143
+ end_ulid = min(moment: end_time)
144
+ else
145
+ end_ulid = max(moment: end_time)
146
+ end
147
+ when nil
148
+ # The end should be max and include end, because nil end means to cover endless ULIDs until the limit
149
+ end_ulid = max
150
+ exclude_end = false
151
+ else
152
+ raise argument_error_for_range_building(time_range)
153
+ end
154
+
155
+ begin_ulid.freeze
156
+ end_ulid.freeze
157
+
158
+ Range.new(begin_ulid, end_ulid, exclude_end)
159
+ end
160
+
161
+ # @param [Time] time
162
+ # @return [Time]
163
+ def self.floor(time)
164
+ if RUBY_VERSION >= '2.7'
165
+ time.floor(3)
166
+ else
167
+ Time.at(0, milliseconds_from_time(time), :millisecond)
168
+ end
169
+ end
170
+
124
171
  # @return [Integer]
125
172
  def self.current_milliseconds
126
- time_to_milliseconds(Time.now)
173
+ milliseconds_from_time(Time.now)
127
174
  end
128
175
 
129
176
  # @param [Time] time
130
177
  # @return [Integer]
131
- def self.time_to_milliseconds(time)
178
+ def self.milliseconds_from_time(time)
132
179
  (time.to_r * 1000).to_i
133
180
  end
134
181
 
182
+ # @param [Time, Integer] moment
183
+ # @return [Integer]
184
+ def self.milliseconds_from_moment(moment)
185
+ moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
186
+ end
187
+
135
188
  # @return [Integer]
136
189
  def self.reasonable_entropy
137
190
  SecureRandom.random_number(MAX_ENTROPY)
@@ -167,6 +220,7 @@ class ULID
167
220
  true
168
221
  end
169
222
 
223
+ # @api private
170
224
  # @param [Integer] integer
171
225
  # @param [Integer] length
172
226
  # @return [Array<Integer>]
@@ -178,6 +232,7 @@ class ULID
178
232
  digits.reverse!
179
233
  end
180
234
 
235
+ # @api private
181
236
  # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
182
237
  # @param [Array<Integer>] reversed_digits
183
238
  # @return [Integer]
@@ -190,8 +245,14 @@ class ULID
190
245
  num
191
246
  end
192
247
 
248
+ # @return [ArgumentError]
249
+ private_class_method def self.argument_error_for_range_building(argument)
250
+ ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
251
+ end
252
+
193
253
  attr_reader :milliseconds, :entropy
194
254
 
255
+ # @api private
195
256
  # @param [Integer] milliseconds
196
257
  # @param [Integer] entropy
197
258
  # @return [void]
@@ -209,10 +270,9 @@ class ULID
209
270
  end
210
271
 
211
272
  # @return [String]
212
- def to_str
273
+ def to_s
213
274
  @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
214
275
  end
215
- alias_method :to_s, :to_str
216
276
 
217
277
  # @return [Integer]
218
278
  def to_i
@@ -227,7 +287,7 @@ class ULID
227
287
 
228
288
  # @return [String]
229
289
  def inspect
230
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
290
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
231
291
  end
232
292
 
233
293
  # @return [Boolean]
@@ -254,7 +314,13 @@ class ULID
254
314
 
255
315
  # @return [Time]
256
316
  def to_time
257
- @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
317
+ @time ||= begin
318
+ if RUBY_VERSION >= '2.7'
319
+ Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
320
+ else
321
+ Time.at(0, @milliseconds, :millisecond).utc.freeze
322
+ end
323
+ end
258
324
  end
259
325
 
260
326
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
@@ -292,21 +358,36 @@ class ULID
292
358
  @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
293
359
  end
294
360
 
295
- # @raise [OverflowError] if the next entropy part is larger than the ULID limit
296
- # @return [ULID]
361
+ # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
297
362
  def next
298
- @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
363
+ next_int = to_i.next
364
+ return nil if next_int > MAX_INTEGER
365
+ @next ||= self.class.from_integer(next_int)
299
366
  end
300
367
  alias_method :succ, :next
301
368
 
369
+ # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
370
+ def pred
371
+ pre_int = to_i.pred
372
+ return nil if pre_int.negative?
373
+ @pred ||= self.class.from_integer(pre_int)
374
+ end
375
+
376
+ # @return [String]
377
+ def to_uuidv4
378
+ @uuidv4 ||= begin
379
+ # This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
380
+ array = octets.pack('C*').unpack('NnnnnN')
381
+ array[2] = (array[2] & 0x0fff) | 0x4000
382
+ array[3] = (array[3] & 0x3fff) | 0x8000
383
+ ('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
384
+ end
385
+ end
386
+
302
387
  # @return [self]
303
388
  def freeze
304
- # Evaluate all caching
305
- inspect
306
- octets
307
- succ
308
- to_i
309
- strict_pattern
389
+ # Need to cache before freezing, because frozen objects can't assign instance variables
390
+ cache_all_instance_variables
310
391
  super
311
392
  end
312
393
 
@@ -314,7 +395,18 @@ class ULID
314
395
 
315
396
  # @return [MatchData]
316
397
  def matchdata
317
- @matchdata ||= STRICT_PATTERN.match(to_str).freeze
398
+ @matchdata ||= STRICT_PATTERN.match(to_s).freeze
399
+ end
400
+
401
+ # @return [void]
402
+ def cache_all_instance_variables
403
+ inspect
404
+ octets
405
+ to_i
406
+ succ
407
+ pred
408
+ strict_pattern
409
+ to_uuidv4
318
410
  end
319
411
  end
320
412
 
@@ -322,7 +414,9 @@ require_relative 'ulid/version'
322
414
  require_relative 'ulid/monotonic_generator'
323
415
 
324
416
  class ULID
417
+ MIN = parse('00000000000000000000000000').freeze
418
+ MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
325
419
  MONOTONIC_GENERATOR = MonotonicGenerator.new
326
420
 
327
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
421
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX
328
422
  end
@@ -4,35 +4,37 @@
4
4
 
5
5
  class ULID
6
6
  class MonotonicGenerator
7
+ # @api private
7
8
  attr_accessor :latest_milliseconds, :latest_entropy
8
9
 
9
10
  def initialize
10
11
  reset
11
12
  end
12
13
 
13
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
14
+ # @param [Time, Integer] moment
14
15
  # @return [ULID]
15
- def generate
16
- milliseconds = ULID.current_milliseconds
17
- reasonable_entropy = ULID.reasonable_entropy
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?
18
21
 
19
- @latest_milliseconds ||= milliseconds
20
- @latest_entropy ||= reasonable_entropy
21
- if @latest_milliseconds != milliseconds
22
+ if @latest_milliseconds < milliseconds
22
23
  @latest_milliseconds = milliseconds
23
- @latest_entropy = reasonable_entropy
24
+ @latest_entropy = ULID.reasonable_entropy
24
25
  else
25
26
  @latest_entropy += 1
26
27
  end
27
28
 
28
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
29
+ ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
29
30
  end
30
31
 
31
- # @return [self]
32
+ # @api private
33
+ # @return [void]
32
34
  def reset
33
- @latest_milliseconds = nil
34
- @latest_entropy = nil
35
- self
35
+ @latest_milliseconds = 0
36
+ @latest_entropy = ULID.reasonable_entropy
37
+ nil
36
38
  end
37
39
 
38
40
  # @raise [TypeError] always raises exception and does not freeze self
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.10'
5
+ VERSION = '0.0.15'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -16,8 +16,13 @@ class ULID
16
16
  STRICT_PATTERN: Regexp
17
17
  UUIDV4_PATTERN: Regexp
18
18
  MONOTONIC_GENERATOR: MonotonicGenerator
19
+ MIN: ULID
20
+ MAX: ULID
19
21
  include Comparable
20
22
 
23
+ # The `moment` is a `Time` or `Intger of the milliseconds`
24
+ type moment = Time | Integer
25
+
21
26
  class Error < StandardError
22
27
  end
23
28
 
@@ -28,15 +33,14 @@ class ULID
28
33
  end
29
34
 
30
35
  class MonotonicGenerator
31
- attr_accessor latest_milliseconds: Integer?
32
- attr_accessor latest_entropy: Integer?
36
+ attr_accessor latest_milliseconds: Integer
37
+ attr_accessor latest_entropy: Integer
33
38
  def initialize: -> void
34
- def generate: -> ULID
39
+ def generate: (?moment: moment) -> ULID
35
40
  def reset: -> void
36
41
  def freeze: -> void
37
42
  end
38
43
 
39
- type moment = Time | Integer
40
44
  type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
41
45
  type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
42
46
  type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
@@ -55,12 +59,16 @@ class ULID
55
59
  @next: ULID?
56
60
  @pattern: Regexp?
57
61
  @strict_pattern: Regexp?
62
+ @uuidv4: String?
58
63
  @matchdata: MatchData?
59
64
 
60
65
  def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
61
66
  def self.monotonic_generate: -> ULID
62
67
  def self.current_milliseconds: -> Integer
63
- def self.time_to_milliseconds: (Time time) -> Integer
68
+ def self.milliseconds_from_time: (Time time) -> Integer
69
+ def self.milliseconds_from_moment: (moment moment) -> Integer
70
+ def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
71
+ def self.floor: (Time time) -> Time
64
72
  def self.reasonable_entropy: -> Integer
65
73
  def self.parse: (String string) -> ULID
66
74
  def self.from_uuidv4: (String uuid) -> ULID
@@ -75,8 +83,7 @@ class ULID
75
83
  attr_reader milliseconds: Integer
76
84
  attr_reader entropy: Integer
77
85
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
78
- def to_str: -> String
79
- alias to_s to_str
86
+ def to_s: -> String
80
87
  def to_i: -> Integer
81
88
  alias hash to_i
82
89
  def <=>: (ULID other) -> Integer
@@ -93,10 +100,14 @@ class ULID
93
100
  def octets: -> octets
94
101
  def timestamp_octets: -> timestamp_octets
95
102
  def randomness_octets: -> randomness_octets
96
- def next: -> ULID
103
+ def to_uuidv4: -> String
104
+ def next: -> ULID?
97
105
  alias succ next
106
+ def pred: -> ULID?
98
107
  def freeze: -> self
99
108
 
100
109
  private
110
+ def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
101
111
  def matchdata: -> MatchData
112
+ def cache_all_instance_variables: -> void
102
113
  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.10
4
+ version: 0.0.15
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-05-01 00:00:00.000000000 Z
11
+ date: 2021-05-04 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,11 +84,10 @@ 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: []
@@ -83,7 +96,6 @@ extra_rdoc_files: []
83
96
  files:
84
97
  - LICENSE
85
98
  - README.md
86
- - Steepfile
87
99
  - lib/ulid.rb
88
100
  - lib/ulid/monotonic_generator.rb
89
101
  - lib/ulid/version.rb
@@ -103,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
115
  requirements:
104
116
  - - ">="
105
117
  - !ruby/object:Gem::Version
106
- version: '2.5'
118
+ version: 2.6.0
107
119
  required_rubygems_version: !ruby/object:Gem::Requirement
108
120
  requirements:
109
121
  - - ">="
data/Steepfile DELETED
@@ -1,7 +0,0 @@
1
- target :lib do
2
- signature 'sig'
3
-
4
- check 'lib'
5
-
6
- library 'securerandom'
7
- end