ruby-ulid 0.0.6 → 0.0.11

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: e00e5eba399c7d96e26ada6a0f0463fdb7fc08d7ec69c96588a5f85320a997be
4
- data.tar.gz: e5f79b9a86ab038be2d0b39a787d8e70108998d3a024c699e401770ec6edade3
3
+ metadata.gz: 91f6d4c9dad8e12663686099c487dbbf3ec5b7995e0e4b2b210ac8a16fbcd91c
4
+ data.tar.gz: 1ca6e29d83771636b2a20aa71d90f15699d07d6a7a75f22c306094507bb51d89
5
5
  SHA512:
6
- metadata.gz: 0b4221414e2cdeeb67f6f6173c5ce25475f388e11df12c16864f576d76cac1fe3197dc5c2dd6ddef03d4fdfe6c079e0256a91c7b1728af73dd9d1b56c6ca7109
7
- data.tar.gz: 369838ea1deac94dc44c71a37a606eb57a88f0266910f555a3ede1b7d85653be564499f7bf009cdd426a4a84a4be7e4d0f046ca4106bf25e8613cede927a47c5
6
+ metadata.gz: 22dcc2541f7e4fa1a1d4014f996aef3d3bfcb69b002c1cc6ca4864e17bce50a327e1ec98453b4dab0773400b678055dc024a7287342a08916ce30772aadb418d
7
+ data.tar.gz: 27c69c7319858a4c732f04f6944ff180f3dfc9a5df12bd67cd1e4cac91421a2bc00d1a79c8ff91190b425731f831fdaef1a2f5bc0c3ef4fdaff73a6ce6749e35
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,77 +17,109 @@ 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
44
+ # @param [Integer, Time] moment
45
+ # @param [Integer] entropy
46
+ # @return [ULID]
47
+ def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
48
+ milliseconds = moment.kind_of?(Time) ? time_to_milliseconds(moment) : moment
49
+ new milliseconds: milliseconds, entropy: entropy
50
+ end
39
51
 
40
- def initialize
41
- reset
42
- end
52
+ # @param [Integer, Time] moment
53
+ # @return [ULID]
54
+ def self.min(moment: 0)
55
+ generate(moment: moment, entropy: 0)
56
+ end
43
57
 
44
- # @return [ULID]
45
- def generate
46
- milliseconds = ULID.current_milliseconds
47
- reasonable_entropy = ULID.reasonable_entropy
48
-
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
58
+ # @param [Integer, Time] moment
59
+ # @return [ULID]
60
+ def self.max(moment: MAX_MILLISECONDS)
61
+ generate(moment: moment, entropy: MAX_ENTROPY)
62
+ end
57
63
 
58
- ULID.new milliseconds: milliseconds, entropy: @latest_entropy
64
+ # @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
65
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
66
+ # @return [ULID]
67
+ def self.monotonic_generate
68
+ warning = "`ULID.monotonic_generate` actually changes class state. Use `ULID::MonotonicGenerator` instead."
69
+ if RUBY_VERSION >= '3.0'
70
+ Warning.warn(warning, category: :deprecated)
71
+ else
72
+ Warning.warn(warning)
59
73
  end
60
74
 
61
- # @return [self]
62
- def reset
63
- @latest_milliseconds = nil
64
- @latest_entropy = nil
65
- self
66
- end
75
+ MONOTONIC_GENERATOR.generate
76
+ end
67
77
 
68
- # @return [void]
69
- def freeze
70
- raise TypeError, "cannot freeze #{self.class}"
78
+ # @param [String, #to_str] string
79
+ # @return [Enumerator]
80
+ # @yieldparam [ULID] ulid
81
+ # @yieldreturn [self]
82
+ def self.scan(string)
83
+ string = string.to_str
84
+ return to_enum(__callee__, string) unless block_given?
85
+ string.scan(PATTERN) do |pair|
86
+ yield parse(pair.join)
71
87
  end
88
+ self
72
89
  end
73
90
 
74
- MONOTONIC_GENERATOR = MonotonicGenerator.instance
75
-
76
- private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :MonotonicGenerator
77
-
78
- # @param [Integer, Time] moment
79
- # @param [Integer] entropy
91
+ # @param [String, #to_str] uuid
80
92
  # @return [ULID]
81
- def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
82
- milliseconds = moment.kind_of?(Time) ? (moment.to_r * 1000).to_i : moment.to_int
83
- new milliseconds: milliseconds, entropy: entropy
93
+ # @raise [ParserError] if the given format is not correct for UUIDv4 specs
94
+ def self.from_uuidv4(uuid)
95
+ begin
96
+ uuid = uuid.to_str
97
+ prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
98
+ raise "given string is not matched to pattern #{UUIDV4_PATTERN.inspect}" unless UUIDV4_PATTERN.match?(prefix_trimmed)
99
+ normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
100
+ from_integer(normalized.to_i(16))
101
+ rescue => err
102
+ raise ParserError, "parsing failure as #{err.inspect} for given #{uuid}"
103
+ end
84
104
  end
85
105
 
106
+ # @param [Integer, #to_int] integer
86
107
  # @return [ULID]
87
- def self.monotonic_generate
88
- MONOTONIC_GENERATOR.generate
108
+ # @raise [OverflowError] if the given integer is larger than the ULID limit
109
+ # @raise [ArgumentError] if the given integer is negative number
110
+ # @todo Need optimized for performance
111
+ def self.from_integer(integer)
112
+ integer = integer.to_int
113
+ raise OverflowError, "integer overflow: given #{integer}, max: #{MAX_INTEGER}" unless integer <= MAX_INTEGER
114
+ raise ArgumentError, "integer should not be negative: given: #{integer}" if integer.negative?
115
+
116
+ octets = octets_from_integer(integer, length: OCTETS_LENGTH).freeze
117
+ time_octets = octets.slice(0, TIMESTAMP_OCTETS_LENGTH).freeze
118
+ randomness_octets = octets.slice(TIMESTAMP_OCTETS_LENGTH, RANDOMNESS_OCTETS_LENGTH).freeze
119
+ milliseconds = inverse_of_digits(time_octets)
120
+ entropy = inverse_of_digits(randomness_octets)
121
+
122
+ new milliseconds: milliseconds, entropy: entropy
89
123
  end
90
124
 
91
125
  # @return [Integer]
@@ -106,14 +140,16 @@ class ULID
106
140
 
107
141
  # @param [String, #to_str] string
108
142
  # @return [ULID]
143
+ # @raise [ParserError] if the given format is not correct for ULID specs
144
+ # @raise [OverflowError] if the given value is larger than the ULID limit
109
145
  def self.parse(string)
110
146
  begin
111
147
  string = string.to_str
112
148
  unless string.size == ENCODED_ID_LENGTH
113
149
  raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
114
150
  end
115
- timestamp = string.slice(0, TIME_PART_LENGTH)
116
- randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
151
+ timestamp = string.slice(0, TIMESTAMP_PART_LENGTH)
152
+ randomness = string.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
117
153
  milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
118
154
  entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
119
155
  rescue => err
@@ -123,7 +159,6 @@ class ULID
123
159
  new milliseconds: milliseconds, entropy: entropy
124
160
  end
125
161
 
126
- # @param [String] string
127
162
  # @return [Boolean]
128
163
  def self.valid?(string)
129
164
  parse(string)
@@ -133,8 +168,36 @@ class ULID
133
168
  true
134
169
  end
135
170
 
171
+ # @param [Integer] integer
172
+ # @param [Integer] length
173
+ # @return [Array<Integer>]
174
+ def self.octets_from_integer(integer, length:)
175
+ digits = integer.digits(256)
176
+ (length - digits.size).times do
177
+ digits.push 0
178
+ end
179
+ digits.reverse!
180
+ end
181
+
182
+ # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
183
+ # @param [Array<Integer>] reversed_digits
184
+ # @return [Integer]
185
+ def self.inverse_of_digits(reversed_digits)
186
+ base = 256
187
+ num = 0
188
+ reversed_digits.each do |digit|
189
+ num = (num * base) + digit
190
+ end
191
+ num
192
+ end
193
+
136
194
  attr_reader :milliseconds, :entropy
137
195
 
196
+ # @param [Integer] milliseconds
197
+ # @param [Integer] entropy
198
+ # @return [void]
199
+ # @raise [OverflowError] if the given value is larger than the ULID limit
200
+ # @raise [ArgumentError] if the given milliseconds and/or entropy is negative number
138
201
  def initialize(milliseconds:, entropy:)
139
202
  milliseconds = milliseconds.to_int
140
203
  entropy = entropy.to_int
@@ -154,13 +217,13 @@ class ULID
154
217
 
155
218
  # @return [Integer]
156
219
  def to_i
157
- @integer ||= inverse_of_digits(octets)
220
+ @integer ||= self.class.inverse_of_digits(octets)
158
221
  end
159
222
  alias_method :hash, :to_i
160
223
 
161
224
  # @return [Integer, nil]
162
225
  def <=>(other)
163
- other.kind_of?(self.class) ? (to_i <=> other.to_i) : nil
226
+ other.kind_of?(ULID) ? (to_i <=> other.to_i) : nil
164
227
  end
165
228
 
166
229
  # @return [String]
@@ -170,68 +233,106 @@ class ULID
170
233
 
171
234
  # @return [Boolean]
172
235
  def eql?(other)
173
- other.equal?(self) || (other.kind_of?(self.class) && other.to_i == to_i)
236
+ other.equal?(self) || (other.kind_of?(ULID) && other.to_i == to_i)
174
237
  end
175
238
  alias_method :==, :eql?
176
239
 
240
+ # @return [Boolean]
241
+ def ===(other)
242
+ case other
243
+ when ULID
244
+ self == other
245
+ when String
246
+ begin
247
+ self == self.class.parse(other)
248
+ rescue Exception
249
+ false
250
+ end
251
+ else
252
+ false
253
+ end
254
+ end
255
+
177
256
  # @return [Time]
178
257
  def to_time
179
- @time ||= Time.at(0, @milliseconds, :millisecond).utc
258
+ @time ||= Time.at(0, @milliseconds, :millisecond).utc.freeze
180
259
  end
181
260
 
182
- # @return [Array<Integer>]
261
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
183
262
  def octets
184
- @octets ||= (time_octets + randomness_octets).freeze
263
+ @octets ||= (timestamp_octets + randomness_octets).freeze
185
264
  end
186
265
 
187
- # @return [Array<Integer>]
188
- def time_octets
189
- @time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
266
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer)]
267
+ def timestamp_octets
268
+ @timestamp_octets ||= self.class.octets_from_integer(@milliseconds, length: TIMESTAMP_OCTETS_LENGTH).freeze
190
269
  end
191
270
 
192
- # @return [Array<Integer>]
271
+ # @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
193
272
  def randomness_octets
194
- @randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
273
+ @randomness_octets ||= self.class.octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
195
274
  end
196
275
 
197
- # @return [ULID]
276
+ # @return [String]
277
+ def timestamp
278
+ @timestamp ||= matchdata[:timestamp].freeze
279
+ end
280
+
281
+ # @return [String]
282
+ def randomness
283
+ @randomness ||= matchdata[:randomness].freeze
284
+ end
285
+
286
+ # @return [Regexp]
287
+ def pattern
288
+ @pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
289
+ end
290
+
291
+ # @return [Regexp]
292
+ def strict_pattern
293
+ @strict_pattern ||= /\A#{pattern.source}\z/i.freeze
294
+ end
295
+
296
+ # @return [ULID, nil] when called on ULID as `7ZZZZZZZZZZZZZZZZZZZZZZZZZ`, returns `nil` instead of ULID
198
297
  def next
199
- @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
298
+ next_int = to_i.next
299
+ return nil if next_int > MAX_INTEGER
300
+ @next ||= self.class.from_integer(next_int)
200
301
  end
201
302
  alias_method :succ, :next
202
303
 
304
+ # @return [ULID, nil] when called on ULID as `00000000000000000000000000`, returns `nil` instead of ULID
305
+ def pred
306
+ pre_int = to_i.pred
307
+ return nil if pre_int.negative?
308
+ @pred ||= self.class.from_integer(pre_int)
309
+ end
310
+
203
311
  # @return [self]
204
312
  def freeze
205
313
  # Evaluate all caching
206
314
  inspect
207
315
  octets
208
- succ
209
316
  to_i
317
+ succ
318
+ pred
319
+ strict_pattern
210
320
  super
211
321
  end
212
322
 
213
323
  private
214
324
 
215
- # @param [Integer] integer
216
- # @param [Integer] length
217
- # @return [Array<Integer>]
218
- def octets_from_integer(integer, length:)
219
- digits = integer.digits(256)
220
- (length - digits.size).times do
221
- digits.push 0
222
- end
223
- digits.reverse!
325
+ # @return [MatchData]
326
+ def matchdata
327
+ @matchdata ||= STRICT_PATTERN.match(to_str).freeze
224
328
  end
329
+ end
225
330
 
226
- # @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
227
- # @param [Array<Integer>] reversed_digits
228
- # @return [Integer]
229
- def inverse_of_digits(reversed_digits)
230
- base = 256
231
- num = 0
232
- reversed_digits.each do |digit|
233
- num = (num * base) + digit
234
- end
235
- num
236
- end
331
+ require_relative 'ulid/version'
332
+ require_relative 'ulid/monotonic_generator'
333
+
334
+ class ULID
335
+ MONOTONIC_GENERATOR = MonotonicGenerator.new
336
+
337
+ private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
237
338
  end
@@ -0,0 +1,44 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ class ULID
6
+ class MonotonicGenerator
7
+ attr_accessor :latest_milliseconds, :latest_entropy
8
+
9
+ def initialize
10
+ reset
11
+ end
12
+
13
+ # @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
14
+ # @return [ULID]
15
+ def generate
16
+ milliseconds = ULID.current_milliseconds
17
+ reasonable_entropy = ULID.reasonable_entropy
18
+
19
+ @latest_milliseconds ||= milliseconds
20
+ @latest_entropy ||= reasonable_entropy
21
+ if @latest_milliseconds != milliseconds
22
+ @latest_milliseconds = milliseconds
23
+ @latest_entropy = reasonable_entropy
24
+ else
25
+ @latest_entropy += 1
26
+ end
27
+
28
+ ULID.new milliseconds: milliseconds, entropy: @latest_entropy
29
+ end
30
+
31
+ # @return [self]
32
+ def reset
33
+ @latest_milliseconds = nil
34
+ @latest_entropy = nil
35
+ self
36
+ end
37
+
38
+ # @raise [TypeError] always raises exception and does not freeze self
39
+ # @return [void]
40
+ def freeze
41
+ raise TypeError, "cannot freeze #{self.class}"
42
+ end
43
+ end
44
+ 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.6'
5
+ VERSION = '0.0.11'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -2,55 +2,21 @@
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
-
25
- def self.generate: (?moment: Time | Integer, ?entropy: Integer) -> ULID
26
- def self.monotonic_generate: -> ULID
27
- def self.current_milliseconds: -> Integer
28
- def self.time_to_milliseconds: (Time time) -> Integer
29
- def self.reasonable_entropy: -> Integer
30
- def self.parse: (String string) -> ULID
31
- def self.valid?: (untyped string) -> bool
32
- attr_reader milliseconds: Integer
33
- attr_reader entropy: Integer
34
- def initialize: (milliseconds: Integer, entropy: Integer) -> void
35
- def to_str: -> String
36
- def to_s: -> String
37
- def to_i: -> Integer
38
- def hash: -> Integer
39
- def <=>: (untyped other) -> Integer?
40
- def inspect: -> String
41
- def eql?: (untyped other) -> bool
42
- def ==: (untyped other) -> bool
43
- def to_time: -> Time
44
- def octets: -> Array[Integer]
45
- def time_octets: -> Array[Integer]
46
- def randomness_octets: -> Array[Integer]
47
- def next: -> ULID
48
- def succ: -> ULID
49
- def freeze: -> self
50
-
51
- private
52
- def octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
53
- def inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
54
20
 
55
21
  class Error < StandardError
56
22
  end
@@ -62,8 +28,6 @@ class ULID
62
28
  end
63
29
 
64
30
  class MonotonicGenerator
65
- include Singleton
66
-
67
31
  attr_accessor latest_milliseconds: Integer?
68
32
  attr_accessor latest_entropy: Integer?
69
33
  def initialize: -> void
@@ -71,4 +35,69 @@ class ULID
71
35
  def reset: -> void
72
36
  def freeze: -> void
73
37
  end
38
+
39
+ type moment = Time | Integer
40
+ type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
41
+ type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
42
+ type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
43
+
44
+ @milliseconds: Integer
45
+ @entropy: Integer
46
+ @string: String?
47
+ @integer: Integer?
48
+ @octets: octets?
49
+ @timestamp_octets: timestamp_octets?
50
+ @randomness_octets: randomness_octets?
51
+ @timestamp: String?
52
+ @randomness: String?
53
+ @inspect: String?
54
+ @time: Time?
55
+ @next: ULID?
56
+ @pattern: Regexp?
57
+ @strict_pattern: Regexp?
58
+ @matchdata: MatchData?
59
+
60
+ def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
61
+ def self.monotonic_generate: -> ULID
62
+ def self.current_milliseconds: -> Integer
63
+ def self.time_to_milliseconds: (Time time) -> Integer
64
+ def self.reasonable_entropy: -> Integer
65
+ def self.parse: (String string) -> ULID
66
+ def self.from_uuidv4: (String uuid) -> ULID
67
+ def self.from_integer: (Integer integer) -> ULID
68
+ def self.min: (?moment: moment) -> ULID
69
+ def self.max: (?moment: moment) -> ULID
70
+ def self.valid?: (untyped string) -> bool
71
+ def self.scan: (String string) -> Enumerator[ULID, singleton(ULID)]
72
+ | (String string) { (ULID ulid) -> void } -> singleton(ULID)
73
+ def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
74
+ def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
75
+ attr_reader milliseconds: Integer
76
+ attr_reader entropy: Integer
77
+ def initialize: (milliseconds: Integer, entropy: Integer) -> void
78
+ def to_str: -> String
79
+ alias to_s to_str
80
+ def to_i: -> Integer
81
+ alias hash to_i
82
+ def <=>: (ULID other) -> Integer
83
+ | (untyped other) -> Integer?
84
+ def inspect: -> String
85
+ def eql?: (untyped other) -> bool
86
+ alias == eql?
87
+ def ===: (untyped other) -> bool
88
+ def to_time: -> Time
89
+ def timestamp: -> String
90
+ def randomness: -> String
91
+ def pattern: -> Regexp
92
+ def strict_pattern: -> Regexp
93
+ def octets: -> octets
94
+ def timestamp_octets: -> timestamp_octets
95
+ def randomness_octets: -> randomness_octets
96
+ def next: -> ULID?
97
+ alias succ next
98
+ def pred: -> ULID?
99
+ def freeze: -> self
100
+
101
+ private
102
+ def matchdata: -> MatchData
74
103
  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.6
4
+ version: 0.0.11
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-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: integer-base
@@ -17,33 +17,19 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 0.1.2
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: 0.1.2
27
- - !ruby/object:Gem::Dependency
28
- name: test-unit
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 3.4.1
34
20
  - - "<"
35
21
  - !ruby/object:Gem::Version
36
- version: '4'
37
- type: :development
22
+ version: 0.2.0
23
+ type: :runtime
38
24
  prerelease: false
39
25
  version_requirements: !ruby/object:Gem::Requirement
40
26
  requirements:
41
27
  - - ">="
42
28
  - !ruby/object:Gem::Version
43
- version: 3.4.1
29
+ version: 0.1.2
44
30
  - - "<"
45
31
  - !ruby/object:Gem::Version
46
- version: '4'
32
+ version: 0.2.0
47
33
  - !ruby/object:Gem::Dependency
48
34
  name: benchmark-ips
49
35
  requirement: !ruby/object:Gem::Requirement
@@ -84,18 +70,21 @@ dependencies:
84
70
  - - "<"
85
71
  - !ruby/object:Gem::Version
86
72
  version: '2'
87
- description: |2
88
- ULID(Universally Unique Lexicographically Sortable Identifier) is defined on https://github.com/ulid/spec.
89
- It has useful specs for actual applications.
90
- This gem aims to provide the generator, monotonic generator, parser and handy manipulation methods for the ID.
91
- 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"
92
77
  email:
93
78
  - kachick1+ruby@gmail.com
94
79
  executables: []
95
80
  extensions: []
96
81
  extra_rdoc_files: []
97
82
  files:
83
+ - LICENSE
84
+ - README.md
85
+ - Steepfile
98
86
  - lib/ulid.rb
87
+ - lib/ulid/monotonic_generator.rb
99
88
  - lib/ulid/version.rb
100
89
  - sig/ulid.rbs
101
90
  homepage: https://github.com/kachick/ruby-ulid