ruby-ulid 0.0.9 → 0.0.14

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: cc7674472c14c213d6fe01c6994520b33bc82ab164b538948a4f1b532d0d88f2
4
- data.tar.gz: 85987c59ca6f119d40caaf69cc830a8e1b5cc2f9ea3250c315be2f9d8b7faa09
3
+ metadata.gz: fa89ecbb2a09940666b57e690d43a985872bb85f40b2b1175c79a762d79c8e45
4
+ data.tar.gz: 2404fe5e1efa77899262fbf8979529fc474e44ffa0a8572ee3cf71eb1caf941a
5
5
  SHA512:
6
- metadata.gz: f56db0a74a7a6559189039a9f7f13a1f514b4fc956f7993a52ca16a7a978b4b93aa352b9f74b72da73c7b0485ebe1f80052d2e3c7b04a1baba231d06ba594f42
7
- data.tar.gz: 597260a0eb43c0fdc187aeb961521fbce22c68fc75b35d445e73d60564b72208aaf1e83a8428daa99423e001572f187872f4077445d6bced8b35d197d7e700ff
6
+ metadata.gz: 1a86224846b27985d641bf485f952583c73dafc87a0ca8f65744cfdfa35371a6f5cfedb4246e7839da6e51fdcfd53632f20057c399038f3399f2986a9bf20085
7
+ data.tar.gz: 24daff089a2b0564a2e7a93c34fef4d44420d1e0bbc6a054c5bcdfac02986c66204515bc085a7dc6499257692682217a888d8a04e04a7ae8de36e884cc182965
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
@@ -23,16 +22,16 @@ class ULID
23
22
  # @see https://www.crockford.com/base32.html
24
23
  ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
25
24
 
26
- TIME_PART_LENGTH = 10
25
+ TIMESTAMP_PART_LENGTH = 10
27
26
  RANDOMNESS_PART_LENGTH = 16
28
- ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
27
+ ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
29
28
  TIMESTAMP_OCTETS_LENGTH = 6
30
29
  RANDOMNESS_OCTETS_LENGTH = 10
31
30
  OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
32
31
  MAX_MILLISECONDS = 281474976710655
33
32
  MAX_ENTROPY = 1208925819614629174706175
34
33
  MAX_INTEGER = 340282366920938463463374607431768211455
35
- PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIME_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
34
+ PATTERN = /(?<timestamp>[0-7][#{encoding_string}]{#{TIMESTAMP_PART_LENGTH - 1}})(?<randomness>[#{encoding_string}]{#{RANDOMNESS_PART_LENGTH}})/i.freeze
36
35
  STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
37
36
 
38
37
  # Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
@@ -42,55 +41,23 @@ class ULID
42
41
  # @see https://bugs.ruby-lang.org/issues/15958
43
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
44
43
 
45
- class MonotonicGenerator
46
- attr_accessor :latest_milliseconds, :latest_entropy
47
-
48
- def initialize
49
- reset
50
- end
51
-
52
- # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
53
- # @return [ULID]
54
- def generate
55
- milliseconds = ULID.current_milliseconds
56
- reasonable_entropy = ULID.reasonable_entropy
57
-
58
- @latest_milliseconds ||= milliseconds
59
- @latest_entropy ||= reasonable_entropy
60
- if @latest_milliseconds != milliseconds
61
- @latest_milliseconds = milliseconds
62
- @latest_entropy = reasonable_entropy
63
- else
64
- @latest_entropy += 1
65
- end
66
-
67
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
68
- end
69
-
70
- # @return [self]
71
- def reset
72
- @latest_milliseconds = nil
73
- @latest_entropy = nil
74
- self
75
- end
76
-
77
- # @raise [TypeError] always raises exception and does not freeze self
78
- # @return [void]
79
- def freeze
80
- raise TypeError, "cannot freeze #{self.class}"
81
- 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
82
49
  end
83
50
 
84
- MONOTONIC_GENERATOR = MonotonicGenerator.new
85
-
86
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
51
+ # @param [Integer, Time] moment
52
+ # @return [ULID]
53
+ def self.min(moment: 0)
54
+ generate(moment: moment, entropy: 0)
55
+ end
87
56
 
88
57
  # @param [Integer, Time] moment
89
- # @param [Integer] entropy
90
58
  # @return [ULID]
91
- def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
92
- milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
93
- new milliseconds: milliseconds, entropy: entropy
59
+ def self.max(moment: MAX_MILLISECONDS)
60
+ generate(moment: moment, entropy: MAX_ENTROPY)
94
61
  end
95
62
 
96
63
  # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
@@ -139,6 +106,7 @@ class ULID
139
106
  # @return [ULID]
140
107
  # @raise [OverflowError] if the given integer is larger than the ULID limit
141
108
  # @raise [ArgumentError] if the given integer is negative number
109
+ # @todo Need optimized for performance
142
110
  def self.from_integer(integer)
143
111
  integer = integer.to_int
144
112
  raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
@@ -153,17 +121,66 @@ class ULID
153
121
  new milliseconds: milliseconds, entropy: entropy
154
122
  end
155
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
+
156
167
  # @return [Integer]
157
168
  def self.current_milliseconds
158
- time_to_milliseconds(Time.now)
169
+ milliseconds_from_time(Time.now)
159
170
  end
160
171
 
161
172
  # @param [Time] time
162
173
  # @return [Integer]
163
- def self.time_to_milliseconds(time)
174
+ def self.milliseconds_from_time(time)
164
175
  (time.to_r * 1000).to_i
165
176
  end
166
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
+
167
184
  # @return [Integer]
168
185
  def self.reasonable_entropy
169
186
  SecureRandom.random_number(MAX_ENTROPY)
@@ -179,8 +196,8 @@ class ULID
179
196
  unless string.size == ENCODED_ID_LENGTH
180
197
  raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
181
198
  end
182
- timestamp = string.slice(0, TIME_PART_LENGTH)
183
- 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)
184
201
  milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
185
202
  entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
186
203
  rescue => err
@@ -199,6 +216,7 @@ class ULID
199
216
  true
200
217
  end
201
218
 
219
+ # @api private
202
220
  # @param [Integer] integer
203
221
  # @param [Integer] length
204
222
  # @return [Array<Integer>]
@@ -210,6 +228,7 @@ class ULID
210
228
  digits.reverse!
211
229
  end
212
230
 
231
+ # @api private
213
232
  # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
214
233
  # @param [Array<Integer>] reversed_digits
215
234
  # @return [Integer]
@@ -224,6 +243,7 @@ class ULID
224
243
 
225
244
  attr_reader :milliseconds, :entropy
226
245
 
246
+ # @api private
227
247
  # @param [Integer] milliseconds
228
248
  # @param [Integer] entropy
229
249
  # @return [void]
@@ -241,10 +261,9 @@ class ULID
241
261
  end
242
262
 
243
263
  # @return [String]
244
- def to_str
264
+ def to_s
245
265
  @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
246
266
  end
247
- alias_method :to_s, :to_str
248
267
 
249
268
  # @return [Integer]
250
269
  def to_i
@@ -259,7 +278,7 @@ class ULID
259
278
 
260
279
  # @return [String]
261
280
  def inspect
262
- @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
281
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
263
282
  end
264
283
 
265
284
  # @return [Boolean]
@@ -286,7 +305,13 @@ class ULID
286
305
 
287
306
  # @return [Time]
288
307
  def to_time
289
- @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
308
+ @time ||= begin
309
+ if RUBY_VERSION >= '2.7'
310
+ Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
311
+ else
312
+ Time.at(0, @milliseconds, :millisecond).utc.freeze
313
+ end
314
+ end
290
315
  end
291
316
 
292
317
  # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
@@ -324,20 +349,29 @@ class ULID
324
349
  @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
325
350
  end
326
351
 
327
- # @raise [OverflowError] if the next entropy part is larger than the ULID limit
328
- # @return [ULID]
352
+ # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
329
353
  def next
330
- @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
354
+ next_int = to_i.next
355
+ return nil if next_int > MAX_INTEGER
356
+ @next ||= self.class.from_integer(next_int)
331
357
  end
332
358
  alias_method :succ, :next
333
359
 
360
+ # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
361
+ def pred
362
+ pre_int = to_i.pred
363
+ return nil if pre_int.negative?
364
+ @pred ||= self.class.from_integer(pre_int)
365
+ end
366
+
334
367
  # @return [self]
335
368
  def freeze
336
369
  # Evaluate all caching
337
370
  inspect
338
371
  octets
339
- succ
340
372
  to_i
373
+ succ
374
+ pred
341
375
  strict_pattern
342
376
  super
343
377
  end
@@ -346,6 +380,15 @@ class ULID
346
380
 
347
381
  # @return [MatchData]
348
382
  def matchdata
349
- @matchdata ||= STRICT_PATTERN.match(to_str).freeze
383
+ @matchdata ||= STRICT_PATTERN.match(to_s).freeze
350
384
  end
351
385
  end
386
+
387
+ require_relative 'ulid/version'
388
+ require_relative 'ulid/monotonic_generator'
389
+
390
+ class ULID
391
+ MONOTONIC_GENERATOR = MonotonicGenerator.new
392
+
393
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
394
+ 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.9'
5
+ VERSION = '0.0.14'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -2,7 +2,7 @@
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
8
  TIMESTAMP_OCTETS_LENGTH: 6
@@ -18,6 +18,8 @@ class ULID
18
18
  MONOTONIC_GENERATOR: MonotonicGenerator
19
19
  include Comparable
20
20
 
21
+ type moment = Time | Integer
22
+
21
23
  class Error < StandardError
22
24
  end
23
25
 
@@ -28,10 +30,10 @@ class ULID
28
30
  end
29
31
 
30
32
  class MonotonicGenerator
31
- attr_accessor latest_milliseconds: Integer?
32
- attr_accessor latest_entropy: Integer?
33
+ attr_accessor latest_milliseconds: Integer
34
+ attr_accessor latest_entropy: Integer
33
35
  def initialize: -> void
34
- def generate: -> ULID
36
+ def generate: (?moment: moment) -> ULID
35
37
  def reset: -> void
36
38
  def freeze: -> void
37
39
  end
@@ -56,14 +58,19 @@ class ULID
56
58
  @strict_pattern: Regexp?
57
59
  @matchdata: MatchData?
58
60
 
59
- def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
61
+ def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
60
62
  def self.monotonic_generate: -> ULID
61
63
  def self.current_milliseconds: -> Integer
62
- 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
63
68
  def self.reasonable_entropy: -> Integer
64
69
  def self.parse: (String string) -> ULID
65
70
  def self.from_uuidv4: (String uuid) -> ULID
66
71
  def self.from_integer: (Integer integer) -> ULID
72
+ def self.min: (?moment: moment) -> ULID
73
+ def self.max: (?moment: moment) -> ULID
67
74
  def self.valid?: (untyped string) -> bool
68
75
  def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
69
76
  | (String string) { (ULID ulid) -> void } -> singleton(ULID)
@@ -72,8 +79,7 @@ class ULID
72
79
  attr_reader milliseconds: Integer
73
80
  attr_reader entropy: Integer
74
81
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
75
- def to_str: -> String
76
- alias to_s to_str
82
+ def to_s: -> String
77
83
  def to_i: -> Integer
78
84
  alias hash to_i
79
85
  def <=>: (ULID other) -> Integer
@@ -90,8 +96,9 @@ class ULID
90
96
  def octets: -> octets
91
97
  def timestamp_octets: -> timestamp_octets
92
98
  def randomness_octets: -> randomness_octets
93
- def next: -> ULID
99
+ def next: -> ULID?
94
100
  alias succ next
101
+ def pred: -> ULID?
95
102
  def freeze: -> self
96
103
 
97
104
  private
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.9
4
+ version: 0.0.14
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-30 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