vers 1.0.3 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b753f78521c853cb7c511190af2d9722a20a1d5f503295da5b922ab020950c60
4
- data.tar.gz: 5a4d226e512a2f71b7634e946272c79d2558db811d8a734d258e2a48663648a3
3
+ metadata.gz: 8ad669b5ca951e8a64ddd205d39e083b3a9836a67ca9ff25d99872a57658a560
4
+ data.tar.gz: d1aefb9f16a5a842ad05d4e427b538a565240cf3c21969542bf614199e8e1955
5
5
  SHA512:
6
- metadata.gz: ff9ae01cbf359c8b76bb10e7e2741c2d70d119039076b520dfa4c3b2e40c022c96b23b72b2251c2d00005d0ae0c81caacc125903506c97f807e7b980a8d07dc6
7
- data.tar.gz: 949060b4a56d8bc53b9ab42ec65eb98afd1fb50ba17108abd57fd98e9d5fa5abd299931b006a341ed813ac4df7d644222aa98205dd209ca30c7fa2f01df74e76
6
+ metadata.gz: 6b87591c17a303bf820c2667f25ac802569173737c8bd63d13c86bc5aedf6e606cc50d3906c08d526dd8c9e934559f0cdc7717e8e6c4cfbd8ae5473ebc53bb9c
7
+ data.tar.gz: 9a6cafa4929f8a71b699d6ce1171e574bb6c99aca17b89850e7bf232d1269727d00f6a15f1b9a870f8319913e77b0ec865fc5633866161cb989425c9eefc76ce
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "test/vers-spec"]
2
+ path = test/vers-spec
3
+ url = https://github.com/package-url/vers-spec.git
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.3...HEAD
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
@@ -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
- return self.class.empty if empty? || other.empty?
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
- # Use the Version class for comparison
226
- Version.compare(a, b)
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"
@@ -103,22 +109,33 @@ module Vers
103
109
  # @return [String] The vers URI string
104
110
  #
105
111
  def to_vers_string(version_range, scheme)
106
- return "*" if version_range.unbounded?
112
+ return "vers:#{scheme}/*" 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
- version_range.intervals.each do |interval|
117
+
118
+ # Detect != pattern: two intervals (-∞,V) ∪ (V,+∞)
119
+ if intervals.length == 2
120
+ a, b = intervals
121
+ if a.min.nil? && !a.max_inclusive && b.max.nil? && !b.min_inclusive && a.max == b.min
122
+ constraints << "!=#{a.max}"
123
+ constraints.sort_by! { |c| sort_key_for_constraint(c) }
124
+ return "vers:#{scheme}/#{constraints.join('|')}"
125
+ end
126
+ end
127
+
128
+ intervals.each do |interval|
112
129
  if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
113
130
  # Exact version
114
- constraints << "=#{interval.min}"
131
+ constraints << interval.min.to_s
115
132
  else
116
133
  # Range constraints
117
134
  if interval.min
118
135
  operator = interval.min_inclusive ? ">=" : ">"
119
136
  constraints << "#{operator}#{interval.min}"
120
137
  end
121
-
138
+
122
139
  if interval.max
123
140
  operator = interval.max_inclusive ? "<=" : "<"
124
141
  constraints << "#{operator}#{interval.max}"
@@ -126,30 +143,45 @@ module Vers
126
143
  end
127
144
  end
128
145
 
146
+ constraints.sort_by! { |c| sort_key_for_constraint(c) }
147
+
129
148
  "vers:#{scheme}/#{constraints.join('|')}"
130
149
  end
131
150
 
132
151
  private
133
152
 
153
+ def sort_key_for_constraint(constraint)
154
+ version = constraint.sub(/\A[><=!]+/, '')
155
+ v = Version.cached_new(version)
156
+ [v, constraint]
157
+ end
158
+
134
159
  def parse_constraints(constraints_string, scheme)
135
- constraint_strings = constraints_string.split('|')
160
+ constraint_strings = constraints_string.split(/[|,]/)
136
161
  intervals = []
137
162
  exclusions = []
163
+ interval_scheme = %w[maven nuget].include?(scheme) ? scheme : nil
138
164
 
139
165
  constraint_strings.each do |constraint_string|
140
166
  constraint = Constraint.parse(constraint_string.strip)
141
-
167
+
142
168
  if constraint.exclusion?
143
169
  exclusions << constraint.version
144
170
  else
145
- interval = constraint.to_interval
171
+ interval = constraint.to_interval(scheme: interval_scheme)
146
172
  intervals << interval if interval
147
173
  end
148
174
  end
149
175
 
150
- # Start with the union of all positive constraints
151
- range = VersionRange.new(intervals)
152
-
176
+ # Start with the union of all positive constraints, or unbounded if only exclusions
177
+ range = if intervals.any?
178
+ VersionRange.new(intervals, scheme: interval_scheme)
179
+ elsif exclusions.any?
180
+ VersionRange.unbounded
181
+ else
182
+ VersionRange.new([], scheme: interval_scheme)
183
+ end
184
+
153
185
  # Apply exclusions
154
186
  exclusions.each do |version|
155
187
  range = range.exclude(version)
@@ -168,7 +200,7 @@ module Vers
168
200
  # Handle || (OR) operator
169
201
  if range_string.include?('||')
170
202
  or_parts = range_string.split('||').map(&:strip)
171
- ranges = or_parts.map { |part| parse_npm_single_range(part) }
203
+ ranges = or_parts.map { |part| parse_npm_range(part) }
172
204
  return ranges.reduce { |acc, range| acc.union(range) }
173
205
  end
174
206
 
@@ -179,8 +211,23 @@ module Vers
179
211
 
180
212
  # Handle space-separated AND constraints
181
213
  and_parts = range_string.split(/\s+/).reject(&:empty?)
182
- ranges = and_parts.map { |part| parse_npm_single_range(part) }
183
- ranges.reduce { |acc, range| acc.intersect(range) }
214
+ # Re-join bare operators with their version
215
+ merged = []
216
+ and_parts.each do |part|
217
+ if merged.last&.match?(/\A(>=|<=|!=|[<>=~^])\z/)
218
+ merged[-1] = "#{merged.last}#{part}"
219
+ else
220
+ merged << part
221
+ end
222
+ end
223
+ ranges = merged.map { |part| parse_npm_single_range(part) }
224
+ # If all parts are bare versions (no operators), treat as union
225
+ all_exact = merged.all? { |part| part.match?(/\A\d/) }
226
+ if all_exact
227
+ ranges.reduce { |acc, range| acc.union(range) }
228
+ else
229
+ ranges.reduce { |acc, range| acc.intersect(range) }
230
+ end
184
231
  end
185
232
 
186
233
  def parse_npm_single_range(range_string)
@@ -230,8 +277,25 @@ module Vers
230
277
  # Invalid patterns that should raise errors
231
278
  raise ArgumentError, "Invalid NPM range format: #{range_string}"
232
279
  else
280
+ # Check for operator + x-range (e.g. ">=2.2.x", ">=1.x")
281
+ if range_string.match(/\A[><=]+(\d+)\.[xX*]\z/)
282
+ major = $1.to_i
283
+ return VersionRange.new([
284
+ Interval.new(min: "#{major}.0.0", max: "#{major + 1}.0.0", min_inclusive: true, max_inclusive: false)
285
+ ])
286
+ end
287
+ if range_string.match(/\A[><=]+(\d+)\.(\d+)\.[xX*]\z/)
288
+ major = $1.to_i
289
+ minor = $2.to_i
290
+ return VersionRange.new([
291
+ Interval.new(min: "#{major}.#{minor}.0", max: "#{major}.#{minor + 1}.0", min_inclusive: true, max_inclusive: false)
292
+ ])
293
+ end
233
294
  # Standard constraint
234
295
  constraint = Constraint.parse(range_string)
296
+ # Normalize version to semver (npm always uses 3 segments)
297
+ normalized_version = Version.cached_new(constraint.version).to_s
298
+ constraint = Constraint.new(constraint.operator, normalized_version)
235
299
  if constraint.exclusion?
236
300
  VersionRange.unbounded.exclude(constraint.version)
237
301
  else
@@ -263,9 +327,27 @@ module Vers
263
327
 
264
328
  def parse_tilde_range(version)
265
329
  v = Version.cached_new(version)
266
- upper_version = if v.minor
330
+
331
+ if v.prerelease
332
+ # ~0.8.0-pre := >=0.8.0-pre <0.8.0 OR >=0.8.0 <0.8.1
333
+ # Prereleases only match their own major.minor.patch
334
+ base = "#{v.major}.#{v.minor || 0}.#{v.patch || 0}"
335
+ next_patch = "#{v.major}.#{v.minor || 0}.#{(v.patch || 0) + 1}"
336
+ pre_range = VersionRange.new([
337
+ Interval.new(min: version, max: base, min_inclusive: true, max_inclusive: false)
338
+ ])
339
+ release_range = VersionRange.new([
340
+ Interval.new(min: base, max: next_patch, min_inclusive: true, max_inclusive: false)
341
+ ])
342
+ return pre_range.union(release_range)
343
+ end
344
+
345
+ upper_version = if v.patch
267
346
  # ~1.2.3 := >=1.2.3 <1.3.0
268
347
  "#{v.major}.#{v.minor + 1}.0"
348
+ elsif v.minor
349
+ # ~1.2 := >=1.2.0 <1.3.0
350
+ "#{v.major}.#{v.minor + 1}.0"
269
351
  else
270
352
  # ~1 := >=1.0.0 <2.0.0
271
353
  "#{v.major + 1}.0.0"
@@ -292,14 +374,14 @@ module Vers
292
374
  def parse_pessimistic_range(version)
293
375
  v = Version.cached_new(version)
294
376
  upper_version = if v.patch
295
- # ~> 1.2.3 := >= 1.2.3, < 1.3.0
296
- "#{v.major}.#{v.minor + 1}.0"
377
+ # ~> 1.2.3 := >= 1.2.3, < 1.3
378
+ "#{v.major}.#{v.minor + 1}"
297
379
  elsif v.minor
298
- # ~> 1.2 := >= 1.2.0, < 2.0.0
299
- "#{v.major + 1}.0.0"
380
+ # ~> 1.2 := >= 1.2.0, < 2
381
+ "#{v.major + 1}"
300
382
  else
301
- # ~> 1 := >= 1.0.0, < 2.0.0
302
- "#{v.major + 1}.0.0"
383
+ # ~> 1 := >= 1.0.0, < 2
384
+ "#{v.major + 1}"
303
385
  end
304
386
 
305
387
  VersionRange.new([
@@ -323,70 +405,70 @@ module Vers
323
405
  raise ArgumentError, "Malformed Maven range: mismatched brackets in '#{range_string}'"
324
406
  end
325
407
  end
326
-
408
+
327
409
  case range_string
328
410
  when /^\[([^,]+),([^,]+)\]$/
329
411
  # [1.0,2.0] := >=1.0 <=2.0
330
412
  min_version = Regexp.last_match(1).strip
331
413
  max_version = Regexp.last_match(2).strip
332
414
  VersionRange.new([
333
- Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true)
334
- ])
415
+ Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true, scheme: "maven")
416
+ ], scheme: "maven")
335
417
  when /^\(([^,]+),([^,]+)\)$/
336
418
  # (1.0,2.0) := >1.0 <2.0
337
419
  min_version = Regexp.last_match(1).strip
338
420
  max_version = Regexp.last_match(2).strip
339
421
  VersionRange.new([
340
- Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false)
341
- ])
422
+ Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false, scheme: "maven")
423
+ ], scheme: "maven")
342
424
  when /^\[([^,]+),([^,]+)\)$/
343
425
  # [1.0,2.0) := >=1.0 <2.0
344
426
  min_version = Regexp.last_match(1).strip
345
427
  max_version = Regexp.last_match(2).strip
346
428
  VersionRange.new([
347
- Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false)
348
- ])
429
+ Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false, scheme: "maven")
430
+ ], scheme: "maven")
349
431
  when /^\(([^,]+),([^,]+)\]$/
350
432
  # (1.0,2.0] := >1.0 <=2.0
351
433
  min_version = Regexp.last_match(1).strip
352
434
  max_version = Regexp.last_match(2).strip
353
435
  VersionRange.new([
354
- Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true)
355
- ])
436
+ Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true, scheme: "maven")
437
+ ], scheme: "maven")
356
438
  when /^\[([^,]+)\]$/
357
439
  # [1.0] := exactly 1.0
358
440
  version = Regexp.last_match(1).strip
359
- VersionRange.exact(version)
441
+ VersionRange.exact(version, scheme: "maven")
360
442
  when /^\[([^,]+),\)$/
361
443
  # [1.0,) := >=1.0
362
444
  min_version = Regexp.last_match(1).strip
363
445
  VersionRange.new([
364
- Interval.new(min: min_version, min_inclusive: true)
365
- ])
446
+ Interval.new(min: min_version, min_inclusive: true, scheme: "maven")
447
+ ], scheme: "maven")
366
448
  when /^\(([^,]+),\)$/
367
449
  # (1.0,) := >1.0
368
450
  min_version = Regexp.last_match(1).strip
369
451
  VersionRange.new([
370
- Interval.new(min: min_version, min_inclusive: false)
371
- ])
452
+ Interval.new(min: min_version, min_inclusive: false, scheme: "maven")
453
+ ], scheme: "maven")
372
454
  when /^\(,([^,]+)\]$/
373
455
  # (,1.0] := <=1.0
374
456
  max_version = Regexp.last_match(1).strip
375
457
  VersionRange.new([
376
- Interval.new(max: max_version, max_inclusive: true)
377
- ])
458
+ Interval.new(max: max_version, max_inclusive: true, scheme: "maven")
459
+ ], scheme: "maven")
378
460
  when /^\(,([^,]+)\)$/
379
461
  # (,1.0) := <1.0
380
462
  max_version = Regexp.last_match(1).strip
381
463
  VersionRange.new([
382
- Interval.new(max: max_version, max_inclusive: false)
383
- ])
464
+ Interval.new(max: max_version, max_inclusive: false, scheme: "maven")
465
+ ], scheme: "maven")
384
466
  when /^[0-9]/
385
467
  # Simple version number without brackets - in Maven, this is minimum version
386
468
  if range_string.match(/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9.-]+)?$/)
387
469
  VersionRange.new([
388
- Interval.new(min: range_string, min_inclusive: true)
389
- ])
470
+ Interval.new(min: range_string, min_inclusive: true, scheme: "maven")
471
+ ], scheme: "maven")
390
472
  else
391
473
  parse_constraints(range_string, 'maven')
392
474
  end
@@ -400,7 +482,7 @@ module Vers
400
482
  # Find all individual ranges by splitting on comma between brackets
401
483
  individual_ranges = []
402
484
  remaining = range_string.strip
403
-
485
+
404
486
  while remaining.length > 0
405
487
  # Find the next complete bracket range
406
488
  if match = remaining.match(/^[\[\(][^\[\]\(\)]*[\]\)]/)
@@ -412,7 +494,7 @@ module Vers
412
494
  break
413
495
  end
414
496
  end
415
-
497
+
416
498
  if individual_ranges.length > 1
417
499
  individual_ranges.each do |range_part|
418
500
  begin
@@ -422,13 +504,13 @@ module Vers
422
504
  # If parsing fails, skip this part
423
505
  end
424
506
  end
425
-
507
+
426
508
  if ranges.any?
427
509
  return ranges.reduce { |acc, range| acc.union(range) }
428
510
  end
429
511
  end
430
512
  end
431
-
513
+
432
514
  # Fall back to standard constraint parsing
433
515
  parse_constraints(range_string, 'maven')
434
516
  else
@@ -443,19 +525,147 @@ module Vers
443
525
  # But simple version strings like "1.0" are minimum versions, not exact
444
526
  case range_string
445
527
  when /^[\[\(].+[\]\)]$/
446
- # Use Maven parsing for bracket notation
447
- parse_maven_range(range_string)
528
+ # Parse bracket notation like Maven but with nuget scheme
529
+ range = parse_nuget_bracket_range(range_string)
530
+ range
448
531
  when /^[0-9]/
449
532
  # Simple version number - treat as minimum version for NuGet
450
533
  VersionRange.new([
451
- Interval.new(min: range_string, min_inclusive: true)
452
- ])
534
+ Interval.new(min: range_string, min_inclusive: true, scheme: "nuget")
535
+ ], scheme: "nuget")
453
536
  else
454
537
  # Fall back to standard constraint parsing
455
538
  parse_constraints(range_string, 'nuget')
456
539
  end
457
540
  end
458
541
 
542
+ def parse_nuget_bracket_range(range_string)
543
+ case range_string
544
+ when /^\[([^,]+),([^,]+)\]$/
545
+ min_v = Regexp.last_match(1).strip
546
+ max_v = Regexp.last_match(2).strip
547
+ VersionRange.new([
548
+ Interval.new(min: min_v, max: max_v, min_inclusive: true, max_inclusive: true, scheme: "nuget")
549
+ ], scheme: "nuget")
550
+ when /^\(([^,]+),([^,]+)\)$/
551
+ min_v = Regexp.last_match(1).strip
552
+ max_v = Regexp.last_match(2).strip
553
+ VersionRange.new([
554
+ Interval.new(min: min_v, max: max_v, min_inclusive: false, max_inclusive: false, scheme: "nuget")
555
+ ], scheme: "nuget")
556
+ when /^\[([^,]+),([^,]+)\)$/
557
+ min_v = Regexp.last_match(1).strip
558
+ max_v = Regexp.last_match(2).strip
559
+ VersionRange.new([
560
+ Interval.new(min: min_v, max: max_v, min_inclusive: true, max_inclusive: false, scheme: "nuget")
561
+ ], scheme: "nuget")
562
+ when /^\(([^,]+),([^,]+)\]$/
563
+ min_v = Regexp.last_match(1).strip
564
+ max_v = Regexp.last_match(2).strip
565
+ VersionRange.new([
566
+ Interval.new(min: min_v, max: max_v, min_inclusive: false, max_inclusive: true, scheme: "nuget")
567
+ ], scheme: "nuget")
568
+ when /^\[([^,]+)\]$/
569
+ version = Regexp.last_match(1).strip
570
+ VersionRange.exact(version, scheme: "nuget")
571
+ when /^\[([^,]+),\)$/
572
+ min_v = Regexp.last_match(1).strip
573
+ VersionRange.new([
574
+ Interval.new(min: min_v, min_inclusive: true, scheme: "nuget")
575
+ ], scheme: "nuget")
576
+ when /^\(([^,]+),\)$/
577
+ min_v = Regexp.last_match(1).strip
578
+ VersionRange.new([
579
+ Interval.new(min: min_v, min_inclusive: false, scheme: "nuget")
580
+ ], scheme: "nuget")
581
+ when /^\(,([^,]+)\]$/
582
+ max_v = Regexp.last_match(1).strip
583
+ VersionRange.new([
584
+ Interval.new(max: max_v, max_inclusive: true, scheme: "nuget")
585
+ ], scheme: "nuget")
586
+ when /^\(,([^,]+)\)$/
587
+ max_v = Regexp.last_match(1).strip
588
+ VersionRange.new([
589
+ Interval.new(max: max_v, max_inclusive: false, scheme: "nuget")
590
+ ], scheme: "nuget")
591
+ else
592
+ parse_constraints(range_string, 'nuget')
593
+ end
594
+ end
595
+
596
+ # Hex/Elixir range parsing
597
+ def parse_hex_range(range_string)
598
+ # Handle "or" disjunction first
599
+ if range_string.include?(" or ")
600
+ or_parts = range_string.split(" or ").map(&:strip)
601
+ ranges = or_parts.map { |part| parse_hex_single_range(part) }
602
+ return ranges.reduce { |acc, range| acc.union(range) }
603
+ end
604
+
605
+ parse_hex_single_range(range_string)
606
+ end
607
+
608
+ def parse_hex_single_range(range_string)
609
+ # Handle "and" conjunction and comma-separated AND constraints
610
+ if range_string.include?(" and ") || range_string.include?(",")
611
+ and_parts = range_string.split(/\s+and\s+|,/).map(&:strip).reject(&:empty?)
612
+ ranges = and_parts.map { |part| parse_hex_constraint(part) }
613
+ return ranges.reduce { |acc, range| acc.intersect(range) }
614
+ end
615
+
616
+ parse_hex_constraint(range_string)
617
+ end
618
+
619
+ def parse_hex_constraint(constraint_string)
620
+ if constraint_string.match(/^~>\s*(.+)$/)
621
+ parse_pessimistic_range(Regexp.last_match(1).strip)
622
+ else
623
+ # Normalize == to = for our internal constraint parsing
624
+ normalized = constraint_string.gsub("==", "=")
625
+ constraint = Constraint.parse(normalized.strip)
626
+ if constraint.exclusion?
627
+ VersionRange.unbounded.exclude(constraint.version)
628
+ else
629
+ VersionRange.new([constraint.to_interval])
630
+ end
631
+ end
632
+ end
633
+
634
+ # Go module range parsing (comma-separated AND constraints, v-prefix preserved)
635
+ def parse_go_range(range_string)
636
+ return VersionRange.unbounded if range_string.nil? || range_string.strip.empty?
637
+
638
+ unless range_string.include?(',')
639
+ return parse_constraints(range_string, 'go')
640
+ end
641
+
642
+ parts = range_string.split(',').map(&:strip)
643
+ constraint_intervals = []
644
+ exclusions = []
645
+
646
+ parts.each do |part|
647
+ constraint = Constraint.parse(part)
648
+ if constraint.exclusion?
649
+ exclusions << constraint.version
650
+ else
651
+ interval = constraint.to_interval
652
+ constraint_intervals << interval if interval
653
+ end
654
+ end
655
+
656
+ if constraint_intervals.any?
657
+ range = VersionRange.new([constraint_intervals.first])
658
+ constraint_intervals[1..].each do |interval|
659
+ range = range.intersect(VersionRange.new([interval]))
660
+ end
661
+ else
662
+ range = VersionRange.unbounded
663
+ end
664
+
665
+ exclusions.each { |version| range = range.exclude(version) }
666
+ range
667
+ end
668
+
459
669
  # Debian range parsing
460
670
  def parse_debian_range(range_string)
461
671
  # 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.3"
4
+ VERSION = "1.2.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
  #
@@ -85,10 +104,12 @@ module Vers
85
104
  # @return [Boolean] true if the version is valid
86
105
  #
87
106
  def self.valid?(version_string)
88
- cached_new(version_string)
89
- true
90
- rescue ArgumentError
91
- false
107
+ version_string.to_s.match?(/\Av?\d+\.\d+\.\d+/)
108
+ end
109
+
110
+ def self.clean(version_string)
111
+ return nil unless valid?(version_string)
112
+ version_string.to_s.sub(/\Av/, '')
92
113
  end
93
114
 
94
115
  ##
@@ -295,6 +316,9 @@ module Vers
295
316
  private
296
317
 
297
318
  def parse_version
319
+ # Strip leading v/V prefix (e.g. "v1.0.0" -> "1.0.0")
320
+ @original = @original.sub(/\Av/i, '')
321
+
298
322
  # Handle simple numeric versions (optimized case)
299
323
  if @original.match(/^\d+$/)
300
324
  @major = @original.to_i
@@ -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
- @intervals = intervals.compact.reject(&:empty?).sort_by { |i| [i.min || '', i.max || ''] }
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
- self.class.new(result_intervals)
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
- self.class.new(intervals + other.intervals)
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 = intervals.sort_by { |i| i.min || '' }
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 && version_compare(interval.min, version) < 0
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 && version_compare(version, interval.max) < 0
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
- # Use the Version class for comparison
170
- Version.compare(a, b)
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"
@@ -117,13 +119,15 @@ module Vers
117
119
  # Vers.satisfies?("1.5.0", "^1.2.3", "npm") # => true
118
120
  #
119
121
  def self.satisfies?(version, constraint, scheme = nil)
120
- range = if scheme
121
- parse_native(constraint, scheme)
122
- else
123
- parse(constraint)
124
- end
125
-
126
- range.contains?(version)
122
+ sub_ranges = constraint.split('||').map(&:strip).reject(&:empty?)
123
+ sub_ranges.any? do |sub_range|
124
+ range = if scheme
125
+ parse_native(sub_range, scheme)
126
+ else
127
+ parse(sub_range)
128
+ end
129
+ range.contains?(version)
130
+ end
127
131
  end
128
132
 
129
133
  ##
@@ -143,6 +147,18 @@ module Vers
143
147
  Version.compare(a, b)
144
148
  end
145
149
 
150
+ ##
151
+ # Compares two version strings using scheme-specific rules
152
+ #
153
+ # @param a [String] First version string
154
+ # @param b [String] Second version string
155
+ # @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
156
+ # @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
157
+ #
158
+ def self.compare_with_scheme(a, b, scheme)
159
+ Version.compare_with_scheme(a, b, scheme)
160
+ end
161
+
146
162
  ##
147
163
  # Normalizes a version string to a consistent format
148
164
  #
@@ -163,6 +179,10 @@ module Vers
163
179
  Version.valid?(version_string)
164
180
  end
165
181
 
182
+ def self.clean(version_string)
183
+ Version.clean(version_string)
184
+ end
185
+
166
186
  ##
167
187
  # Creates an exact version range
168
188
  #
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.3
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -18,6 +18,7 @@ executables: []
18
18
  extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
+ - ".gitmodules"
21
22
  - ".ruby-version"
22
23
  - CHANGELOG.md
23
24
  - CODE_OF_CONDUCT.md
@@ -30,6 +31,8 @@ files:
30
31
  - lib/vers.rb
31
32
  - lib/vers/constraint.rb
32
33
  - lib/vers/interval.rb
34
+ - lib/vers/maven_version.rb
35
+ - lib/vers/nuget_version.rb
33
36
  - lib/vers/parser.rb
34
37
  - lib/vers/version.rb
35
38
  - lib/vers/version_range.rb