ruby-ulid 0.0.7 → 0.0.12

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: 1f33ff714eef2b217cb557fe4ae46087a77b834aa42c406e940f2f8a6630464c
4
- data.tar.gz: ce004faa759110d8494f34a058230a90d7231af4a309c5c7bf66b0f0963bcf81
3
+ metadata.gz: 0adfc974863e1b2d248eac28305322e20d6b2dc94465b7ca3462fdd2c8676352
4
+ data.tar.gz: caecb83a030fa1eda5534e748ee3206208ebf25a1d9f76d33ad04030b4ea0f0c
5
5
  SHA512:
6
- metadata.gz: d60377fcf5cd9523b09eb78be1feef0fb996b9f6ebf0e430db9bf3902899ee6e6e6d62518a9f33ecfb5d6786390e08bbf545d5a9b36f141fd4a95bc7ff8f6612
7
- data.tar.gz: 981bafadfcbb63254a276a1ae9b201b2b636555416450ce421ffa0f4482e343072811623ae4856970904227ae0da0fe2f991ffc809556d4bc6a33a910446c7dd
6
+ metadata.gz: 4a216a8032385e9be00252fb0d9faf1aaa18bdfe75a0ae4b0a2cb1aa438a8786a7e9c477debd87fb6871274c0a50e1579cee41584513199c47b0198c51c6ac6a
7
+ data.tar.gz: 664d85fe6070a9c0017e831727a33a87799d1b10e7c4992b44b77be288fcc3cab51ecb528ce01fe0ce5d7a6262f61a2786ce30c54dcd90e8ef36c698f3f76f45
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,210 @@
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
+ ```console
39
+ $ gem install ruby-ulid
40
+ #=> Installed
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ The generated `ULID` is an object not just a string.
46
+ It means easily get the timestamps and binary formats.
47
+
48
+ ```ruby
49
+ require 'ulid'
50
+
51
+ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GRMJ9AA)
52
+ ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
53
+ ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
54
+ ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
55
+ ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
56
+ ```
57
+
58
+ Generator can take `Time` instance
59
+
60
+ ```ruby
61
+ time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
62
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
63
+ ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
64
+
65
+ ulids = 1000.times.map do
66
+ ULID.generate(moment: time)
67
+ end
68
+ ulids.sort == ulids #=> false
69
+
70
+ ulids = 1000.times.map do |n|
71
+ ULID.generate(moment: time + n)
72
+ end
73
+ ulids.sort == ulids #=> true
74
+ ```
75
+
76
+ You can parse from exists IDs
77
+
78
+ FYI: 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).
79
+
80
+ ```ruby
81
+ ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
82
+ ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
83
+ ```
84
+
85
+ ULIDs are sortable when they are generated in different timestamp with milliseconds precision
86
+
87
+ ```ruby
88
+ ulids = 1000.times.map do
89
+ sleep(0.001)
90
+ ULID.generate
91
+ end
92
+ ulids.sort == ulids #=> true
93
+ ulids.uniq(&:to_time).size #=> 1000
94
+ ```
95
+
96
+ The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
97
+
98
+ ```ruby
99
+ ulids = 10000.times.map do
100
+ ULID.generate
101
+ end
102
+ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in environment)
103
+ ulids.sort == ulids #=> false
104
+ ```
105
+
106
+ If you want to prefer `sortable` rather than the `strict randomness`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
107
+ (Though it starts with new random value when changed the timestamp)
108
+
109
+ ```ruby
110
+ monotonic_generator = ULID::MonotonicGenerator.new
111
+ monotonic_ulids = 10000.times.map do
112
+ monotonic_generator.generate
113
+ end
114
+ sample_ulids_by_the_time = monotonic_ulids.uniq(&:to_time)
115
+ sample_ulids_by_the_time.size #=> 34 (the size is not fixed, might be changed in environment)
116
+ sample_ulids_by_the_time.take(10).map(&:randomness)
117
+ #=>
118
+ ["JZW56CTA8704D5AQ",
119
+ "JGEBH2A2B2EA97MW",
120
+ "0XPE4NS3MZH0NAJ4",
121
+ "E0S3ZAVADFBPW57Y",
122
+ "E5CX1T6281443THQ",
123
+ "3SK8WHSH03CVF7J2",
124
+ "DDS35BT0R20P3V49",
125
+ "60KG2W9FVEN1ZX8C",
126
+ "X59YJVXXVH7AXJJE",
127
+ "1ZBQ7SNGFKXGH1Y4"]
128
+
129
+ monotonic_ulids.sort == monotonic_ulids #=> true
130
+ ```
131
+
132
+ For rough operations, `ULID.scan` might be useful.
133
+
134
+ ```ruby
135
+ json =<<'EOD'
136
+ {
137
+ "id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3",
138
+ "author": {
139
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
140
+ "name": "kachick"
141
+ },
142
+ "title": "My awesome blog post",
143
+ "comments": [
144
+ {
145
+ "id": "01F4GNCNC3CH0BCRZBPPDEKBKS",
146
+ "commenter": {
147
+ "id": "01F4GNBXW1AM2KWW52PVT3ZY9X",
148
+ "name": "kachick"
149
+ }
150
+ },
151
+ {
152
+ "id": "01F4GNCXAMXQ1SGBH5XCR6ZH0M",
153
+ "commenter": {
154
+ "id": "01F4GND4RYYSKNAADHQ9BNXAWJ",
155
+ "name": "pankona"
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ EOD
161
+
162
+ ULID.scan(json).to_a
163
+ #=>
164
+ [ULID(2021-04-30 05:51:57.119 UTC: 01F4GNAV5ZR6FJQ5SFQC7WDSY3),
165
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
166
+ ULID(2021-04-30 05:52:56.707 UTC: 01F4GNCNC3CH0BCRZBPPDEKBKS),
167
+ ULID(2021-04-30 05:52:32.641 UTC: 01F4GNBXW1AM2KWW52PVT3ZY9X),
168
+ ULID(2021-04-30 05:53:04.852 UTC: 01F4GNCXAMXQ1SGBH5XCR6ZH0M),
169
+ ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
170
+ ```
171
+
172
+ `ULID.min` and `ULID.max` return termination values for ULID spec.
173
+
174
+ ```ruby
175
+ ULID.min #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000000000000000000000)
176
+ ULID.max #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZZZZZZZZZZZZZZZZZ)
177
+
178
+ time = Time.at(946684800, Rational('123456.789')).utc #=> 2000-01-01 00:00:00.123456789 UTC
179
+ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V0000000000000000)
180
+ ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
181
+ ```
182
+
183
+ `ULID#next` and `ULID#succ` returns next(successor) ULID
184
+
185
+ ```ruby
186
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
187
+ ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZZ').next.to_s #=> "01BX5ZZKBM0000000000000000"
188
+ ULID.parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').next #=> nil
189
+ ```
190
+
191
+ `ULID#pred` returns predecessor ULID
192
+
193
+ ```ruby
194
+ ULID.parse('01BX5ZZKBK0000000000000001').pred.to_s #=> "01BX5ZZKBK0000000000000000"
195
+ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZZZ"
196
+ ULID.parse('00000000000000000000000000').pred #=> nil
197
+ ```
198
+
199
+ UUIDv4 converter for migration use-cases. (Of course the timestamp will be useless one. Sortable benefit is lost.)
200
+
201
+ ```ruby
202
+ ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')
203
+ #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
204
+ ```
205
+
206
+ ## References
207
+
208
+ - [API documents](https://kachick.github.io/ruby-ulid/)
209
+ - [ulid/spec](https://github.com/ulid/spec)
210
+ - [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)
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
@@ -3,11 +3,13 @@
3
3
  # Copyright (C) 2021 Kenichi Kamiya
4
4
 
5
5
  require 'securerandom'
6
- require 'singleton'
7
6
  require 'integer/base'
8
- require_relative 'ulid/version'
9
7
 
10
8
  # @see https://github.com/ulid/spec
9
+ # @!attribute [r] milliseconds
10
+ # @return [Integer]
11
+ # @!attribute [r] entropy
12
+ # @return [Integer]
11
13
  class ULID
12
14
  include Comparable
13
15
 
@@ -15,90 +17,137 @@ class ULID
15
17
  class OverflowError < Error; end
16
18
  class ParserError < Error; end
17
19
 
20
+ encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
18
21
  # Crockford's Base32. Excluded I, L, O, U.
19
22
  # @see https://www.crockford.com/base32.html
20
- ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.chars.map(&:freeze).freeze
23
+ ENCODING_CHARS = encoding_string.chars.map(&:freeze).freeze
21
24
 
22
- TIME_PART_LENGTH = 10
25
+ TIMESTAMP_PART_LENGTH = 10
23
26
  RANDOMNESS_PART_LENGTH = 16
24
- ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
25
- TIME_OCTETS_LENGTH = 6
27
+ ENCODED_ID_LENGTH = TIMESTAMP_PART_LENGTH + RANDOMNESS_PART_LENGTH
28
+ TIMESTAMP_OCTETS_LENGTH = 6
26
29
  RANDOMNESS_OCTETS_LENGTH = 10
27
- OCTETS_LENGTH = TIME_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
30
+ OCTETS_LENGTH = TIMESTAMP_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
28
31
  MAX_MILLISECONDS = 281474976710655
29
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
30
39
 
31
40
  # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
32
41
  # @see https://bugs.ruby-lang.org/issues/15958
33
42
  TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
34
43
 
35
- class MonotonicGenerator
36
- include Singleton
37
-
38
- attr_accessor :latest_milliseconds, :latest_entropy
39
-
40
- def initialize
41
- reset
42
- 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
49
+ end
43
50
 
44
- # @return [ULID]
45
- def generate
46
- milliseconds = ULID.current_milliseconds
47
- reasonable_entropy = ULID.reasonable_entropy
51
+ # @param [Integer, Time] moment
52
+ # @return [ULID]
53
+ def self.min(moment: 0)
54
+ generate(moment: moment, entropy: 0)
55
+ end
48
56
 
49
- @latest_milliseconds ||= milliseconds
50
- @latest_entropy ||= reasonable_entropy
51
- if @latest_milliseconds != milliseconds
52
- @latest_milliseconds = milliseconds
53
- @latest_entropy = reasonable_entropy
54
- else
55
- @latest_entropy += 1
56
- end
57
+ # @param [Integer, Time] moment
58
+ # @return [ULID]
59
+ def self.max(moment: MAX_MILLISECONDS)
60
+ generate(moment: moment, entropy: MAX_ENTROPY)
61
+ end
57
62
 
58
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
63
+ # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
64
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
65
+ # @return [ULID]
66
+ def self.monotonic_generate
67
+ warning = "`ULID.monotonic_generate` actually changes class state. Use `ULID::MonotonicGenerator` instead."
68
+ if RUBY_VERSION >= '3.0'
69
+ Warning.warn(warning, category: :deprecated)
70
+ else
71
+ Warning.warn(warning)
59
72
  end
60
73
 
61
- # @return [self]
62
- def reset
63
- @latest_milliseconds = nil
64
- @latest_entropy = nil
65
- self
66
- end
74
+ MONOTONIC_GENERATOR.generate
75
+ end
67
76
 
68
- # @return [void]
69
- def freeze
70
- raise TypeError, "cannot freeze #{self.class}"
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)
71
86
  end
87
+ self
72
88
  end
73
89
 
74
- MONOTONIC_GENERATOR = MonotonicGenerator.instance
75
-
76
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :MonotonicGenerator
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
77
104
 
78
- # @param [Integer, Time] moment
79
- # @param [Integer] entropy
105
+ # @param [Integer, #to_int] integer
80
106
  # @return [ULID]
81
- def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
82
- milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
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
+
83
121
  new milliseconds: milliseconds, entropy: entropy
84
122
  end
85
123
 
86
- # @return [ULID]
87
- def self.monotonic_generate
88
- MONOTONIC_GENERATOR.generate
124
+ # @param [Time] time
125
+ # @return [Time]
126
+ def self.floor(time)
127
+ if RUBY_VERSION >= '2.7'
128
+ time.floor(3)
129
+ else
130
+ Time.at(0, milliseconds_from_time(time), :millisecond)
131
+ end
89
132
  end
90
133
 
91
134
  # @return [Integer]
92
135
  def self.current_milliseconds
93
- time_to_milliseconds(Time.now)
136
+ milliseconds_from_time(Time.now)
94
137
  end
95
138
 
96
139
  # @param [Time] time
97
140
  # @return [Integer]
98
- def self.time_to_milliseconds(time)
141
+ def self.milliseconds_from_time(time)
99
142
  (time.to_r * 1000).to_i
100
143
  end
101
144
 
145
+ # @param [Time, Integer] moment
146
+ # @return [Integer]
147
+ def self.milliseconds_from_moment(moment)
148
+ moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
149
+ end
150
+
102
151
  # @return [Integer]
103
152
  def self.reasonable_entropy
104
153
  SecureRandom.random_number(MAX_ENTROPY)
@@ -106,14 +155,16 @@ class ULID
106
155
 
107
156
  # @param [String, #to_str] string
108
157
  # @return [ULID]
158
+ # @raise [ParserError] if the given format is not correct for ULID specs
159
+ # @raise [OverflowError] if the given value is larger than the ULID limit
109
160
  def self.parse(string)
110
161
  begin
111
162
  string = string.to_str
112
163
  unless string.size == ENCODED_ID_LENGTH
113
164
  raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
114
165
  end
115
- timestamp = string.slice(0, TIME_PART_LENGTH)
116
- randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
166
+ timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
167
+ randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
117
168
  milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
118
169
  entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
119
170
  rescue => err
@@ -123,7 +174,6 @@ class ULID
123
174
  new milliseconds: milliseconds, entropy: entropy
124
175
  end
125
176
 
126
- # @param [String] string
127
177
  # @return [Boolean]
128
178
  def self.valid?(string)
129
179
  parse(string)
@@ -133,8 +183,39 @@ class ULID
133
183
  true
134
184
  end
135
185
 
186
+ # @api private
187
+ # @param [Integer] integer
188
+ # @param [Integer] length
189
+ # @return [Array<Integer>]
190
+ def self.octets_from_integer(integer, length:)
191
+ digits = integer.digits(256)
192
+ (length - digits.size).times do
193
+ digits.push 0
194
+ end
195
+ digits.reverse!
196
+ end
197
+
198
+ # @api private
199
+ # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
200
+ # @param [Array<Integer>] reversed_digits
201
+ # @return [Integer]
202
+ def self.inverse_of_digits(reversed_digits)
203
+ base = 256
204
+ num = 0
205
+ reversed_digits.each do |digit|
206
+ num = (num * base) + digit
207
+ end
208
+ num
209
+ end
210
+
136
211
  attr_reader :milliseconds, :entropy
137
212
 
213
+ # @api private
214
+ # @param [Integer] milliseconds
215
+ # @param [Integer] entropy
216
+ # @return [void]
217
+ # @raise [OverflowError] if the given value is larger than the ULID limit
218
+ # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
138
219
  def initialize(milliseconds:, entropy:)
139
220
  milliseconds = milliseconds.to_int
140
221
  entropy = entropy.to_int
@@ -154,13 +235,13 @@ class ULID
154
235
 
155
236
  # @return [Integer]
156
237
  def to_i
157
- @integer ||= inverse_of_digits(octets)
238
+ @integer ||= self.class.inverse_of_digits(octets)
158
239
  end
159
240
  alias_method :hash, :to_i
160
241
 
161
242
  # @return [Integer, nil]
162
243
  def <=>(other)
163
- other.kind_of?(self.class) ? (to_i <=> other.to_i) : nil
244
+ other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
164
245
  end
165
246
 
166
247
  # @return [String]
@@ -170,7 +251,7 @@ class ULID
170
251
 
171
252
  # @return [Boolean]
172
253
  def eql?(other)
173
- other.equal?(self) || (other.kind_of?(self.class) && other.to_i == to_i)
254
+ other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
174
255
  end
175
256
  alias_method :==, :eql?
176
257
 
@@ -192,62 +273,90 @@ class ULID
192
273
 
193
274
  # @return [Time]
194
275
  def to_time
195
- @time ||= Time.at(0, @milliseconds, :millisecond).utc
276
+ @time ||= begin
277
+ if RUBY_VERSION >= '2.7'
278
+ Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
279
+ else
280
+ Time.at(0, @milliseconds, :millisecond).utc.freeze
281
+ end
282
+ end
196
283
  end
197
284
 
198
- # @return [Array<Integer>]
285
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
199
286
  def octets
200
- @octets ||= (time_octets + randomness_octets).freeze
287
+ @octets ||= (timestamp_octets + randomness_octets).freeze
201
288
  end
202
289
 
203
- # @return [Array<Integer>]
204
- def time_octets
205
- @time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
290
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
291
+ def timestamp_octets
292
+ @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
206
293
  end
207
294
 
208
- # @return [Array<Integer>]
295
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
209
296
  def randomness_octets
210
- @randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
297
+ @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
211
298
  end
212
299
 
213
- # @return [ULID]
300
+ # @return [String]
301
+ def timestamp
302
+ @timestamp ||= matchdata[:timestamp].freeze
303
+ end
304
+
305
+ # @return [String]
306
+ def randomness
307
+ @randomness ||= matchdata[:randomness].freeze
308
+ end
309
+
310
+ # @return [Regexp]
311
+ def pattern
312
+ @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
313
+ end
314
+
315
+ # @return [Regexp]
316
+ def strict_pattern
317
+ @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
318
+ end
319
+
320
+ # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
214
321
  def next
215
- @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
322
+ next_int = to_i.next
323
+ return nil if next_int > MAX_INTEGER
324
+ @next ||= self.class.from_integer(next_int)
216
325
  end
217
326
  alias_method :succ, :next
218
327
 
328
+ # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
329
+ def pred
330
+ pre_int = to_i.pred
331
+ return nil if pre_int.negative?
332
+ @pred ||= self.class.from_integer(pre_int)
333
+ end
334
+
219
335
  # @return [self]
220
336
  def freeze
221
337
  # Evaluate all caching
222
338
  inspect
223
339
  octets
224
- succ
225
340
  to_i
341
+ succ
342
+ pred
343
+ strict_pattern
226
344
  super
227
345
  end
228
346
 
229
347
  private
230
348
 
231
- # @param [Integer] integer
232
- # @param [Integer] length
233
- # @return [Array<Integer>]
234
- def octets_from_integer(integer, length:)
235
- digits = integer.digits(256)
236
- (length - digits.size).times do
237
- digits.push 0
238
- end
239
- digits.reverse!
349
+ # @return [MatchData]
350
+ def matchdata
351
+ @matchdata ||= STRICT_PATTERN.match(to_str).freeze
240
352
  end
353
+ end
241
354
 
242
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
243
- # @param [Array<Integer>] reversed_digits
244
- # @return [Integer]
245
- def inverse_of_digits(reversed_digits)
246
- base = 256
247
- num = 0
248
- reversed_digits.each do |digit|
249
- num = (num * base) + digit
250
- end
251
- num
252
- end
355
+ require_relative 'ulid/version'
356
+ require_relative 'ulid/monotonic_generator'
357
+
358
+ class ULID
359
+ MONOTONIC_GENERATOR = MonotonicGenerator.new
360
+
361
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
253
362
  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.7'
5
+ VERSION = '0.0.12'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -2,74 +2,105 @@
2
2
  class ULID
3
3
  VERSION: String
4
4
  ENCODING_CHARS: Array[String]
5
- TIME_PART_LENGTH: Integer
6
- RANDOMNESS_PART_LENGTH: Integer
7
- ENCODED_ID_LENGTH: Integer
8
- TIME_OCTETS_LENGTH: Integer
9
- RANDOMNESS_OCTETS_LENGTH: Integer
10
- OCTETS_LENGTH: Integer
11
- MAX_MILLISECONDS: Integer
12
- MAX_ENTROPY: Integer
13
- TIME_FORMAT_IN_INSPECT: String
5
+ TIMESTAMP_PART_LENGTH: 10
6
+ RANDOMNESS_PART_LENGTH: 16
7
+ ENCODED_ID_LENGTH: 26
8
+ TIMESTAMP_OCTETS_LENGTH: 6
9
+ RANDOMNESS_OCTETS_LENGTH: 10
10
+ OCTETS_LENGTH: 16
11
+ MAX_MILLISECONDS: 281474976710655
12
+ MAX_ENTROPY: 1208925819614629174706175
13
+ MAX_INTEGER: 340282366920938463463374607431768211455
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
- @string: String
17
- @integer: Integer
18
- @octets: Array[Integer]
19
- @time_octets: Array[Integer]
20
- @randomness_octets: Array[Integer]
21
- @inspect: String
22
- @time: Time
23
- @next: ULID
24
20
 
25
- def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
21
+ type moment = Time | Integer
22
+
23
+ class Error < StandardError
24
+ end
25
+
26
+ class OverflowError < Error
27
+ end
28
+
29
+ class ParserError < Error
30
+ end
31
+
32
+ class MonotonicGenerator
33
+ attr_accessor latest_milliseconds: Integer
34
+ attr_accessor latest_entropy: Integer
35
+ def initialize: -> void
36
+ def generate: (?moment: moment) -> ULID
37
+ def reset: -> void
38
+ def freeze: -> void
39
+ end
40
+
41
+ type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
42
+ type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
43
+ type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
44
+
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?
60
+
61
+ def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
26
62
  def self.monotonic_generate: -> ULID
27
63
  def self.current_milliseconds: -> Integer
28
- 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.floor: (Time time) -> Time
29
67
  def self.reasonable_entropy: -> Integer
30
68
  def self.parse: (String string) -> ULID
69
+ def self.from_uuidv4: (String uuid) -> ULID
70
+ def self.from_integer: (Integer integer) -> ULID
71
+ def self.min: (?moment: moment) -> ULID
72
+ def self.max: (?moment: moment) -> ULID
31
73
  def self.valid?: (untyped string) -> bool
74
+ def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
75
+ | (String string) { (ULID ulid) -> void } -> singleton(ULID)
76
+ def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
77
+ def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
32
78
  attr_reader milliseconds: Integer
33
79
  attr_reader entropy: Integer
34
80
  def initialize: (milliseconds: Integer, entropy: Integer) -> void
35
81
  def to_str: -> String
36
- def to_s: -> String
82
+ alias to_s to_str
37
83
  def to_i: -> Integer
38
- def hash: -> Integer
39
- def <=>: (untyped other) -> Integer?
84
+ alias hash to_i
85
+ def <=>: (ULID other) -> Integer
86
+ | (untyped other) -> Integer?
40
87
  def inspect: -> String
41
88
  def eql?: (untyped other) -> bool
42
- def ==: (untyped other) -> bool
89
+ alias == eql?
43
90
  def ===: (untyped other) -> bool
44
91
  def to_time: -> Time
45
- def octets: -> Array[Integer]
46
- def time_octets: -> Array[Integer]
47
- def randomness_octets: -> Array[Integer]
48
- def next: -> ULID
49
- def succ: -> ULID
92
+ def timestamp: -> String
93
+ def randomness: -> String
94
+ def pattern: -> Regexp
95
+ def strict_pattern: -> Regexp
96
+ def octets: -> octets
97
+ def timestamp_octets: -> timestamp_octets
98
+ def randomness_octets: -> randomness_octets
99
+ def next: -> ULID?
100
+ alias succ next
101
+ def pred: -> ULID?
50
102
  def freeze: -> self
51
103
 
52
104
  private
53
- def octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
54
- def inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
55
-
56
- class Error < StandardError
57
- end
58
-
59
- class OverflowError < Error
60
- end
61
-
62
- class ParserError < Error
63
- end
64
-
65
- class MonotonicGenerator
66
- include Singleton
67
-
68
- attr_accessor latest_milliseconds: Integer?
69
- attr_accessor latest_entropy: Integer?
70
- def initialize: -> void
71
- def generate: -> ULID
72
- def reset: -> void
73
- def freeze: -> void
74
- end
105
+ def matchdata: -> MatchData
75
106
  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.7
4
+ version: 0.0.12
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-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -70,18 +70,21 @@ dependencies:
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
72
  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.
73
+ description: " ULID(Universally Unique Lexicographically Sortable Identifier) has
74
+ useful specs for applications (e.g. `Database key`). \n This gem aims to provide
75
+ the generator, monotonic generator, parser and handy manipulation features around
76
+ the ULID.\n Also providing `rbs` signature files.\n"
78
77
  email:
79
78
  - kachick1+ruby@gmail.com
80
79
  executables: []
81
80
  extensions: []
82
81
  extra_rdoc_files: []
83
82
  files:
83
+ - LICENSE
84
+ - README.md
85
+ - Steepfile
84
86
  - lib/ulid.rb
87
+ - lib/ulid/monotonic_generator.rb
85
88
  - lib/ulid/version.rb
86
89
  - sig/ulid.rbs
87
90
  homepage: https://github.com/kachick/ruby-ulid