vers 1.0.3 → 1.1.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/CHANGELOG.md +11 -1
- data/lib/vers/constraint.rb +6 -6
- data/lib/vers/interval.rb +26 -18
- data/lib/vers/maven_version.rb +218 -0
- data/lib/vers/nuget_version.rb +79 -0
- data/lib/vers/parser.rb +191 -37
- data/lib/vers/version.rb +24 -2
- data/lib/vers/version_range.rb +92 -42
- data/lib/vers.rb +14 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b7aa0b15219b0734ba33605567d7f6283c51b06afeb66d65f6b6893ada38b95
|
|
4
|
+
data.tar.gz: 79f6860358cba90b4d744af1af5c4198328f14a1bd555373018c3943b591c1ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27d9b7fc130fc8a70f738114c5566d4cefbe092259c9a181d006322aecf6f2dc656474c99ee610b2ffc5fe58a9b84410c2b322813c213f63473d929e21286e4d
|
|
7
|
+
data.tar.gz: 58035172c00bbbd2d8e8e9a96aa22bbae5eff4f0de8c8e2dbfe4c5340cc4267c0e28c8766705d527a0b387a788dd12841bad6a31ebb399c4f4a0663975f641c0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-02-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Scheme-specific version comparison for Maven and NuGet via `Version.compare_with_scheme` and `Vers.compare_with_scheme`
|
|
14
|
+
- `Vers::MavenVersion` module with Maven qualifier ordering (alpha < beta < milestone < rc < snapshot < release < sp), digit/letter transitions, sublist rules, qualifier aliases, and trailing zero normalization
|
|
15
|
+
- `Vers::NuGetVersion` module with 4-part numeric versions, case-insensitive prereleases, and build metadata stripping
|
|
16
|
+
- `scheme` parameter threaded through `Interval`, `VersionRange`, `Constraint`, and `Parser` so Maven and NuGet ranges use their own comparison rules in `contains?`, `intersect`, and other interval operations
|
|
17
|
+
- 1010 conformance test cases from the vers-spec test suite for Maven and NuGet version comparison
|
|
18
|
+
|
|
10
19
|
## [1.0.3] - 2026-01-09
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -117,7 +126,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
117
126
|
- **No runtime dependencies** - pure Ruby implementation
|
|
118
127
|
- **Minitest** for testing (development dependency only)
|
|
119
128
|
|
|
120
|
-
[Unreleased]: https://github.com/andrew/vers/compare/v1.0
|
|
129
|
+
[Unreleased]: https://github.com/andrew/vers/compare/v1.1.0...HEAD
|
|
130
|
+
[1.1.0]: https://github.com/andrew/vers/compare/v1.0.3...v1.1.0
|
|
121
131
|
[1.0.3]: https://github.com/andrew/vers/compare/v1.0.2...v1.0.3
|
|
122
132
|
[1.0.2]: https://github.com/andrew/vers/compare/v1.0.1...v1.0.2
|
|
123
133
|
[1.0.1]: https://github.com/andrew/vers/compare/v1.0.0...v1.0.1
|
data/lib/vers/constraint.rb
CHANGED
|
@@ -93,21 +93,21 @@ module Vers
|
|
|
93
93
|
# Vers::Constraint.new(">=", "1.2.3").to_interval # => [1.2.3,+∞)
|
|
94
94
|
# Vers::Constraint.new("=", "1.0.0").to_interval # => [1.0.0,1.0.0]
|
|
95
95
|
#
|
|
96
|
-
def to_interval
|
|
96
|
+
def to_interval(scheme: nil)
|
|
97
97
|
case operator
|
|
98
98
|
when "="
|
|
99
|
-
Interval.exact(version)
|
|
99
|
+
Interval.exact(version, scheme: scheme)
|
|
100
100
|
when "!="
|
|
101
101
|
# != constraints need special handling in ranges - they create exclusions
|
|
102
102
|
nil
|
|
103
103
|
when ">"
|
|
104
|
-
Interval.greater_than(version, inclusive: false)
|
|
104
|
+
Interval.greater_than(version, inclusive: false, scheme: scheme)
|
|
105
105
|
when ">="
|
|
106
|
-
Interval.greater_than(version, inclusive: true)
|
|
106
|
+
Interval.greater_than(version, inclusive: true, scheme: scheme)
|
|
107
107
|
when "<"
|
|
108
|
-
Interval.less_than(version, inclusive: false)
|
|
108
|
+
Interval.less_than(version, inclusive: false, scheme: scheme)
|
|
109
109
|
when "<="
|
|
110
|
-
Interval.less_than(version, inclusive: true)
|
|
110
|
+
Interval.less_than(version, inclusive: true, scheme: scheme)
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
data/lib/vers/interval.rb
CHANGED
|
@@ -4,35 +4,36 @@ require_relative 'version'
|
|
|
4
4
|
|
|
5
5
|
module Vers
|
|
6
6
|
class Interval
|
|
7
|
-
attr_reader :min, :max, :min_inclusive, :max_inclusive
|
|
7
|
+
attr_reader :min, :max, :min_inclusive, :max_inclusive, :scheme
|
|
8
8
|
|
|
9
|
-
def initialize(min: nil, max: nil, min_inclusive: true, max_inclusive: true)
|
|
9
|
+
def initialize(min: nil, max: nil, min_inclusive: true, max_inclusive: true, scheme: nil)
|
|
10
10
|
@min = min
|
|
11
11
|
@max = max
|
|
12
12
|
@min_inclusive = min_inclusive
|
|
13
13
|
@max_inclusive = max_inclusive
|
|
14
|
+
@scheme = scheme
|
|
14
15
|
|
|
15
16
|
validate_bounds!
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def self.empty
|
|
19
|
-
new(min: "1", max: "0", min_inclusive: true, max_inclusive: true)
|
|
19
|
+
def self.empty(scheme: nil)
|
|
20
|
+
new(min: "1", max: "0", min_inclusive: true, max_inclusive: true, scheme: scheme)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
def self.unbounded
|
|
23
|
-
new
|
|
23
|
+
def self.unbounded(scheme: nil)
|
|
24
|
+
new(scheme: scheme)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def self.exact(version)
|
|
27
|
-
new(min: version, max: version, min_inclusive: true, max_inclusive: true)
|
|
27
|
+
def self.exact(version, scheme: nil)
|
|
28
|
+
new(min: version, max: version, min_inclusive: true, max_inclusive: true, scheme: scheme)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
def self.greater_than(version, inclusive: false)
|
|
31
|
-
new(min: version, min_inclusive: inclusive)
|
|
31
|
+
def self.greater_than(version, inclusive: false, scheme: nil)
|
|
32
|
+
new(min: version, min_inclusive: inclusive, scheme: scheme)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def self.less_than(version, inclusive: false)
|
|
35
|
-
new(max: version, max_inclusive: inclusive)
|
|
35
|
+
def self.less_than(version, inclusive: false, scheme: nil)
|
|
36
|
+
new(max: version, max_inclusive: inclusive, scheme: scheme)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def empty?
|
|
@@ -59,7 +60,8 @@ module Vers
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def intersect(other)
|
|
62
|
-
|
|
63
|
+
merged_scheme = @scheme || other.scheme
|
|
64
|
+
return self.class.empty(scheme: merged_scheme) if empty? || other.empty?
|
|
63
65
|
|
|
64
66
|
new_min = nil
|
|
65
67
|
new_min_inclusive = true
|
|
@@ -110,7 +112,8 @@ module Vers
|
|
|
110
112
|
min: new_min,
|
|
111
113
|
max: new_max,
|
|
112
114
|
min_inclusive: new_min_inclusive,
|
|
113
|
-
max_inclusive: new_max_inclusive
|
|
115
|
+
max_inclusive: new_max_inclusive,
|
|
116
|
+
scheme: merged_scheme
|
|
114
117
|
)
|
|
115
118
|
end
|
|
116
119
|
|
|
@@ -120,6 +123,7 @@ module Vers
|
|
|
120
123
|
|
|
121
124
|
return nil unless overlaps?(other) || adjacent?(other)
|
|
122
125
|
|
|
126
|
+
merged_scheme = @scheme || other.scheme
|
|
123
127
|
new_min = nil
|
|
124
128
|
new_min_inclusive = true
|
|
125
129
|
new_max = nil
|
|
@@ -169,7 +173,8 @@ module Vers
|
|
|
169
173
|
min: new_min,
|
|
170
174
|
max: new_max,
|
|
171
175
|
min_inclusive: new_min_inclusive,
|
|
172
|
-
max_inclusive: new_max_inclusive
|
|
176
|
+
max_inclusive: new_max_inclusive,
|
|
177
|
+
scheme: merged_scheme
|
|
173
178
|
)
|
|
174
179
|
end
|
|
175
180
|
|
|
@@ -221,9 +226,12 @@ module Vers
|
|
|
221
226
|
return 0 if a == b
|
|
222
227
|
return -1 if a.nil?
|
|
223
228
|
return 1 if b.nil?
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
|
|
230
|
+
if @scheme
|
|
231
|
+
Version.compare_with_scheme(a, b, @scheme)
|
|
232
|
+
else
|
|
233
|
+
Version.compare(a, b)
|
|
234
|
+
end
|
|
227
235
|
end
|
|
228
236
|
end
|
|
229
237
|
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vers
|
|
4
|
+
module MavenVersion
|
|
5
|
+
MavenComponent = Struct.new(:is_numeric, :numeric, :qualifier, :is_null, :after_dash, keyword_init: true) do
|
|
6
|
+
def initialize(is_numeric: false, numeric: 0, qualifier: "", is_null: false, after_dash: false)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
QUALIFIER_ORDER = {
|
|
12
|
+
"alpha" => 1,
|
|
13
|
+
"beta" => 2,
|
|
14
|
+
"milestone" => 3,
|
|
15
|
+
"rc" => 4,
|
|
16
|
+
"snapshot" => 5,
|
|
17
|
+
"" => 6,
|
|
18
|
+
"sp" => 7
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
UNKNOWN_QUALIFIER_ORDER = 8
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def compare(a, b)
|
|
26
|
+
return 0 if a == b
|
|
27
|
+
|
|
28
|
+
parts_a = parse_maven_version(a)
|
|
29
|
+
parts_b = parse_maven_version(b)
|
|
30
|
+
|
|
31
|
+
max_len = [parts_a.length, parts_b.length].max
|
|
32
|
+
|
|
33
|
+
max_len.times do |i|
|
|
34
|
+
comp_a = i < parts_a.length ? parts_a[i] : MavenComponent.new(is_null: true)
|
|
35
|
+
comp_b = i < parts_b.length ? parts_b[i] : MavenComponent.new(is_null: true)
|
|
36
|
+
|
|
37
|
+
cmp = compare_components(comp_a, comp_b)
|
|
38
|
+
return cmp unless cmp == 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_maven_version(s)
|
|
45
|
+
s = s.downcase
|
|
46
|
+
|
|
47
|
+
parts, after_dash_flags = split_with_separators(s)
|
|
48
|
+
|
|
49
|
+
result = []
|
|
50
|
+
parts.each_with_index do |part, i|
|
|
51
|
+
next if part.empty?
|
|
52
|
+
|
|
53
|
+
next_is_digit = if i + 1 < parts.length
|
|
54
|
+
parts[i + 1].match?(/\A\d+\z/)
|
|
55
|
+
else
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
normalized = normalize_qualifier(part, next_is_digit)
|
|
60
|
+
next if normalized.empty?
|
|
61
|
+
|
|
62
|
+
after_dash = i < after_dash_flags.length ? after_dash_flags[i] : false
|
|
63
|
+
|
|
64
|
+
if normalized.match?(/\A\d+\z/)
|
|
65
|
+
result << MavenComponent.new(is_numeric: true, numeric: normalized.to_i, after_dash: after_dash)
|
|
66
|
+
else
|
|
67
|
+
result << MavenComponent.new(qualifier: normalized, after_dash: after_dash)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
normalize_components(result)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def split_with_separators(s)
|
|
75
|
+
parts = []
|
|
76
|
+
after_dash = []
|
|
77
|
+
current = +""
|
|
78
|
+
last_was_digit = false
|
|
79
|
+
first_char = true
|
|
80
|
+
current_after_dash = false
|
|
81
|
+
|
|
82
|
+
s.each_char do |c|
|
|
83
|
+
if c == "." || c == "-"
|
|
84
|
+
if current.length > 0
|
|
85
|
+
parts << current
|
|
86
|
+
after_dash << current_after_dash
|
|
87
|
+
current = +""
|
|
88
|
+
end
|
|
89
|
+
current_after_dash = (c == "-")
|
|
90
|
+
first_char = true
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
is_digit = c >= "0" && c <= "9"
|
|
95
|
+
|
|
96
|
+
if !first_char && is_digit != last_was_digit && current.length > 0
|
|
97
|
+
parts << current
|
|
98
|
+
after_dash << current_after_dash
|
|
99
|
+
current = +""
|
|
100
|
+
current_after_dash = true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
current << c
|
|
104
|
+
last_was_digit = is_digit
|
|
105
|
+
first_char = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if current.length > 0
|
|
109
|
+
parts << current
|
|
110
|
+
after_dash << current_after_dash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
[parts, after_dash]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def normalize_qualifier(q, next_is_digit)
|
|
117
|
+
if next_is_digit && q.length == 1
|
|
118
|
+
case q
|
|
119
|
+
when "a" then return "alpha"
|
|
120
|
+
when "b" then return "beta"
|
|
121
|
+
when "m" then return "milestone"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
case q
|
|
126
|
+
when "cr" then "rc"
|
|
127
|
+
when "ga", "final", "release" then ""
|
|
128
|
+
else q
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalize_components(components)
|
|
133
|
+
return components if components.empty?
|
|
134
|
+
|
|
135
|
+
first_sublist_idx = components.index { |c| c.after_dash }
|
|
136
|
+
|
|
137
|
+
if first_sublist_idx && first_sublist_idx > 0
|
|
138
|
+
base_end = first_sublist_idx
|
|
139
|
+
while base_end > 1 && components[base_end - 1].is_numeric && components[base_end - 1].numeric == 0
|
|
140
|
+
base_end -= 1
|
|
141
|
+
end
|
|
142
|
+
if base_end < first_sublist_idx
|
|
143
|
+
components = components[0...base_end] + components[first_sublist_idx..]
|
|
144
|
+
end
|
|
145
|
+
elsif first_sublist_idx.nil?
|
|
146
|
+
while components.length > 0 && components.last.is_numeric && components.last.numeric == 0
|
|
147
|
+
components.pop
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
components
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def compare_components(a, b)
|
|
155
|
+
return 0 if a.is_null && b.is_null
|
|
156
|
+
|
|
157
|
+
if a.is_null
|
|
158
|
+
return compare_to_null(b) * -1
|
|
159
|
+
end
|
|
160
|
+
if b.is_null
|
|
161
|
+
return compare_to_null(a)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if a.after_dash != b.after_dash
|
|
165
|
+
if a.after_dash
|
|
166
|
+
return b.is_numeric ? -1 : 1
|
|
167
|
+
else
|
|
168
|
+
return a.is_numeric ? 1 : -1
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if a.is_numeric && b.is_numeric
|
|
173
|
+
return a.numeric <=> b.numeric
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if a.is_numeric && !b.is_numeric
|
|
177
|
+
return 1
|
|
178
|
+
end
|
|
179
|
+
if !a.is_numeric && b.is_numeric
|
|
180
|
+
return -1
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
order_a = qualifier_order(a.qualifier)
|
|
184
|
+
order_b = qualifier_order(b.qualifier)
|
|
185
|
+
|
|
186
|
+
if order_a != order_b
|
|
187
|
+
return order_a <=> order_b
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
known_a = QUALIFIER_ORDER.key?(a.qualifier)
|
|
191
|
+
known_b = QUALIFIER_ORDER.key?(b.qualifier)
|
|
192
|
+
|
|
193
|
+
if !known_a && !known_b
|
|
194
|
+
return a.qualifier <=> b.qualifier
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def compare_to_null(comp)
|
|
201
|
+
if comp.is_numeric
|
|
202
|
+
if comp.numeric == 0
|
|
203
|
+
0
|
|
204
|
+
else
|
|
205
|
+
1
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
order_comp = qualifier_order(comp.qualifier)
|
|
209
|
+
order_null = QUALIFIER_ORDER[""]
|
|
210
|
+
order_comp <=> order_null
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def qualifier_order(q)
|
|
215
|
+
QUALIFIER_ORDER.fetch(q, UNKNOWN_QUALIFIER_ORDER)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vers
|
|
4
|
+
module NuGetVersion
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def compare(a, b)
|
|
8
|
+
return 0 if a == b
|
|
9
|
+
|
|
10
|
+
parts_a = parse_nuget(a)
|
|
11
|
+
parts_b = parse_nuget(b)
|
|
12
|
+
|
|
13
|
+
4.times do |i|
|
|
14
|
+
cmp = parts_a[:numeric][i] <=> parts_b[:numeric][i]
|
|
15
|
+
return cmp unless cmp == 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
pre_a = parts_a[:prerelease]
|
|
19
|
+
pre_b = parts_b[:prerelease]
|
|
20
|
+
|
|
21
|
+
return 1 if pre_a.empty? && !pre_b.empty?
|
|
22
|
+
return -1 if !pre_a.empty? && pre_b.empty?
|
|
23
|
+
return 0 if pre_a.empty? && pre_b.empty?
|
|
24
|
+
|
|
25
|
+
compare_prerelease(pre_a, pre_b)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_nuget(s)
|
|
29
|
+
s = s.dup
|
|
30
|
+
|
|
31
|
+
if (idx = s.index("+"))
|
|
32
|
+
s = s[0...idx]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
prerelease = ""
|
|
36
|
+
if (idx = s.index("-"))
|
|
37
|
+
prerelease = s[(idx + 1)..]
|
|
38
|
+
s = s[0...idx]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
numeric = [0, 0, 0, 0]
|
|
42
|
+
parts = s.split(".")
|
|
43
|
+
parts.each_with_index do |part, i|
|
|
44
|
+
break if i >= 4
|
|
45
|
+
numeric[i] = part.to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
{ numeric: numeric, prerelease: prerelease }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compare_prerelease(a, b)
|
|
52
|
+
parts_a = a.downcase.split(".")
|
|
53
|
+
parts_b = b.downcase.split(".")
|
|
54
|
+
|
|
55
|
+
max_len = [parts_a.length, parts_b.length].max
|
|
56
|
+
|
|
57
|
+
max_len.times do |i|
|
|
58
|
+
part_a = i < parts_a.length ? parts_a[i] : nil
|
|
59
|
+
part_b = i < parts_b.length ? parts_b[i] : nil
|
|
60
|
+
|
|
61
|
+
return -1 if part_a.nil?
|
|
62
|
+
return 1 if part_b.nil?
|
|
63
|
+
|
|
64
|
+
num_a = part_a.match?(/\A\d+\z/) ? part_a.to_i : nil
|
|
65
|
+
num_b = part_b.match?(/\A\d+\z/) ? part_b.to_i : nil
|
|
66
|
+
|
|
67
|
+
if num_a && num_b
|
|
68
|
+
cmp = num_a <=> num_b
|
|
69
|
+
return cmp unless cmp == 0
|
|
70
|
+
else
|
|
71
|
+
cmp = part_a <=> part_b
|
|
72
|
+
return cmp unless cmp == 0
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
0
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/vers/parser.rb
CHANGED
|
@@ -83,8 +83,14 @@ module Vers
|
|
|
83
83
|
parse_pypi_range(range_string)
|
|
84
84
|
when "maven"
|
|
85
85
|
parse_maven_range(range_string)
|
|
86
|
+
when "cargo"
|
|
87
|
+
parse_npm_range(range_string)
|
|
86
88
|
when "nuget"
|
|
87
89
|
parse_nuget_range(range_string)
|
|
90
|
+
when "hex", "elixir"
|
|
91
|
+
parse_hex_range(range_string)
|
|
92
|
+
when "go", "golang"
|
|
93
|
+
parse_go_range(range_string)
|
|
88
94
|
when "deb", "debian"
|
|
89
95
|
parse_debian_range(range_string)
|
|
90
96
|
when "rpm"
|
|
@@ -106,9 +112,10 @@ module Vers
|
|
|
106
112
|
return "*" if version_range.unbounded?
|
|
107
113
|
return "vers:#{scheme}/" if version_range.empty?
|
|
108
114
|
|
|
115
|
+
intervals = version_range.raw_constraints || version_range.intervals
|
|
109
116
|
constraints = []
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
|
|
118
|
+
intervals.each do |interval|
|
|
112
119
|
if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
|
|
113
120
|
# Exact version
|
|
114
121
|
constraints << "=#{interval.min}"
|
|
@@ -118,7 +125,7 @@ module Vers
|
|
|
118
125
|
operator = interval.min_inclusive ? ">=" : ">"
|
|
119
126
|
constraints << "#{operator}#{interval.min}"
|
|
120
127
|
end
|
|
121
|
-
|
|
128
|
+
|
|
122
129
|
if interval.max
|
|
123
130
|
operator = interval.max_inclusive ? "<=" : "<"
|
|
124
131
|
constraints << "#{operator}#{interval.max}"
|
|
@@ -126,30 +133,40 @@ module Vers
|
|
|
126
133
|
end
|
|
127
134
|
end
|
|
128
135
|
|
|
136
|
+
constraints.sort_by! { |c| sort_key_for_constraint(c) }
|
|
137
|
+
constraints.uniq!
|
|
138
|
+
|
|
129
139
|
"vers:#{scheme}/#{constraints.join('|')}"
|
|
130
140
|
end
|
|
131
141
|
|
|
132
142
|
private
|
|
133
143
|
|
|
144
|
+
def sort_key_for_constraint(constraint)
|
|
145
|
+
version = constraint.sub(/\A[><=!]+/, '')
|
|
146
|
+
v = Version.cached_new(version)
|
|
147
|
+
[v.major || 0, v.minor || 0, v.patch || 0, constraint]
|
|
148
|
+
end
|
|
149
|
+
|
|
134
150
|
def parse_constraints(constraints_string, scheme)
|
|
135
|
-
constraint_strings = constraints_string.split(
|
|
151
|
+
constraint_strings = constraints_string.split(/[|,]/)
|
|
136
152
|
intervals = []
|
|
137
153
|
exclusions = []
|
|
154
|
+
interval_scheme = %w[maven nuget].include?(scheme) ? scheme : nil
|
|
138
155
|
|
|
139
156
|
constraint_strings.each do |constraint_string|
|
|
140
157
|
constraint = Constraint.parse(constraint_string.strip)
|
|
141
|
-
|
|
158
|
+
|
|
142
159
|
if constraint.exclusion?
|
|
143
160
|
exclusions << constraint.version
|
|
144
161
|
else
|
|
145
|
-
interval = constraint.to_interval
|
|
162
|
+
interval = constraint.to_interval(scheme: interval_scheme)
|
|
146
163
|
intervals << interval if interval
|
|
147
164
|
end
|
|
148
165
|
end
|
|
149
166
|
|
|
150
167
|
# Start with the union of all positive constraints
|
|
151
|
-
range = VersionRange.new(intervals)
|
|
152
|
-
|
|
168
|
+
range = VersionRange.new(intervals, scheme: interval_scheme)
|
|
169
|
+
|
|
153
170
|
# Apply exclusions
|
|
154
171
|
exclusions.each do |version|
|
|
155
172
|
range = range.exclude(version)
|
|
@@ -179,7 +196,16 @@ module Vers
|
|
|
179
196
|
|
|
180
197
|
# Handle space-separated AND constraints
|
|
181
198
|
and_parts = range_string.split(/\s+/).reject(&:empty?)
|
|
182
|
-
|
|
199
|
+
# Re-join bare operators with their version
|
|
200
|
+
merged = []
|
|
201
|
+
and_parts.each do |part|
|
|
202
|
+
if merged.last&.match?(/\A(>=|<=|!=|[<>=~^])\z/)
|
|
203
|
+
merged[-1] = "#{merged.last}#{part}"
|
|
204
|
+
else
|
|
205
|
+
merged << part
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
ranges = merged.map { |part| parse_npm_single_range(part) }
|
|
183
209
|
ranges.reduce { |acc, range| acc.intersect(range) }
|
|
184
210
|
end
|
|
185
211
|
|
|
@@ -323,70 +349,70 @@ module Vers
|
|
|
323
349
|
raise ArgumentError, "Malformed Maven range: mismatched brackets in '#{range_string}'"
|
|
324
350
|
end
|
|
325
351
|
end
|
|
326
|
-
|
|
352
|
+
|
|
327
353
|
case range_string
|
|
328
354
|
when /^\[([^,]+),([^,]+)\]$/
|
|
329
355
|
# [1.0,2.0] := >=1.0 <=2.0
|
|
330
356
|
min_version = Regexp.last_match(1).strip
|
|
331
357
|
max_version = Regexp.last_match(2).strip
|
|
332
358
|
VersionRange.new([
|
|
333
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true)
|
|
334
|
-
])
|
|
359
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true, scheme: "maven")
|
|
360
|
+
], scheme: "maven")
|
|
335
361
|
when /^\(([^,]+),([^,]+)\)$/
|
|
336
362
|
# (1.0,2.0) := >1.0 <2.0
|
|
337
363
|
min_version = Regexp.last_match(1).strip
|
|
338
364
|
max_version = Regexp.last_match(2).strip
|
|
339
365
|
VersionRange.new([
|
|
340
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false)
|
|
341
|
-
])
|
|
366
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false, scheme: "maven")
|
|
367
|
+
], scheme: "maven")
|
|
342
368
|
when /^\[([^,]+),([^,]+)\)$/
|
|
343
369
|
# [1.0,2.0) := >=1.0 <2.0
|
|
344
370
|
min_version = Regexp.last_match(1).strip
|
|
345
371
|
max_version = Regexp.last_match(2).strip
|
|
346
372
|
VersionRange.new([
|
|
347
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false)
|
|
348
|
-
])
|
|
373
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false, scheme: "maven")
|
|
374
|
+
], scheme: "maven")
|
|
349
375
|
when /^\(([^,]+),([^,]+)\]$/
|
|
350
376
|
# (1.0,2.0] := >1.0 <=2.0
|
|
351
377
|
min_version = Regexp.last_match(1).strip
|
|
352
378
|
max_version = Regexp.last_match(2).strip
|
|
353
379
|
VersionRange.new([
|
|
354
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true)
|
|
355
|
-
])
|
|
380
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true, scheme: "maven")
|
|
381
|
+
], scheme: "maven")
|
|
356
382
|
when /^\[([^,]+)\]$/
|
|
357
383
|
# [1.0] := exactly 1.0
|
|
358
384
|
version = Regexp.last_match(1).strip
|
|
359
|
-
VersionRange.exact(version)
|
|
385
|
+
VersionRange.exact(version, scheme: "maven")
|
|
360
386
|
when /^\[([^,]+),\)$/
|
|
361
387
|
# [1.0,) := >=1.0
|
|
362
388
|
min_version = Regexp.last_match(1).strip
|
|
363
389
|
VersionRange.new([
|
|
364
|
-
Interval.new(min: min_version, min_inclusive: true)
|
|
365
|
-
])
|
|
390
|
+
Interval.new(min: min_version, min_inclusive: true, scheme: "maven")
|
|
391
|
+
], scheme: "maven")
|
|
366
392
|
when /^\(([^,]+),\)$/
|
|
367
393
|
# (1.0,) := >1.0
|
|
368
394
|
min_version = Regexp.last_match(1).strip
|
|
369
395
|
VersionRange.new([
|
|
370
|
-
Interval.new(min: min_version, min_inclusive: false)
|
|
371
|
-
])
|
|
396
|
+
Interval.new(min: min_version, min_inclusive: false, scheme: "maven")
|
|
397
|
+
], scheme: "maven")
|
|
372
398
|
when /^\(,([^,]+)\]$/
|
|
373
399
|
# (,1.0] := <=1.0
|
|
374
400
|
max_version = Regexp.last_match(1).strip
|
|
375
401
|
VersionRange.new([
|
|
376
|
-
Interval.new(max: max_version, max_inclusive: true)
|
|
377
|
-
])
|
|
402
|
+
Interval.new(max: max_version, max_inclusive: true, scheme: "maven")
|
|
403
|
+
], scheme: "maven")
|
|
378
404
|
when /^\(,([^,]+)\)$/
|
|
379
405
|
# (,1.0) := <1.0
|
|
380
406
|
max_version = Regexp.last_match(1).strip
|
|
381
407
|
VersionRange.new([
|
|
382
|
-
Interval.new(max: max_version, max_inclusive: false)
|
|
383
|
-
])
|
|
408
|
+
Interval.new(max: max_version, max_inclusive: false, scheme: "maven")
|
|
409
|
+
], scheme: "maven")
|
|
384
410
|
when /^[0-9]/
|
|
385
411
|
# Simple version number without brackets - in Maven, this is minimum version
|
|
386
412
|
if range_string.match(/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9.-]+)?$/)
|
|
387
413
|
VersionRange.new([
|
|
388
|
-
Interval.new(min: range_string, min_inclusive: true)
|
|
389
|
-
])
|
|
414
|
+
Interval.new(min: range_string, min_inclusive: true, scheme: "maven")
|
|
415
|
+
], scheme: "maven")
|
|
390
416
|
else
|
|
391
417
|
parse_constraints(range_string, 'maven')
|
|
392
418
|
end
|
|
@@ -400,7 +426,7 @@ module Vers
|
|
|
400
426
|
# Find all individual ranges by splitting on comma between brackets
|
|
401
427
|
individual_ranges = []
|
|
402
428
|
remaining = range_string.strip
|
|
403
|
-
|
|
429
|
+
|
|
404
430
|
while remaining.length > 0
|
|
405
431
|
# Find the next complete bracket range
|
|
406
432
|
if match = remaining.match(/^[\[\(][^\[\]\(\)]*[\]\)]/)
|
|
@@ -412,7 +438,7 @@ module Vers
|
|
|
412
438
|
break
|
|
413
439
|
end
|
|
414
440
|
end
|
|
415
|
-
|
|
441
|
+
|
|
416
442
|
if individual_ranges.length > 1
|
|
417
443
|
individual_ranges.each do |range_part|
|
|
418
444
|
begin
|
|
@@ -422,13 +448,13 @@ module Vers
|
|
|
422
448
|
# If parsing fails, skip this part
|
|
423
449
|
end
|
|
424
450
|
end
|
|
425
|
-
|
|
451
|
+
|
|
426
452
|
if ranges.any?
|
|
427
453
|
return ranges.reduce { |acc, range| acc.union(range) }
|
|
428
454
|
end
|
|
429
455
|
end
|
|
430
456
|
end
|
|
431
|
-
|
|
457
|
+
|
|
432
458
|
# Fall back to standard constraint parsing
|
|
433
459
|
parse_constraints(range_string, 'maven')
|
|
434
460
|
else
|
|
@@ -443,19 +469,147 @@ module Vers
|
|
|
443
469
|
# But simple version strings like "1.0" are minimum versions, not exact
|
|
444
470
|
case range_string
|
|
445
471
|
when /^[\[\(].+[\]\)]$/
|
|
446
|
-
#
|
|
447
|
-
|
|
472
|
+
# Parse bracket notation like Maven but with nuget scheme
|
|
473
|
+
range = parse_nuget_bracket_range(range_string)
|
|
474
|
+
range
|
|
448
475
|
when /^[0-9]/
|
|
449
476
|
# Simple version number - treat as minimum version for NuGet
|
|
450
477
|
VersionRange.new([
|
|
451
|
-
Interval.new(min: range_string, min_inclusive: true)
|
|
452
|
-
])
|
|
478
|
+
Interval.new(min: range_string, min_inclusive: true, scheme: "nuget")
|
|
479
|
+
], scheme: "nuget")
|
|
453
480
|
else
|
|
454
481
|
# Fall back to standard constraint parsing
|
|
455
482
|
parse_constraints(range_string, 'nuget')
|
|
456
483
|
end
|
|
457
484
|
end
|
|
458
485
|
|
|
486
|
+
def parse_nuget_bracket_range(range_string)
|
|
487
|
+
case range_string
|
|
488
|
+
when /^\[([^,]+),([^,]+)\]$/
|
|
489
|
+
min_v = Regexp.last_match(1).strip
|
|
490
|
+
max_v = Regexp.last_match(2).strip
|
|
491
|
+
VersionRange.new([
|
|
492
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: true, max_inclusive: true, scheme: "nuget")
|
|
493
|
+
], scheme: "nuget")
|
|
494
|
+
when /^\(([^,]+),([^,]+)\)$/
|
|
495
|
+
min_v = Regexp.last_match(1).strip
|
|
496
|
+
max_v = Regexp.last_match(2).strip
|
|
497
|
+
VersionRange.new([
|
|
498
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: false, max_inclusive: false, scheme: "nuget")
|
|
499
|
+
], scheme: "nuget")
|
|
500
|
+
when /^\[([^,]+),([^,]+)\)$/
|
|
501
|
+
min_v = Regexp.last_match(1).strip
|
|
502
|
+
max_v = Regexp.last_match(2).strip
|
|
503
|
+
VersionRange.new([
|
|
504
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: true, max_inclusive: false, scheme: "nuget")
|
|
505
|
+
], scheme: "nuget")
|
|
506
|
+
when /^\(([^,]+),([^,]+)\]$/
|
|
507
|
+
min_v = Regexp.last_match(1).strip
|
|
508
|
+
max_v = Regexp.last_match(2).strip
|
|
509
|
+
VersionRange.new([
|
|
510
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: false, max_inclusive: true, scheme: "nuget")
|
|
511
|
+
], scheme: "nuget")
|
|
512
|
+
when /^\[([^,]+)\]$/
|
|
513
|
+
version = Regexp.last_match(1).strip
|
|
514
|
+
VersionRange.exact(version, scheme: "nuget")
|
|
515
|
+
when /^\[([^,]+),\)$/
|
|
516
|
+
min_v = Regexp.last_match(1).strip
|
|
517
|
+
VersionRange.new([
|
|
518
|
+
Interval.new(min: min_v, min_inclusive: true, scheme: "nuget")
|
|
519
|
+
], scheme: "nuget")
|
|
520
|
+
when /^\(([^,]+),\)$/
|
|
521
|
+
min_v = Regexp.last_match(1).strip
|
|
522
|
+
VersionRange.new([
|
|
523
|
+
Interval.new(min: min_v, min_inclusive: false, scheme: "nuget")
|
|
524
|
+
], scheme: "nuget")
|
|
525
|
+
when /^\(,([^,]+)\]$/
|
|
526
|
+
max_v = Regexp.last_match(1).strip
|
|
527
|
+
VersionRange.new([
|
|
528
|
+
Interval.new(max: max_v, max_inclusive: true, scheme: "nuget")
|
|
529
|
+
], scheme: "nuget")
|
|
530
|
+
when /^\(,([^,]+)\)$/
|
|
531
|
+
max_v = Regexp.last_match(1).strip
|
|
532
|
+
VersionRange.new([
|
|
533
|
+
Interval.new(max: max_v, max_inclusive: false, scheme: "nuget")
|
|
534
|
+
], scheme: "nuget")
|
|
535
|
+
else
|
|
536
|
+
parse_constraints(range_string, 'nuget')
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Hex/Elixir range parsing
|
|
541
|
+
def parse_hex_range(range_string)
|
|
542
|
+
# Handle "or" disjunction first
|
|
543
|
+
if range_string.include?(" or ")
|
|
544
|
+
or_parts = range_string.split(" or ").map(&:strip)
|
|
545
|
+
ranges = or_parts.map { |part| parse_hex_single_range(part) }
|
|
546
|
+
return ranges.reduce { |acc, range| acc.union(range) }
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
parse_hex_single_range(range_string)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def parse_hex_single_range(range_string)
|
|
553
|
+
# Handle "and" conjunction
|
|
554
|
+
if range_string.include?(" and ")
|
|
555
|
+
and_parts = range_string.split(" and ").map(&:strip)
|
|
556
|
+
ranges = and_parts.map { |part| parse_hex_constraint(part) }
|
|
557
|
+
return ranges.reduce { |acc, range| acc.intersect(range) }
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
parse_hex_constraint(range_string)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def parse_hex_constraint(constraint_string)
|
|
564
|
+
if constraint_string.match(/^~>\s*(.+)$/)
|
|
565
|
+
parse_pessimistic_range(Regexp.last_match(1).strip)
|
|
566
|
+
else
|
|
567
|
+
# Normalize == to = for our internal constraint parsing
|
|
568
|
+
normalized = constraint_string.gsub("==", "=")
|
|
569
|
+
constraint = Constraint.parse(normalized.strip)
|
|
570
|
+
if constraint.exclusion?
|
|
571
|
+
VersionRange.unbounded.exclude(constraint.version)
|
|
572
|
+
else
|
|
573
|
+
VersionRange.new([constraint.to_interval])
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Go module range parsing (comma-separated AND constraints, v-prefix preserved)
|
|
579
|
+
def parse_go_range(range_string)
|
|
580
|
+
return VersionRange.unbounded if range_string.nil? || range_string.strip.empty?
|
|
581
|
+
|
|
582
|
+
unless range_string.include?(',')
|
|
583
|
+
return parse_constraints(range_string, 'go')
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
parts = range_string.split(',').map(&:strip)
|
|
587
|
+
constraint_intervals = []
|
|
588
|
+
exclusions = []
|
|
589
|
+
|
|
590
|
+
parts.each do |part|
|
|
591
|
+
constraint = Constraint.parse(part)
|
|
592
|
+
if constraint.exclusion?
|
|
593
|
+
exclusions << constraint.version
|
|
594
|
+
else
|
|
595
|
+
interval = constraint.to_interval
|
|
596
|
+
constraint_intervals << interval if interval
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
if constraint_intervals.any?
|
|
601
|
+
range = VersionRange.new([constraint_intervals.first])
|
|
602
|
+
constraint_intervals[1..].each do |interval|
|
|
603
|
+
range = range.intersect(VersionRange.new([interval]))
|
|
604
|
+
end
|
|
605
|
+
else
|
|
606
|
+
range = VersionRange.unbounded
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
exclusions.each { |version| range = range.exclude(version) }
|
|
610
|
+
range
|
|
611
|
+
end
|
|
612
|
+
|
|
459
613
|
# Debian range parsing
|
|
460
614
|
def parse_debian_range(range_string)
|
|
461
615
|
# Debian uses operators like >=, <=, =, >>, <<
|
data/lib/vers/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Vers
|
|
4
|
-
VERSION = "1.0
|
|
4
|
+
VERSION = "1.1.0"
|
|
5
5
|
|
|
6
6
|
##
|
|
7
7
|
# Handles version comparison and normalization across different package ecosystems.
|
|
@@ -64,10 +64,29 @@ module Vers
|
|
|
64
64
|
# Use cached versions for better performance
|
|
65
65
|
version_a = cached_new(a)
|
|
66
66
|
version_b = cached_new(b)
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
version_a <=> version_b
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
##
|
|
72
|
+
# Compares two version strings using scheme-specific rules
|
|
73
|
+
#
|
|
74
|
+
# @param a [String] First version string
|
|
75
|
+
# @param b [String] Second version string
|
|
76
|
+
# @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
|
|
77
|
+
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
|
78
|
+
#
|
|
79
|
+
def self.compare_with_scheme(a, b, scheme)
|
|
80
|
+
case scheme
|
|
81
|
+
when "maven"
|
|
82
|
+
MavenVersion.compare(a, b)
|
|
83
|
+
when "nuget"
|
|
84
|
+
NuGetVersion.compare(a, b)
|
|
85
|
+
else
|
|
86
|
+
compare(a, b)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
##
|
|
72
91
|
# Normalizes a version string to a consistent format
|
|
73
92
|
#
|
|
@@ -295,6 +314,9 @@ module Vers
|
|
|
295
314
|
private
|
|
296
315
|
|
|
297
316
|
def parse_version
|
|
317
|
+
# Strip leading v/V prefix (e.g. "v1.0.0" -> "1.0.0")
|
|
318
|
+
@original = @original.sub(/\Av/i, '')
|
|
319
|
+
|
|
298
320
|
# Handle simple numeric versions (optimized case)
|
|
299
321
|
if @original.match(/^\d+$/)
|
|
300
322
|
@major = @original.to_i
|
data/lib/vers/version_range.rb
CHANGED
|
@@ -5,31 +5,38 @@ require_relative 'version'
|
|
|
5
5
|
|
|
6
6
|
module Vers
|
|
7
7
|
class VersionRange
|
|
8
|
-
attr_reader :intervals
|
|
9
|
-
|
|
10
|
-
def initialize(intervals = [])
|
|
11
|
-
@
|
|
8
|
+
attr_reader :intervals, :raw_constraints, :scheme
|
|
9
|
+
|
|
10
|
+
def initialize(intervals = [], raw_constraints: nil, scheme: nil)
|
|
11
|
+
@scheme = scheme
|
|
12
|
+
@intervals = intervals.compact.reject(&:empty?)
|
|
13
|
+
if @scheme
|
|
14
|
+
@intervals.sort! { |a, b| compare_interval_bounds(a, b) }
|
|
15
|
+
else
|
|
16
|
+
@intervals.sort_by! { |i| [i.min || '', i.max || ''] }
|
|
17
|
+
end
|
|
18
|
+
@raw_constraints = raw_constraints
|
|
12
19
|
merge_overlapping_intervals!
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
def self.empty
|
|
16
|
-
new([])
|
|
22
|
+
def self.empty(scheme: nil)
|
|
23
|
+
new([], scheme: scheme)
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
def self.unbounded
|
|
20
|
-
new([Interval.unbounded])
|
|
26
|
+
def self.unbounded(scheme: nil)
|
|
27
|
+
new([Interval.unbounded(scheme: scheme)], scheme: scheme)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
|
-
def self.exact(version)
|
|
24
|
-
new([Interval.exact(version)])
|
|
30
|
+
def self.exact(version, scheme: nil)
|
|
31
|
+
new([Interval.exact(version, scheme: scheme)], scheme: scheme)
|
|
25
32
|
end
|
|
26
33
|
|
|
27
|
-
def self.greater_than(version, inclusive: false)
|
|
28
|
-
new([Interval.greater_than(version, inclusive: inclusive)])
|
|
34
|
+
def self.greater_than(version, inclusive: false, scheme: nil)
|
|
35
|
+
new([Interval.greater_than(version, inclusive: inclusive, scheme: scheme)], scheme: scheme)
|
|
29
36
|
end
|
|
30
37
|
|
|
31
|
-
def self.less_than(version, inclusive: false)
|
|
32
|
-
new([Interval.less_than(version, inclusive: inclusive)])
|
|
38
|
+
def self.less_than(version, inclusive: false, scheme: nil)
|
|
39
|
+
new([Interval.less_than(version, inclusive: inclusive, scheme: scheme)], scheme: scheme)
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def empty?
|
|
@@ -45,38 +52,47 @@ module Vers
|
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
def intersect(other)
|
|
55
|
+
merged_scheme = @scheme || other.scheme
|
|
48
56
|
result_intervals = []
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
intervals.each do |interval1|
|
|
51
59
|
other.intervals.each do |interval2|
|
|
52
60
|
intersection = interval1.intersect(interval2)
|
|
53
61
|
result_intervals << intersection unless intersection.empty?
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
|
|
65
|
+
combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals)
|
|
66
|
+
self.class.new(result_intervals, raw_constraints: combined_raw, scheme: merged_scheme)
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
def union(other)
|
|
61
|
-
|
|
70
|
+
merged_scheme = @scheme || other.scheme
|
|
71
|
+
combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals)
|
|
72
|
+
self.class.new(intervals + other.intervals, raw_constraints: combined_raw, scheme: merged_scheme)
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
def complement
|
|
65
|
-
return self.class.unbounded if empty?
|
|
66
|
-
return self.class.empty if unbounded?
|
|
76
|
+
return self.class.unbounded(scheme: @scheme) if empty?
|
|
77
|
+
return self.class.empty(scheme: @scheme) if unbounded?
|
|
67
78
|
|
|
68
79
|
result_intervals = []
|
|
69
|
-
|
|
70
|
-
sorted_intervals =
|
|
71
|
-
|
|
80
|
+
|
|
81
|
+
sorted_intervals = if @scheme
|
|
82
|
+
intervals.sort { |a, b| compare_interval_bounds(a, b) }
|
|
83
|
+
else
|
|
84
|
+
intervals.sort_by { |i| i.min || '' }
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
first_interval = sorted_intervals.first
|
|
73
88
|
if first_interval.min
|
|
74
89
|
result_intervals << Interval.new(
|
|
75
90
|
max: first_interval.min,
|
|
76
|
-
max_inclusive: !first_interval.min_inclusive
|
|
91
|
+
max_inclusive: !first_interval.min_inclusive,
|
|
92
|
+
scheme: @scheme
|
|
77
93
|
)
|
|
78
94
|
end
|
|
79
|
-
|
|
95
|
+
|
|
80
96
|
sorted_intervals.each_cons(2) do |curr, next_interval|
|
|
81
97
|
if curr.max && next_interval.min
|
|
82
98
|
comparison = version_compare(curr.max, next_interval.min)
|
|
@@ -85,53 +101,57 @@ module Vers
|
|
|
85
101
|
min: curr.max,
|
|
86
102
|
max: next_interval.min,
|
|
87
103
|
min_inclusive: !curr.max_inclusive,
|
|
88
|
-
max_inclusive: !next_interval.min_inclusive
|
|
104
|
+
max_inclusive: !next_interval.min_inclusive,
|
|
105
|
+
scheme: @scheme
|
|
89
106
|
)
|
|
90
107
|
end
|
|
91
108
|
end
|
|
92
109
|
end
|
|
93
|
-
|
|
110
|
+
|
|
94
111
|
last_interval = sorted_intervals.last
|
|
95
112
|
if last_interval.max
|
|
96
113
|
result_intervals << Interval.new(
|
|
97
114
|
min: last_interval.max,
|
|
98
|
-
min_inclusive: !last_interval.max_inclusive
|
|
115
|
+
min_inclusive: !last_interval.max_inclusive,
|
|
116
|
+
scheme: @scheme
|
|
99
117
|
)
|
|
100
118
|
end
|
|
101
|
-
|
|
102
|
-
self.class.new(result_intervals)
|
|
119
|
+
|
|
120
|
+
self.class.new(result_intervals, scheme: @scheme)
|
|
103
121
|
end
|
|
104
122
|
|
|
105
123
|
def exclude(version)
|
|
106
124
|
return self if !contains?(version)
|
|
107
|
-
|
|
125
|
+
|
|
108
126
|
result_intervals = []
|
|
109
|
-
|
|
127
|
+
|
|
110
128
|
intervals.each do |interval|
|
|
111
129
|
if interval.contains?(version)
|
|
112
|
-
if interval.min
|
|
130
|
+
if interval.min.nil? || version_compare(interval.min, version) < 0
|
|
113
131
|
result_intervals << Interval.new(
|
|
114
132
|
min: interval.min,
|
|
115
133
|
max: version,
|
|
116
134
|
min_inclusive: interval.min_inclusive,
|
|
117
|
-
max_inclusive: false
|
|
135
|
+
max_inclusive: false,
|
|
136
|
+
scheme: @scheme
|
|
118
137
|
)
|
|
119
138
|
end
|
|
120
|
-
|
|
121
|
-
if interval.max
|
|
139
|
+
|
|
140
|
+
if interval.max.nil? || version_compare(version, interval.max) < 0
|
|
122
141
|
result_intervals << Interval.new(
|
|
123
142
|
min: version,
|
|
124
143
|
max: interval.max,
|
|
125
144
|
min_inclusive: false,
|
|
126
|
-
max_inclusive: interval.max_inclusive
|
|
145
|
+
max_inclusive: interval.max_inclusive,
|
|
146
|
+
scheme: @scheme
|
|
127
147
|
)
|
|
128
148
|
end
|
|
129
149
|
else
|
|
130
150
|
result_intervals << interval
|
|
131
151
|
end
|
|
132
152
|
end
|
|
133
|
-
|
|
134
|
-
self.class.new(result_intervals)
|
|
153
|
+
|
|
154
|
+
self.class.new(result_intervals, raw_constraints: raw_constraints, scheme: @scheme)
|
|
135
155
|
end
|
|
136
156
|
|
|
137
157
|
def to_s
|
|
@@ -165,9 +185,39 @@ module Vers
|
|
|
165
185
|
return 0 if a == b
|
|
166
186
|
return -1 if a.nil?
|
|
167
187
|
return 1 if b.nil?
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
|
|
189
|
+
if @scheme
|
|
190
|
+
Version.compare_with_scheme(a, b, @scheme)
|
|
191
|
+
else
|
|
192
|
+
Version.compare(a, b)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def compare_interval_bounds(a, b)
|
|
197
|
+
min_a = a.min
|
|
198
|
+
min_b = b.min
|
|
199
|
+
min_cmp = if min_a.nil? && min_b.nil?
|
|
200
|
+
0
|
|
201
|
+
elsif min_a.nil?
|
|
202
|
+
-1
|
|
203
|
+
elsif min_b.nil?
|
|
204
|
+
1
|
|
205
|
+
else
|
|
206
|
+
version_compare(min_a, min_b)
|
|
207
|
+
end
|
|
208
|
+
return min_cmp unless min_cmp == 0
|
|
209
|
+
|
|
210
|
+
max_a = a.max
|
|
211
|
+
max_b = b.max
|
|
212
|
+
if max_a.nil? && max_b.nil?
|
|
213
|
+
0
|
|
214
|
+
elsif max_a.nil?
|
|
215
|
+
1
|
|
216
|
+
elsif max_b.nil?
|
|
217
|
+
-1
|
|
218
|
+
else
|
|
219
|
+
version_compare(max_a, max_b)
|
|
220
|
+
end
|
|
171
221
|
end
|
|
172
222
|
end
|
|
173
223
|
end
|
data/lib/vers.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "vers/version"
|
|
4
|
+
require_relative "vers/maven_version"
|
|
5
|
+
require_relative "vers/nuget_version"
|
|
4
6
|
require_relative "vers/interval"
|
|
5
7
|
require_relative "vers/version_range"
|
|
6
8
|
require_relative "vers/constraint"
|
|
@@ -143,6 +145,18 @@ module Vers
|
|
|
143
145
|
Version.compare(a, b)
|
|
144
146
|
end
|
|
145
147
|
|
|
148
|
+
##
|
|
149
|
+
# Compares two version strings using scheme-specific rules
|
|
150
|
+
#
|
|
151
|
+
# @param a [String] First version string
|
|
152
|
+
# @param b [String] Second version string
|
|
153
|
+
# @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
|
|
154
|
+
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
|
155
|
+
#
|
|
156
|
+
def self.compare_with_scheme(a, b, scheme)
|
|
157
|
+
Version.compare_with_scheme(a, b, scheme)
|
|
158
|
+
end
|
|
159
|
+
|
|
146
160
|
##
|
|
147
161
|
# Normalizes a version string to a consistent format
|
|
148
162
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -30,6 +30,8 @@ files:
|
|
|
30
30
|
- lib/vers.rb
|
|
31
31
|
- lib/vers/constraint.rb
|
|
32
32
|
- lib/vers/interval.rb
|
|
33
|
+
- lib/vers/maven_version.rb
|
|
34
|
+
- lib/vers/nuget_version.rb
|
|
33
35
|
- lib/vers/parser.rb
|
|
34
36
|
- lib/vers/version.rb
|
|
35
37
|
- lib/vers/version_range.rb
|