ruby-ulid 0.0.9 → 0.0.14

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