ruby-ulid 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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: []