ruby-ulid 0.0.7 → 0.0.12

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