ruby-ulid 0.0.6

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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ulid/version.rb +6 -0
  3. data/lib/ulid.rb +237 -0
  4. data/sig/ulid.rbs +74 -0
  5. metadata +127 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e00e5eba399c7d96e26ada6a0f0463fdb7fc08d7ec69c96588a5f85320a997be
4
+ data.tar.gz: e5f79b9a86ab038be2d0b39a787d8e70108998d3a024c699e401770ec6edade3
5
+ SHA512:
6
+ metadata.gz: 0b4221414e2cdeeb67f6f6173c5ce25475f388e11df12c16864f576d76cac1fe3197dc5c2dd6ddef03d4fdfe6c079e0256a91c7b1728af73dd9d1b56c6ca7109
7
+ data.tar.gz: 369838ea1deac94dc44c71a37a606eb57a88f0266910f555a3ede1b7d85653be564499f7bf009cdd426a4a84a4be7e4d0f046ca4106bf25e8613cede927a47c5
@@ -0,0 +1,6 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+
4
+ class ULID
5
+ VERSION = '0.0.6'
6
+ end
data/lib/ulid.rb ADDED
@@ -0,0 +1,237 @@
1
+ # coding: us-ascii
2
+ # frozen_string_literal: true
3
+ # Copyright (C) 2021 Kenichi Kamiya
4
+
5
+ require 'securerandom'
6
+ require 'singleton'
7
+ require 'integer/base'
8
+ require_relative 'ulid/version'
9
+
10
+ # @see https://github.com/ulid/spec
11
+ class ULID
12
+ include Comparable
13
+
14
+ class Error < StandardError; end
15
+ class OverflowError < Error; end
16
+ class ParserError < Error; end
17
+
18
+ # Crockford's Base32. Excluded I, L, O, U.
19
+ # @see https://www.crockford.com/base32.html
20
+ ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.chars.map(&:freeze).freeze
21
+
22
+ TIME_PART_LENGTH = 10
23
+ RANDOMNESS_PART_LENGTH = 16
24
+ ENCODED_ID_LENGTH = TIME_PART_LENGTH + RANDOMNESS_PART_LENGTH
25
+ TIME_OCTETS_LENGTH = 6
26
+ RANDOMNESS_OCTETS_LENGTH = 10
27
+ OCTETS_LENGTH = TIME_OCTETS_LENGTH + RANDOMNESS_OCTETS_LENGTH
28
+ MAX_MILLISECONDS = 281474976710655
29
+ MAX_ENTROPY = 1208925819614629174706175
30
+
31
+ # Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
32
+ # @see https://bugs.ruby-lang.org/issues/15958
33
+ TIME_FORMAT_IN_INSPECT = '%Y-%m-%d %H:%M:%S.%3N %Z'
34
+
35
+ class MonotonicGenerator
36
+ include Singleton
37
+
38
+ attr_accessor :latest_milliseconds, :latest_entropy
39
+
40
+ def initialize
41
+ reset
42
+ end
43
+
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
57
+
58
+ ULID.new milliseconds: milliseconds, entropy: @latest_entropy
59
+ end
60
+
61
+ # @return [self]
62
+ def reset
63
+ @latest_milliseconds = nil
64
+ @latest_entropy = nil
65
+ self
66
+ end
67
+
68
+ # @return [void]
69
+ def freeze
70
+ raise TypeError, "cannot freeze #{self.class}"
71
+ end
72
+ end
73
+
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
80
+ # @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
84
+ end
85
+
86
+ # @return [ULID]
87
+ def self.monotonic_generate
88
+ MONOTONIC_GENERATOR.generate
89
+ end
90
+
91
+ # @return [Integer]
92
+ def self.current_milliseconds
93
+ time_to_milliseconds(Time.now)
94
+ end
95
+
96
+ # @param [Time] time
97
+ # @return [Integer]
98
+ def self.time_to_milliseconds(time)
99
+ (time.to_r * 1000).to_i
100
+ end
101
+
102
+ # @return [Integer]
103
+ def self.reasonable_entropy
104
+ SecureRandom.random_number(MAX_ENTROPY)
105
+ end
106
+
107
+ # @param [String, #to_str] string
108
+ # @return [ULID]
109
+ def self.parse(string)
110
+ begin
111
+ string = string.to_str
112
+ unless string.size == ENCODED_ID_LENGTH
113
+ raise "parsable string must be #{ENCODED_ID_LENGTH} characters, but actually given #{string.size} characters"
114
+ end
115
+ timestamp = string.slice(0, TIME_PART_LENGTH)
116
+ randomness = string.slice(TIME_PART_LENGTH, RANDOMNESS_PART_LENGTH)
117
+ milliseconds = Integer::Base.parse(timestamp, ENCODING_CHARS)
118
+ entropy = Integer::Base.parse(randomness, ENCODING_CHARS)
119
+ rescue => err
120
+ raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
121
+ end
122
+
123
+ new milliseconds: milliseconds, entropy: entropy
124
+ end
125
+
126
+ # @param [String] string
127
+ # @return [Boolean]
128
+ def self.valid?(string)
129
+ parse(string)
130
+ rescue Exception
131
+ false
132
+ else
133
+ true
134
+ end
135
+
136
+ attr_reader :milliseconds, :entropy
137
+
138
+ def initialize(milliseconds:, entropy:)
139
+ milliseconds = milliseconds.to_int
140
+ entropy = entropy.to_int
141
+ raise OverflowError, "timestamp overflow: given #{milliseconds}, max: #{MAX_MILLISECONDS}" unless milliseconds <= MAX_MILLISECONDS
142
+ raise OverflowError, "entropy overflow: given #{entropy}, max: #{MAX_ENTROPY}" unless entropy <= MAX_ENTROPY
143
+ raise ArgumentError, 'milliseconds and entropy should not be negative' if milliseconds.negative? || entropy.negative?
144
+
145
+ @milliseconds = milliseconds
146
+ @entropy = entropy
147
+ end
148
+
149
+ # @return [String]
150
+ def to_str
151
+ @string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
152
+ end
153
+ alias_method :to_s, :to_str
154
+
155
+ # @return [Integer]
156
+ def to_i
157
+ @integer ||= inverse_of_digits(octets)
158
+ end
159
+ alias_method :hash, :to_i
160
+
161
+ # @return [Integer, nil]
162
+ def <=>(other)
163
+ other.kind_of?(self.class) ? (to_i <=> other.to_i) : nil
164
+ end
165
+
166
+ # @return [String]
167
+ def inspect
168
+ @inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_str})".freeze
169
+ end
170
+
171
+ # @return [Boolean]
172
+ def eql?(other)
173
+ other.equal?(self) || (other.kind_of?(self.class) && other.to_i == to_i)
174
+ end
175
+ alias_method :==, :eql?
176
+
177
+ # @return [Time]
178
+ def to_time
179
+ @time ||= Time.at(0, @milliseconds, :millisecond).utc
180
+ end
181
+
182
+ # @return [Array<Integer>]
183
+ def octets
184
+ @octets ||= (time_octets + randomness_octets).freeze
185
+ end
186
+
187
+ # @return [Array<Integer>]
188
+ def time_octets
189
+ @time_octets ||= octets_from_integer(@milliseconds, length: TIME_OCTETS_LENGTH).freeze
190
+ end
191
+
192
+ # @return [Array<Integer>]
193
+ def randomness_octets
194
+ @randomness_octets ||= octets_from_integer(@entropy, length: RANDOMNESS_OCTETS_LENGTH).freeze
195
+ end
196
+
197
+ # @return [ULID]
198
+ def next
199
+ @next ||= self.class.new(milliseconds: @milliseconds, entropy: @entropy + 1)
200
+ end
201
+ alias_method :succ, :next
202
+
203
+ # @return [self]
204
+ def freeze
205
+ # Evaluate all caching
206
+ inspect
207
+ octets
208
+ succ
209
+ to_i
210
+ super
211
+ end
212
+
213
+ private
214
+
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!
224
+ end
225
+
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
237
+ end
data/sig/ulid.rbs ADDED
@@ -0,0 +1,74 @@
1
+ # Classes
2
+ class ULID
3
+ VERSION: String
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
14
+ MONOTONIC_GENERATOR: MonotonicGenerator
15
+ 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
+
55
+ class Error < StandardError
56
+ end
57
+
58
+ class OverflowError < Error
59
+ end
60
+
61
+ class ParserError < Error
62
+ end
63
+
64
+ class MonotonicGenerator
65
+ include Singleton
66
+
67
+ attr_accessor latest_milliseconds: Integer?
68
+ attr_accessor latest_entropy: Integer?
69
+ def initialize: -> void
70
+ def generate: -> ULID
71
+ def reset: -> void
72
+ def freeze: -> void
73
+ end
74
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-ulid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Kenichi Kamiya
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: integer-base
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
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
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '4'
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.4.1
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '4'
47
+ - !ruby/object:Gem::Dependency
48
+ name: benchmark-ips
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.8.4
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '3'
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.8.4
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '3'
67
+ - !ruby/object:Gem::Dependency
68
+ name: yard
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 0.9.26
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '2'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.9.26
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ 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.
92
+ email:
93
+ - kachick1+ruby@gmail.com
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files: []
97
+ files:
98
+ - lib/ulid.rb
99
+ - lib/ulid/version.rb
100
+ - sig/ulid.rbs
101
+ homepage: https://github.com/kachick/ruby-ulid
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ documentation_uri: https://kachick.github.io/ruby-ulid/
106
+ homepage_uri: https://github.com/kachick/ruby-ulid
107
+ source_code_uri: https://github.com/kachick/ruby-ulid
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '2.5'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.2.15
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: A handy ULID library
127
+ test_files: []