vers 1.3.1 → 2.0.0
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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +29 -1
- data/CONTRIBUTING.md +2 -2
- data/README.md +27 -24
- data/lib/vers/bazel_version.rb +114 -0
- data/lib/vers/composer_version.rb +117 -0
- data/lib/vers/conan_version.rb +87 -0
- data/lib/vers/constraint.rb +7 -3
- data/lib/vers/distribution_version.rb +482 -0
- data/lib/vers/gem_version.rb +81 -0
- data/lib/vers/interval.rb +49 -33
- data/lib/vers/maven_version.rb +16 -0
- data/lib/vers/nuget_version.rb +14 -0
- data/lib/vers/parser.rb +893 -202
- data/lib/vers/pub_version.rb +59 -0
- data/lib/vers/pypi_version.rb +256 -0
- data/lib/vers/scheme.rb +52 -0
- data/lib/vers/semver_version.rb +294 -0
- data/lib/vers/special_version.rb +336 -0
- data/lib/vers/version.rb +68 -13
- data/lib/vers/version_comparison.rb +26 -0
- data/lib/vers/version_range.rb +201 -48
- data/lib/vers.rb +39 -9
- metadata +15 -4
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "version_comparison"
|
|
5
|
+
|
|
6
|
+
module Vers
|
|
7
|
+
module APKVersion
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
TOKEN_INITIAL_DIGIT = 0
|
|
11
|
+
TOKEN_DIGIT = 1
|
|
12
|
+
TOKEN_LETTER = 2
|
|
13
|
+
TOKEN_SUFFIX = 3
|
|
14
|
+
TOKEN_SUFFIX_NUMBER = 4
|
|
15
|
+
TOKEN_COMMIT_HASH = 5
|
|
16
|
+
TOKEN_REVISION_NUMBER = 6
|
|
17
|
+
TOKEN_END = 7
|
|
18
|
+
TOKEN_INVALID = 8
|
|
19
|
+
SUFFIXES = {
|
|
20
|
+
"alpha" => 1,
|
|
21
|
+
"beta" => 2,
|
|
22
|
+
"pre" => 3,
|
|
23
|
+
"rc" => 4,
|
|
24
|
+
"cvs" => 6,
|
|
25
|
+
"svn" => 7,
|
|
26
|
+
"git" => 8,
|
|
27
|
+
"hg" => 9,
|
|
28
|
+
"p" => 10
|
|
29
|
+
}.freeze
|
|
30
|
+
SUFFIX_NONE = 5
|
|
31
|
+
UINT64_MASK = (1 << 64) - 1
|
|
32
|
+
|
|
33
|
+
def compare(left, right)
|
|
34
|
+
left_tokens = parse(left)
|
|
35
|
+
right_tokens = parse(right)
|
|
36
|
+
|
|
37
|
+
index = 0
|
|
38
|
+
while token_type(left_tokens, index) == token_type(right_tokens, index) && token_type(left_tokens, index) < TOKEN_END
|
|
39
|
+
comparison = compare_token(left_tokens.fetch(index), right_tokens.fetch(index))
|
|
40
|
+
return comparison unless comparison.zero?
|
|
41
|
+
|
|
42
|
+
index += 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
left_type = token_type(left_tokens, index)
|
|
46
|
+
right_type = token_type(right_tokens, index)
|
|
47
|
+
return 0 if left_type == right_type
|
|
48
|
+
|
|
49
|
+
left_token = left_tokens[index]
|
|
50
|
+
right_token = right_tokens[index]
|
|
51
|
+
return -1 if left_type == TOKEN_SUFFIX && left_token.fetch(1) < SUFFIX_NONE
|
|
52
|
+
return 1 if right_type == TOKEN_SUFFIX && right_token.fetch(1) < SUFFIX_NONE
|
|
53
|
+
return -1 if left_type > right_type
|
|
54
|
+
return 1 if right_type > left_type
|
|
55
|
+
|
|
56
|
+
0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def valid?(value)
|
|
60
|
+
parse(value.to_s.strip).none? { |type, _value| type == TOKEN_INVALID }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalize(value)
|
|
64
|
+
value.to_s.strip
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def prerelease?(value)
|
|
68
|
+
tokens = parse(value.to_s.strip)
|
|
69
|
+
return false if tokens.any? { |type, _value| type == TOKEN_INVALID }
|
|
70
|
+
|
|
71
|
+
tokens.any? { |type, suffix| type == TOKEN_SUFFIX && suffix < SUFFIX_NONE }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse(value)
|
|
75
|
+
string = value.to_s.strip
|
|
76
|
+
tokens = []
|
|
77
|
+
first, index = scan_digits(string, 0)
|
|
78
|
+
return invalid_tokens(tokens) if first.empty?
|
|
79
|
+
|
|
80
|
+
tokens << [TOKEN_INITIAL_DIGIT, first]
|
|
81
|
+
previous = TOKEN_INITIAL_DIGIT
|
|
82
|
+
|
|
83
|
+
while index < string.length
|
|
84
|
+
byte = string.getbyte(index)
|
|
85
|
+
case byte
|
|
86
|
+
when 97..122
|
|
87
|
+
return invalid_tokens(tokens) if previous > TOKEN_DIGIT
|
|
88
|
+
|
|
89
|
+
tokens << [TOKEN_LETTER, byte]
|
|
90
|
+
previous = TOKEN_LETTER
|
|
91
|
+
index += 1
|
|
92
|
+
when 46
|
|
93
|
+
return invalid_tokens(tokens) if previous > TOKEN_DIGIT
|
|
94
|
+
|
|
95
|
+
digits, index = scan_digits(string, index + 1)
|
|
96
|
+
return invalid_tokens(tokens) if digits.empty?
|
|
97
|
+
|
|
98
|
+
tokens << [TOKEN_DIGIT, digits]
|
|
99
|
+
previous = TOKEN_DIGIT
|
|
100
|
+
when 48..57
|
|
101
|
+
type = if previous == TOKEN_INITIAL_DIGIT || previous == TOKEN_DIGIT
|
|
102
|
+
TOKEN_DIGIT
|
|
103
|
+
elsif previous == TOKEN_SUFFIX
|
|
104
|
+
TOKEN_SUFFIX_NUMBER
|
|
105
|
+
end
|
|
106
|
+
return invalid_tokens(tokens) unless type
|
|
107
|
+
|
|
108
|
+
digits, index = scan_digits(string, index)
|
|
109
|
+
tokens << [type, digits]
|
|
110
|
+
previous = type
|
|
111
|
+
when 95
|
|
112
|
+
return invalid_tokens(tokens) if previous > TOKEN_SUFFIX_NUMBER
|
|
113
|
+
|
|
114
|
+
suffix, index = scan_lowercase(string, index + 1)
|
|
115
|
+
rank = SUFFIXES[suffix]
|
|
116
|
+
return invalid_tokens(tokens) unless rank
|
|
117
|
+
|
|
118
|
+
tokens << [TOKEN_SUFFIX, rank]
|
|
119
|
+
previous = TOKEN_SUFFIX
|
|
120
|
+
when 126
|
|
121
|
+
return invalid_tokens(tokens) if previous >= TOKEN_COMMIT_HASH
|
|
122
|
+
|
|
123
|
+
commit, index = scan_hex(string, index + 1)
|
|
124
|
+
return invalid_tokens(tokens) if commit.empty?
|
|
125
|
+
|
|
126
|
+
tokens << [TOKEN_COMMIT_HASH, commit]
|
|
127
|
+
previous = TOKEN_COMMIT_HASH
|
|
128
|
+
when 45
|
|
129
|
+
if previous >= TOKEN_REVISION_NUMBER || string[index, 2] != "-r"
|
|
130
|
+
return invalid_tokens(tokens)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
revision, index = scan_digits(string, index + 2)
|
|
134
|
+
return invalid_tokens(tokens) if revision.empty?
|
|
135
|
+
|
|
136
|
+
tokens << [TOKEN_REVISION_NUMBER, revision]
|
|
137
|
+
previous = TOKEN_REVISION_NUMBER
|
|
138
|
+
else
|
|
139
|
+
return invalid_tokens(tokens)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
tokens
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def invalid_tokens(tokens)
|
|
147
|
+
tokens << [TOKEN_INVALID, nil]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def compare_token(left, right)
|
|
151
|
+
type = left.fetch(0)
|
|
152
|
+
left_value = left.fetch(1)
|
|
153
|
+
right_value = right.fetch(1)
|
|
154
|
+
|
|
155
|
+
case type
|
|
156
|
+
when TOKEN_DIGIT
|
|
157
|
+
return left_value <=> right_value if left_value.start_with?("0") || right_value.start_with?("0")
|
|
158
|
+
|
|
159
|
+
uint64(left_value) <=> uint64(right_value)
|
|
160
|
+
when TOKEN_INITIAL_DIGIT, TOKEN_SUFFIX_NUMBER, TOKEN_REVISION_NUMBER
|
|
161
|
+
uint64(left_value) <=> uint64(right_value)
|
|
162
|
+
else
|
|
163
|
+
left_value <=> right_value
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def token_type(tokens, index)
|
|
168
|
+
tokens[index]&.fetch(0) || TOKEN_END
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def uint64(value)
|
|
172
|
+
value.each_byte.reduce(0) { |number, byte| ((number * 10) + byte - 48) & UINT64_MASK }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def scan_digits(value, index)
|
|
176
|
+
ending = index
|
|
177
|
+
ending += 1 while ending < value.length && value.getbyte(ending).between?(48, 57)
|
|
178
|
+
[value[index...ending], ending]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def scan_lowercase(value, index)
|
|
182
|
+
ending = index
|
|
183
|
+
ending += 1 while ending < value.length && value.getbyte(ending).between?(97, 122)
|
|
184
|
+
[value[index...ending], ending]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def scan_hex(value, index)
|
|
188
|
+
ending = index
|
|
189
|
+
while ending < value.length
|
|
190
|
+
byte = value.getbyte(ending)
|
|
191
|
+
break unless byte.between?(48, 57) || byte.between?(65, 70) || byte.between?(97, 102)
|
|
192
|
+
|
|
193
|
+
ending += 1
|
|
194
|
+
end
|
|
195
|
+
[value[index...ending], ending]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
module OpenSSLVersion
|
|
200
|
+
extend self
|
|
201
|
+
|
|
202
|
+
Parsed = Data.define(:core, :patch)
|
|
203
|
+
|
|
204
|
+
def compare(left, right)
|
|
205
|
+
parsed_left = parse(left)
|
|
206
|
+
parsed_right = parse(right)
|
|
207
|
+
return Version.compare(left, right) unless parsed_left && parsed_right
|
|
208
|
+
return SemverVersion.compare(left, right) if version_three_or_newer?(parsed_left, parsed_right)
|
|
209
|
+
|
|
210
|
+
parsed_left.core.zip(parsed_right.core).each do |left_part, right_part|
|
|
211
|
+
comparison = VersionComparison.compare_numbers(left_part, right_part)
|
|
212
|
+
return comparison unless comparison.zero?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
left_prerelease = prerelease_patch?(parsed_left.patch)
|
|
216
|
+
right_prerelease = prerelease_patch?(parsed_right.patch)
|
|
217
|
+
return -1 if left_prerelease && !right_prerelease
|
|
218
|
+
return 1 if !left_prerelease && right_prerelease
|
|
219
|
+
|
|
220
|
+
parsed_left.patch <=> parsed_right.patch
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def parse(value)
|
|
224
|
+
parts = value.to_s.split(".", 3)
|
|
225
|
+
return nil unless parts.length == 3
|
|
226
|
+
return nil unless parts[0].match?(/\A\d+\z/) && parts[1].match?(/\A\d+\z/)
|
|
227
|
+
|
|
228
|
+
match = /\A(\d+)(.*)\z/.match(parts[2])
|
|
229
|
+
return nil unless match
|
|
230
|
+
|
|
231
|
+
Parsed.new([parts[0], parts[1], match[1]].freeze, match[2])
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def version_three_or_newer?(left, right)
|
|
235
|
+
VersionComparison.compare_numbers(left.core[0], "3") >= 0 &&
|
|
236
|
+
VersionComparison.compare_numbers(right.core[0], "3") >= 0
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def prerelease_patch?(patch)
|
|
240
|
+
patch.start_with?("-alpha", "-beta")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def valid?(value)
|
|
244
|
+
!parse(value.to_s.strip).nil?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def normalize(value)
|
|
248
|
+
value.to_s.strip
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def prerelease?(value)
|
|
252
|
+
parsed = parse(value.to_s.strip)
|
|
253
|
+
return false unless parsed
|
|
254
|
+
|
|
255
|
+
if VersionComparison.compare_numbers(parsed.core[0], "3") >= 0
|
|
256
|
+
parsed.patch.start_with?("-")
|
|
257
|
+
else
|
|
258
|
+
prerelease_patch?(parsed.patch)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
module DatetimeVersion
|
|
264
|
+
extend self
|
|
265
|
+
|
|
266
|
+
def compare(left, right)
|
|
267
|
+
Time.iso8601(left) <=> Time.iso8601(right)
|
|
268
|
+
rescue ArgumentError
|
|
269
|
+
left <=> right
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def valid?(value)
|
|
273
|
+
Time.iso8601(value.to_s.strip)
|
|
274
|
+
value.to_s.strip.match?(/T/) && !value.to_s.strip.match?(/[[:space:]]/)
|
|
275
|
+
rescue ArgumentError
|
|
276
|
+
false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def normalize(value)
|
|
280
|
+
value.to_s.strip
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def prerelease?(_value)
|
|
284
|
+
false
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
module IntDotVersion
|
|
289
|
+
extend self
|
|
290
|
+
|
|
291
|
+
def compare(left, right)
|
|
292
|
+
left_parts = left.to_s.split(".", -1)
|
|
293
|
+
right_parts = right.to_s.split(".", -1)
|
|
294
|
+
|
|
295
|
+
[left_parts.length, right_parts.length].max.times do |index|
|
|
296
|
+
comparison = VersionComparison.compare_numbers(left_parts[index], right_parts[index])
|
|
297
|
+
return comparison unless comparison.zero?
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
0
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def valid?(value)
|
|
304
|
+
value.to_s.strip.match?(/\A\d+(?:\.\d+)*\z/)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def normalize(value)
|
|
308
|
+
value.to_s.strip
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def prerelease?(_value)
|
|
312
|
+
false
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
module LexicographicVersion
|
|
317
|
+
extend self
|
|
318
|
+
|
|
319
|
+
def compare(left, right)
|
|
320
|
+
left <=> right
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def valid?(value)
|
|
324
|
+
string = value.to_s.strip
|
|
325
|
+
!string.empty? && !string.match?(/[[:space:]]/)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def normalize(value)
|
|
329
|
+
value.to_s.strip
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def prerelease?(_value)
|
|
333
|
+
false
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
data/lib/vers/version.rb
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "bazel_version"
|
|
4
|
+
require_relative "maven_version"
|
|
5
|
+
require_relative "nuget_version"
|
|
6
|
+
require_relative "semver_version"
|
|
7
|
+
require_relative "gem_version"
|
|
8
|
+
require_relative "pypi_version"
|
|
9
|
+
require_relative "composer_version"
|
|
10
|
+
require_relative "pub_version"
|
|
11
|
+
require_relative "distribution_version"
|
|
12
|
+
require_relative "conan_version"
|
|
13
|
+
require_relative "special_version"
|
|
14
|
+
require_relative "scheme"
|
|
15
|
+
|
|
3
16
|
module Vers
|
|
4
|
-
VERSION = "
|
|
17
|
+
VERSION = "2.0.0"
|
|
5
18
|
|
|
6
19
|
##
|
|
7
20
|
# Handles version comparison and normalization across different package ecosystems.
|
|
@@ -87,17 +100,30 @@ module Vers
|
|
|
87
100
|
#
|
|
88
101
|
# @param a [String] First version string
|
|
89
102
|
# @param b [String] Second version string
|
|
90
|
-
# @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
|
|
103
|
+
# @param scheme [String, nil] Package manager scheme (bazel, maven, nuget, or nil for generic)
|
|
91
104
|
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
|
92
105
|
#
|
|
93
106
|
def self.compare_with_scheme(a, b, scheme)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
return 0 if a == b
|
|
108
|
+
return -1 if a.nil?
|
|
109
|
+
return 1 if b.nil?
|
|
110
|
+
|
|
111
|
+
handler = Scheme.handler(scheme)
|
|
112
|
+
handler ? handler.compare(a, b) : compare(a, b)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.compare_for_range(a, b, scheme)
|
|
116
|
+
return 0 if a == b
|
|
117
|
+
return -1 if a.nil?
|
|
118
|
+
return 1 if b.nil?
|
|
119
|
+
|
|
120
|
+
case Scheme.canonical(scheme)
|
|
121
|
+
when "cargo"
|
|
122
|
+
SemverVersion.compare(a, b)
|
|
123
|
+
when "pypi"
|
|
124
|
+
PyPIVersion.compare_public(a, b)
|
|
99
125
|
else
|
|
100
|
-
|
|
126
|
+
compare_with_scheme(a, b, scheme)
|
|
101
127
|
end
|
|
102
128
|
end
|
|
103
129
|
|
|
@@ -107,22 +133,51 @@ module Vers
|
|
|
107
133
|
# @param version_string [String] The version string to normalize
|
|
108
134
|
# @return [String] The normalized version string
|
|
109
135
|
#
|
|
110
|
-
def self.normalize(version_string)
|
|
111
|
-
|
|
136
|
+
def self.normalize(version_string, scheme = nil)
|
|
137
|
+
handler = Scheme.handler(scheme)
|
|
138
|
+
return cached_new(version_string).to_s unless handler
|
|
139
|
+
|
|
140
|
+
raise ArgumentError, "Invalid #{Scheme.canonical(scheme)} version: #{version_string}" unless handler.valid?(version_string)
|
|
141
|
+
|
|
142
|
+
handler.respond_to?(:normalize) ? handler.normalize(version_string) : version_string.to_s.strip
|
|
112
143
|
end
|
|
113
144
|
|
|
114
145
|
##
|
|
115
146
|
# Checks if a version string is valid
|
|
116
147
|
#
|
|
117
148
|
# @param version_string [String] The version string to validate
|
|
149
|
+
# @param scheme [String, nil] Package manager scheme or nil for generic validation
|
|
118
150
|
# @return [Boolean] true if the version is valid
|
|
119
151
|
#
|
|
120
|
-
def self.valid?(version_string)
|
|
152
|
+
def self.valid?(version_string, scheme = nil)
|
|
153
|
+
handler = Scheme.handler(scheme)
|
|
154
|
+
return handler.valid?(version_string) if handler
|
|
155
|
+
|
|
121
156
|
version_string.to_s.match?(/\Av?\d+\.\d+\.\d+/)
|
|
122
157
|
end
|
|
123
158
|
|
|
124
|
-
def self.
|
|
125
|
-
|
|
159
|
+
def self.stable?(version_string, scheme = nil)
|
|
160
|
+
handler = Scheme.handler(scheme)
|
|
161
|
+
return handler.valid?(version_string) && !handler.prerelease?(version_string) if handler
|
|
162
|
+
|
|
163
|
+
cached_new(version_string).stable?
|
|
164
|
+
rescue ArgumentError
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.prerelease?(version_string, scheme = nil)
|
|
169
|
+
handler = Scheme.handler(scheme)
|
|
170
|
+
return handler.valid?(version_string) && handler.prerelease?(version_string) if handler
|
|
171
|
+
|
|
172
|
+
cached_new(version_string).prerelease?
|
|
173
|
+
rescue ArgumentError
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.clean(version_string, scheme = nil)
|
|
178
|
+
return nil unless valid?(version_string, scheme)
|
|
179
|
+
return normalize(version_string, scheme) if Scheme.handler(scheme)
|
|
180
|
+
|
|
126
181
|
version_string.to_s.sub(/\Av/, '')
|
|
127
182
|
end
|
|
128
183
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vers
|
|
4
|
+
module VersionComparison
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def compare_numbers(left, right)
|
|
8
|
+
normalized_left = normalize_number(left)
|
|
9
|
+
normalized_right = normalize_number(right)
|
|
10
|
+
|
|
11
|
+
length_comparison = normalized_left.length <=> normalized_right.length
|
|
12
|
+
return length_comparison unless length_comparison.zero?
|
|
13
|
+
|
|
14
|
+
normalized_left <=> normalized_right
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def normalize_number(value)
|
|
18
|
+
normalized = value.to_s.sub(/\A0+/, "")
|
|
19
|
+
normalized.empty? ? "0" : normalized
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def numeric?(value)
|
|
23
|
+
value.match?(/\A\d+\z/)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|