vers 1.3.0 → 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 +13 -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 +924 -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 +81 -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.
|
|
@@ -22,15 +35,24 @@ module Vers
|
|
|
22
35
|
# Regex for parsing semantic version components including build metadata
|
|
23
36
|
SEMANTIC_VERSION_REGEX = /\A(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([^+]+))?(?:\+(.+))?\z/
|
|
24
37
|
|
|
38
|
+
# Maximum accepted length for a version string. Real-world version
|
|
39
|
+
# strings rarely exceed 100 characters; 256 leaves headroom for unusual
|
|
40
|
+
# prerelease tags while bounding regex/split work and cache key size.
|
|
41
|
+
MAX_LENGTH = 256
|
|
42
|
+
|
|
25
43
|
attr_reader :major, :minor, :patch, :prerelease, :build
|
|
26
44
|
|
|
27
45
|
##
|
|
28
46
|
# Creates a new Version object
|
|
29
47
|
#
|
|
30
48
|
# @param version_string [String] The version string to parse
|
|
49
|
+
# @raise [ArgumentError] if the version string exceeds MAX_LENGTH
|
|
31
50
|
#
|
|
32
51
|
def initialize(version_string)
|
|
33
52
|
@original = version_string.to_s
|
|
53
|
+
if @original.length > MAX_LENGTH
|
|
54
|
+
raise ArgumentError, "Version string too long (#{@original.length} > #{MAX_LENGTH})"
|
|
55
|
+
end
|
|
34
56
|
parse_version
|
|
35
57
|
end
|
|
36
58
|
|
|
@@ -41,6 +63,10 @@ module Vers
|
|
|
41
63
|
# @return [Version] Cached or new Version object
|
|
42
64
|
#
|
|
43
65
|
def self.cached_new(version_string)
|
|
66
|
+
# Skip caching for oversized keys to bound cache memory by entry
|
|
67
|
+
# count, not by attacker-controlled key length.
|
|
68
|
+
return new(version_string) if version_string.to_s.length > MAX_LENGTH
|
|
69
|
+
|
|
44
70
|
if @@version_cache.size >= @@cache_size_limit
|
|
45
71
|
# Keep the most recent half instead of clearing everything
|
|
46
72
|
keys = @@version_cache.keys
|
|
@@ -74,17 +100,30 @@ module Vers
|
|
|
74
100
|
#
|
|
75
101
|
# @param a [String] First version string
|
|
76
102
|
# @param b [String] Second version string
|
|
77
|
-
# @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)
|
|
78
104
|
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
|
79
105
|
#
|
|
80
106
|
def self.compare_with_scheme(a, b, scheme)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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)
|
|
86
125
|
else
|
|
87
|
-
|
|
126
|
+
compare_with_scheme(a, b, scheme)
|
|
88
127
|
end
|
|
89
128
|
end
|
|
90
129
|
|
|
@@ -94,22 +133,51 @@ module Vers
|
|
|
94
133
|
# @param version_string [String] The version string to normalize
|
|
95
134
|
# @return [String] The normalized version string
|
|
96
135
|
#
|
|
97
|
-
def self.normalize(version_string)
|
|
98
|
-
|
|
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
|
|
99
143
|
end
|
|
100
144
|
|
|
101
145
|
##
|
|
102
146
|
# Checks if a version string is valid
|
|
103
147
|
#
|
|
104
148
|
# @param version_string [String] The version string to validate
|
|
149
|
+
# @param scheme [String, nil] Package manager scheme or nil for generic validation
|
|
105
150
|
# @return [Boolean] true if the version is valid
|
|
106
151
|
#
|
|
107
|
-
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
|
+
|
|
108
156
|
version_string.to_s.match?(/\Av?\d+\.\d+\.\d+/)
|
|
109
157
|
end
|
|
110
158
|
|
|
111
|
-
def self.
|
|
112
|
-
|
|
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
|
+
|
|
113
181
|
version_string.to_s.sub(/\Av/, '')
|
|
114
182
|
end
|
|
115
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
|