wip
Some checks failed
default / dotnet-default-workflow (push) Failing after 1m40s

This commit is contained in:
Louis Seubert 2026-05-21 20:42:05 +02:00
commit f9fb920e07
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
5 changed files with 231 additions and 238 deletions

View file

@ -81,6 +81,16 @@ internal sealed class SemanticVersionRangeTests
await Assert.That(r.Contains(v)).IsEqualTo(expected); await Assert.That(r.Contains(v)).IsEqualTo(expected);
} }
[Test]
public async Task I_can_parse_exact_version_ranges_via_public_api()
{
var range = SemanticVersionRange.Parse("=1.2.3");
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.3"))).IsTrue();
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse();
await Assert.That(range.ToString("n", null)).IsEqualTo("1.2.3");
}
[Test] [Test]
[Arguments(">1.2.3 || <1.0.0", "1.2.4", true)] [Arguments(">1.2.3 || <1.0.0", "1.2.4", true)]
[Arguments(">1.2.3 || <1.0.0", "0.9.9", true)] [Arguments(">1.2.3 || <1.0.0", "0.9.9", true)]
@ -180,6 +190,7 @@ internal sealed class SemanticVersionRangeTests
[Test] [Test]
[Arguments(">=1.0.0 !2.0.0")] [Arguments(">=1.0.0 !2.0.0")]
[Arguments("1.2.3 - ")] [Arguments("1.2.3 - ")]
[Arguments("!=1.x")]
public async Task I_can_not_parse_invalid_ranges(string range) public async Task I_can_not_parse_invalid_ranges(string range)
{ {
await Assert.That(() => SemanticVersionRange.Parse(range)) await Assert.That(() => SemanticVersionRange.Parse(range))

View file

@ -203,7 +203,7 @@ internal sealed class SemanticVersionTests
var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") }; var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") };
var json = JsonSerializer.Serialize(obj, RelaxedOptions); var json = JsonSerializer.Serialize(obj, RelaxedOptions);
await Assert.That(json).IsEqualTo("{\"Version\":\"1.0.0-rc.1+metadata\"}"); await Assert.That(json).IsEqualTo(/*lang=json,strict*/ "{\"Version\":\"1.0.0-rc.1+metadata\"}");
} }
[Test] [Test]

View file

@ -57,18 +57,18 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
} }
var buf = new SpanBuffer(destination); var buf = new SpanBuffer(destination);
var groups = _groups ?? []; var sets = _sets ?? [];
if (format is "m") if (format is "m")
{ {
for (var i = 0; i < groups.Length; i++) for (var i = 0; i < sets.Length; i++)
{ {
if (i > 0 && !buf.TryWrite(',')) if (i > 0 && !buf.TryWrite(','))
{ {
return false; return false;
} }
if (!TryFormatMaven(ref buf, groups[i])) if (!TryFormatMaven(ref buf, sets[i]))
{ {
return false; return false;
} }
@ -76,7 +76,7 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
} }
else else
{ {
for (var i = 0; i < groups.Length; i++) for (var i = 0; i < sets.Length; i++)
{ {
if (i > 0 && !buf.TryWrite(" || ")) if (i > 0 && !buf.TryWrite(" || "))
{ {
@ -85,14 +85,14 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
if (format is "ns") if (format is "ns")
{ {
if (!TryFormatSimpleNpm(ref buf, groups[i])) if (!TryFormatSimpleNpm(ref buf, sets[i]))
{ {
return false; return false;
} }
} }
else else
{ {
if (!TryFormatNormalNpm(ref buf, groups[i])) if (!TryFormatNormalNpm(ref buf, sets[i]))
{ {
return false; return false;
} }
@ -106,34 +106,34 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
#endregion #endregion
private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ComparatorGroup group) private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ConstraintSet set)
{ {
if (group.Comparators is []) if (IsAny(set))
{ {
return buf.TryWrite('*'); return buf.TryWrite('*');
} }
if (group.Comparators is [{ Op: ComparatorOp.Eq, Version: var version }]) if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
{ {
return buf.TryWrite(version); return buf.TryWrite(version);
} }
if (group.Comparators.Length is not 2) if (set.Constraints.Length is not 2)
{ {
return false; return false;
} }
if (group is not { Lower: { Op: ComparatorOp.Gte or ComparatorOp.Gt, Version: var lo } }) if (set is not { Lower: { Operation: Comparison.Gte or Comparison.Gt, Version: var lo } })
{ {
return false; return false;
} }
if (group is not { Upper: { Op: ComparatorOp.Lt or ComparatorOp.Lte, Version: var hi } upper }) if (set is not { Upper: { Operation: Comparison.Lt or Comparison.Lte, Version: var hi } upper })
{ {
return false; return false;
} }
if (upper.Op is ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } }) if (upper.Operation is Comparison.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
{ {
if (lo is { Major: > 0 } && hi.Major == lo.Major + 1) if (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
{ {
@ -151,7 +151,7 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
} }
} }
if (upper.Op is ComparatorOp.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } }) if (upper.Operation is Comparison.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
{ {
if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1) if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1)
{ {
@ -162,28 +162,28 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
return false; return false;
} }
private static bool TryFormatNormalNpm(ref SpanBuffer buf, ComparatorGroup group) private static bool TryFormatNormalNpm(ref SpanBuffer buf, ConstraintSet set)
{ {
if (group.Comparators.Length is 0) if (IsAny(set))
{ {
return buf.TryWrite('*'); return buf.TryWrite('*');
} }
for (var i = 0; i < group.Comparators.Length; i++) for (var i = 0; i < set.Constraints.Length; i++)
{ {
if (i > 0 && !buf.TryWrite(' ')) if (i > 0 && !buf.TryWrite(' '))
{ {
return false; return false;
} }
var @operator = group.Comparators[i].Op switch var @operator = set.Constraints[i].Operation switch
{ {
ComparatorOp.Neq => "!=", Comparison.Neq => "!=",
ComparatorOp.Lt => "<", Comparison.Lt => "<",
ComparatorOp.Lte => "<=", Comparison.Lte => "<=",
ComparatorOp.Gt => ">", Comparison.Gt => ">",
ComparatorOp.Gte => ">=", Comparison.Gte => ">=",
_ => ReadOnlySpan<char>.Empty, Comparison.Eq or _ => ReadOnlySpan<char>.Empty,
}; };
if (!@operator.IsEmpty && !buf.TryWrite(@operator)) if (!@operator.IsEmpty && !buf.TryWrite(@operator))
@ -191,7 +191,7 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
return false; return false;
} }
if (!buf.TryWrite(group.Comparators[i].Version)) if (!buf.TryWrite(set.Constraints[i].Version))
{ {
return false; return false;
} }
@ -200,17 +200,27 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
return true; return true;
} }
private static bool TryFormatMaven(ref SpanBuffer buf, ComparatorGroup group) private static bool TryFormatMaven(ref SpanBuffer buf, ConstraintSet set)
{ {
if (group.Comparators is [{ Op: ComparatorOp.Eq, Version: var version }]) if (IsAny(set))
{
return buf.TryWrite("(,)");
}
if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
{ {
return buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']'); return buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']');
} }
return buf.TryWrite(group.Lower?.Op == ComparatorOp.Gte ? '[' : '(') && return buf.TryWrite(set.Lower?.Operation == Comparison.Gte ? '[' : '(') &&
(!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) && (!set.Lower.HasValue || buf.TryWrite(set.Lower.Value.Version)) &&
buf.TryWrite(',') && buf.TryWrite(',') &&
(!group.Upper.HasValue || buf.TryWrite(group.Upper.Value.Version)) && (!set.Upper.HasValue || buf.TryWrite(set.Upper.Value.Version)) &&
buf.TryWrite(group.Upper?.Op == ComparatorOp.Lte ? ']' : ')'); buf.TryWrite(set.Upper?.Operation == Comparison.Lte ? ']' : ')');
}
private static bool IsAny(ConstraintSet set)
{
return set.Constraints is [] or [{ Operation: Comparison.Gte, Version: { Major: 0, Minor: 0, Patch: 0 } }];
} }
} }

View file

@ -1,6 +1,7 @@
// Copyright (c) The Geekeey Authors // Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace Geekeey.SemVer; namespace Geekeey.SemVer;
@ -90,29 +91,29 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result) private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
{ {
result = default; result = default;
var groups = new List<ComparatorGroup>(); var sets = new List<ConstraintSet>();
foreach (var range in new NodeSetGrouping(s)) foreach (var range in new NpmSetGrouping(s))
{ {
if (!TryParseNpmGroup(s[range].Trim(), out var group)) if (!TryParseNpmSet(s[range].Trim(), out var set))
{ {
return false; return false;
} }
groups.Add(group); sets.Add(set);
} }
result = new SemanticVersionRange([.. groups]); result = new SemanticVersionRange([.. sets]);
return true; return true;
} }
private ref struct NodeSetGrouping private ref struct NpmSetGrouping
{ {
private readonly ReadOnlySpan<char> _span; private readonly ReadOnlySpan<char> _span;
private int _currentStart; private int _currentStart;
private int _currentEnd; private int _currentEnd;
public NodeSetGrouping(ReadOnlySpan<char> span) public NpmSetGrouping(ReadOnlySpan<char> span)
{ {
_span = span; _span = span;
_currentStart = 0; _currentStart = 0;
@ -136,15 +137,15 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return true; return true;
} }
public readonly NodeSetGrouping GetEnumerator() public readonly NpmSetGrouping GetEnumerator()
{ {
return this; return this;
} }
} }
private static bool TryParseNpmGroup(ReadOnlySpan<char> s, out ComparatorGroup group) private static bool TryParseNpmSet(ReadOnlySpan<char> s, out ConstraintSet set)
{ {
group = default; set = default;
if (s.IsEmpty) if (s.IsEmpty)
{ {
@ -152,60 +153,38 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
} }
// Hyphen range: "1.0.0 - 2.0.0" OR Caret: "^x.y.z" OR Tilde: "~x.y.z" // Hyphen range: "1.0.0 - 2.0.0" OR Caret: "^x.y.z" OR Tilde: "~x.y.z"
if (TryParseHyphenRange(s, out group) || TryParseCaretRange(s, out group) || TryParseTildeRange(s, out group)) if (TryParseHyphenRange(s, out set) || TryParseCaretRange(s, out set) || TryParseTildeRange(s, out set))
{ {
return true; return true;
} }
// Space-separated AND comparators var comparators = new List<Constraint>();
var comparators = new List<Comparator>(); foreach (var range in s.Split(' '))
var tokens = 0;
while (!s.IsEmpty)
{ {
// Skip spaces if (s[range] is not { IsEmpty: false } segment)
s = s.TrimStart();
if (s.IsEmpty)
{ {
break; continue;
} }
// Find end of this token (next space) if (!TryParseCompare(segment, comparators))
var end = s.IndexOf(' ');
var token = end >= 0 ? s[..end] : s;
tokens++;
if (token.ContainsAny("*xX"))
{ {
if (!TryExpandWildcardComparators(token, comparators)) return false;
{
return false;
}
} }
else
{
if (!TryParseComparator(token, out var comp))
{
return false;
}
comparators.Add(comp);
}
s = end >= 0 ? s[(end + 1)..] : [];
} }
if (comparators.Count is 0 && tokens is 0) if (comparators.Count is 0)
{ {
return false; return false;
} }
group = new ComparatorGroup([.. comparators]); set = new ConstraintSet([.. comparators]);
return true; return true;
} }
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ComparatorGroup group) private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ConstraintSet set)
{ {
group = default; set = default;
for (var i = 0; i < s.Length; i++) for (var i = 0; i < s.Length; i++)
{ {
if (s[i..] is [' ', '-', ' ', ..]) if (s[i..] is [' ', '-', ' ', ..])
@ -220,7 +199,7 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return false; return false;
} }
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lte, hi)]); set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lte, hi)]);
return true; return true;
} }
} }
@ -228,9 +207,9 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return false; return false;
} }
private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ComparatorGroup group) private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ConstraintSet set)
{ {
group = default; set = default;
if (s.IsEmpty || s[0] is not '^') if (s.IsEmpty || s[0] is not '^')
{ {
@ -259,13 +238,13 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
hi = new SemanticVersion(0, 0, lo.Patch + 1); hi = new SemanticVersion(0, 0, lo.Patch + 1);
} }
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]); set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
return true; return true;
} }
private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ComparatorGroup group) private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ConstraintSet set)
{ {
group = default; set = default;
if (s.IsEmpty || s[0] is not '~') if (s.IsEmpty || s[0] is not '~')
{ {
@ -290,37 +269,72 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0); hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
} }
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]); set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
return true; return true;
} }
private static bool TryParseComparator(ReadOnlySpan<char> token, out Comparator result) private static bool TryParseCompare(ReadOnlySpan<char> s, List<Constraint> constraints)
{ {
result = default; var op = Comparison.Eq;
if (!TryParseComparatorPrefix(token.Trim(), out var op, out token)) if (TryParseComparatorPrefix(s, out var comparison, out var rest))
{ {
if (!SemanticVersion.TryParse(token, null, out var version)) op = comparison;
{ s = rest;
return false; }
}
result = new Comparator(op, version); if (!SemanticVersion.TryParsePartially(s, out var lo, out var components))
{
return false;
}
if (components is 0)
{
constraints.Add(new Constraint(Comparison.Gte, new SemanticVersion(0, 0, 0)));
return op is Comparison.Eq;
}
if (components is 3)
{
constraints.Add(new Constraint(op, lo));
return true; return true;
} }
SemanticVersion hi;
if (components is 1)
{
hi = new SemanticVersion(lo.Major + 1, 0, 0);
}
else else
{ {
if (!SemanticVersion.TryParse(token, null, out var version)) hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
{ }
return false;
}
result = new Comparator(op, version); switch (op)
return true; {
case Comparison.Eq:
case Comparison.Gte:
constraints.Add(new Constraint(Comparison.Gte, lo));
constraints.Add(new Constraint(Comparison.Lt, hi));
return true;
case Comparison.Gt:
constraints.Add(new Constraint(Comparison.Gt, lo));
constraints.Add(new Constraint(Comparison.Lt, hi));
return true;
case Comparison.Lte:
constraints.Add(new Constraint(Comparison.Lt, hi));
return true;
case Comparison.Lt:
constraints.Add(new Constraint(Comparison.Lt, lo));
return true;
case Comparison.Neq:
default:
return false;
} }
} }
private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out ComparatorOp op, out ReadOnlySpan<char> remainder) private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out Comparison op, out ReadOnlySpan<char> remainder)
{ {
if (s.IsEmpty) if (s.IsEmpty)
{ {
@ -332,27 +346,27 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
switch (s) switch (s)
{ {
case ['!', '=', ..]: case ['!', '=', ..]:
op = ComparatorOp.Neq; op = Comparison.Neq;
remainder = s[2..]; remainder = s[2..];
return true; return true;
case ['>', '=', ..]: case ['>', '=', ..]:
op = ComparatorOp.Gte; op = Comparison.Gte;
remainder = s[2..]; remainder = s[2..];
return true; return true;
case ['<', '=', ..]: case ['<', '=', ..]:
op = ComparatorOp.Lte; op = Comparison.Lte;
remainder = s[2..]; remainder = s[2..];
return true; return true;
case ['>', ..]: case ['>', ..]:
op = ComparatorOp.Gt; op = Comparison.Gt;
remainder = s[1..]; remainder = s[1..];
return true; return true;
case ['<', ..]: case ['<', ..]:
op = ComparatorOp.Lt; op = Comparison.Lt;
remainder = s[1..]; remainder = s[1..];
return true; return true;
case ['=', ..]: case ['=', ..]:
op = ComparatorOp.Eq; op = Comparison.Eq;
remainder = s[1..]; remainder = s[1..];
return true; return true;
default: default:
@ -362,133 +376,86 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
} }
} }
private static bool TryExpandWildcardComparators(ReadOnlySpan<char> s, List<Comparator> comparators)
{
s = s.Trim();
if (s is ['*' or 'x' or 'X'])
{
// bare wildcards only make sense when it is an exact version comparison.
return true;
}
var op = ComparatorOp.Eq;
if (TryParseComparatorPrefix(s, out var parsedOp, out var remainder))
{
op = parsedOp;
s = remainder;
}
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
{
return false;
}
if (s is ['*' or 'x' or 'X'])
{
// bare wildcards only make sense when it is an exact version comparison.
return op == ComparatorOp.Eq;
}
SemanticVersion hi;
if (wildcard is 1)
{
hi = new SemanticVersion(lo.Major + 1, 0, 0);
}
else
{
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
}
switch (op)
{
case ComparatorOp.Eq:
case ComparatorOp.Gte:
comparators.Add(new Comparator(ComparatorOp.Gte, lo));
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
break;
case ComparatorOp.Gt:
comparators.Add(new Comparator(ComparatorOp.Gt, lo));
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
break;
case ComparatorOp.Lte:
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
break;
case ComparatorOp.Lt:
comparators.Add(new Comparator(ComparatorOp.Lt, lo));
break;
default:
return false;
}
return true;
}
private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result) private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result)
{ {
result = default; result = default;
var groups = new List<ComparatorGroup>(); var sets = new List<ConstraintSet>();
while (!s.IsEmpty) foreach (var range in new MavenSetGrouping(s))
{ {
s = s.Trim(); if (!TryParseMavenSet(s[range].Trim(), out var set))
if (s.IsEmpty)
{
break;
}
if (s[0] is not '[' and not '(')
{ {
return false; return false;
} }
// Find matching close bracket sets.Add(set);
var close = FindMavenClose(s);
if (close < 0)
{
return false;
}
var bracket = s[..(close + 1)];
if (!TryParseMavenGroup(bracket, out var g))
{
return false;
}
groups.Add(g);
s = s[(close + 1)..].Trim();
if (!s.IsEmpty && s[0] == ',')
{
s = s[1..]; // OR separator between groups
}
} }
if (groups.Count == 0) result = new SemanticVersionRange([.. sets]);
{
return false;
}
result = new SemanticVersionRange([.. groups]);
return true; return true;
} }
private static int FindMavenClose(ReadOnlySpan<char> s) private ref struct MavenSetGrouping
{ {
for (var i = 1; i < s.Length; i++) private readonly ReadOnlySpan<char> _span;
private int _currentStart;
private int _currentEnd;
public MavenSetGrouping(ReadOnlySpan<char> readOnlySpan)
{ {
if (s[i] is ']' or ')') _span = readOnlySpan;
{ _currentStart = 0;
return i; _currentEnd = -1;
}
} }
return -1; public readonly Range Current => _currentStart.._currentEnd;
public bool MoveNext()
{
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
if (_currentStart >= _span.Length)
{
return false;
}
var depth = 0;
var i = _currentStart;
while (i < _span.Length)
{
var ch = _span[i];
if (ch is '[' or '(')
{
depth++;
}
else if (ch is ']' or ')')
{
depth--;
}
else if (ch is ',' && depth is 0)
{
_currentEnd = i;
return true;
}
i++;
}
_currentEnd = _span.Length;
return true;
}
public readonly MavenSetGrouping GetEnumerator()
{
return this;
}
} }
private static bool TryParseMavenGroup(ReadOnlySpan<char> s, out ComparatorGroup group) private static bool TryParseMavenSet(ReadOnlySpan<char> s, out ConstraintSet set)
{ {
group = default; set = default;
var loInclusive = s[0] is '['; var loInclusive = s[0] is '[';
var hiInclusive = s[^1] is ']'; var hiInclusive = s[^1] is ']';
@ -507,11 +474,11 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return false; return false;
} }
group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, version)]); set = new ConstraintSet([new Constraint(Comparison.Eq, version)]);
return true; return true;
} }
var comps = new List<Comparator>(); var comps = new List<Constraint>();
if (s[..i].Trim() is { IsEmpty: false } loStr) if (s[..i].Trim() is { IsEmpty: false } loStr)
{ {
@ -520,7 +487,7 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return false; return false;
} }
comps.Add(new Comparator(loInclusive ? ComparatorOp.Gte : ComparatorOp.Gt, lo)); comps.Add(new Constraint(loInclusive ? Comparison.Gte : Comparison.Gt, lo));
} }
if (s[(i + 1)..].Trim() is { IsEmpty: false } hiStr) if (s[(i + 1)..].Trim() is { IsEmpty: false } hiStr)
@ -530,20 +497,20 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return false; return false;
} }
comps.Add(new Comparator(hiInclusive ? ComparatorOp.Lte : ComparatorOp.Lt, hi)); comps.Add(new Constraint(hiInclusive ? Comparison.Lte : Comparison.Lt, hi));
} }
// Check for exact match [a,a] // Check for exact match [a,a]
if (comps is [{ Op: ComparatorOp.Gte, Version: var lhs }, { Op: ComparatorOp.Lte, Version: var rhs }]) if (comps is [{ Operation: Comparison.Gte, Version: var lhs }, { Operation: Comparison.Lte, Version: var rhs }])
{ {
if (lhs == rhs) if (lhs == rhs)
{ {
group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, lhs)]); set = new ConstraintSet([new Constraint(Comparison.Eq, lhs)]);
return true; return true;
} }
} }
group = new ComparatorGroup([.. comps]); set = new ConstraintSet([.. comps]);
return true; return true;
} }
} }

View file

@ -10,11 +10,11 @@ namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersionRange public readonly partial record struct SemanticVersionRange
{ {
// OR of AND-groups. null == empty range (matches nothing). // OR of AND-groups. null == empty range (matches nothing).
private readonly ComparatorGroup[]? _groups; private readonly ConstraintSet[]? _sets;
internal SemanticVersionRange(ComparatorGroup[] groups) internal SemanticVersionRange(ConstraintSet[] sets)
{ {
_groups = groups; _sets = sets;
} }
/// <summary> /// <summary>
@ -24,70 +24,75 @@ public readonly partial record struct SemanticVersionRange
/// <returns><c>true</c> if the version is contained in the range, otherwise <c>false</c>.</returns> /// <returns><c>true</c> if the version is contained in the range, otherwise <c>false</c>.</returns>
public bool Contains(SemanticVersion version) public bool Contains(SemanticVersion version)
{ {
if (_groups is null || _groups.Length == 0) if (_sets is null || _sets.Length == 0)
{ {
return false; return false;
} }
return _groups.Any(group => group.Includes(version)); return _sets.Any(set => set.Includes(version));
} }
} }
internal enum ComparatorOp { Eq, Neq, Lt, Lte, Gt, Gte } internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
internal readonly struct Comparator internal readonly struct Constraint
{ {
public readonly ComparatorOp Op; public readonly Comparison Operation;
public readonly SemanticVersion Version; public readonly SemanticVersion Version;
public Comparator(ComparatorOp op, SemanticVersion version) public Constraint(Comparison operation, SemanticVersion version)
{ {
Op = op; Operation = operation;
Version = version; Version = version;
} }
public bool Includes(SemanticVersion v) public bool Includes(SemanticVersion v)
{ {
return Op switch return Operation switch
{ {
ComparatorOp.Eq => v == Version, Comparison.Eq => v == Version,
ComparatorOp.Neq => v != Version, Comparison.Neq => v != Version,
ComparatorOp.Lt => v < Version, Comparison.Lt => v < Version,
ComparatorOp.Lte => v <= Version, Comparison.Lte => v <= Version,
ComparatorOp.Gt => v > Version, Comparison.Gt => v > Version,
ComparatorOp.Gte => v >= Version, Comparison.Gte => v >= Version,
_ => false _ => false
}; };
} }
public override string ToString() public override string ToString()
{ {
return Op switch return Operation switch
{ {
ComparatorOp.Neq => $"!={Version}", Comparison.Neq => $"!={Version}",
ComparatorOp.Lt => $"<{Version}", Comparison.Lt => $"<{Version}",
ComparatorOp.Lte => $"<={Version}", Comparison.Lte => $"<={Version}",
ComparatorOp.Gt => $">{Version}", Comparison.Gt => $">{Version}",
ComparatorOp.Gte => $">={Version}", Comparison.Gte => $">={Version}",
_ => $"{Version}", Comparison.Eq or _ => $"{Version}",
}; };
} }
} }
// One AND-group of comparators (all must be satisfied). // One AND-group of comparators (all must be satisfied).
internal readonly struct ComparatorGroup(Comparator[] comparators) internal readonly struct ConstraintSet
{ {
public readonly Comparator[] Comparators = comparators; public readonly Constraint[] Constraints;
public Comparator? Upper => Comparators.Where(c => c.Op is ComparatorOp.Lt or ComparatorOp.Lte) public ConstraintSet(Constraint[] constraints)
.Select(it => (Comparator?)it).FirstOrDefault(); {
Constraints = constraints;
}
public Comparator? Lower => Comparators.Where(c => c.Op is ComparatorOp.Gt or ComparatorOp.Gte) public Constraint? Upper => Constraints.Where(c => c.Operation is Comparison.Lt or Comparison.Lte)
.Select(it => (Comparator?)it).FirstOrDefault(); .Select(it => (Constraint?)it).FirstOrDefault();
public Constraint? Lower => Constraints.Where(c => c.Operation is Comparison.Gt or Comparison.Gte)
.Select(it => (Constraint?)it).FirstOrDefault();
public bool Includes(SemanticVersion v) public bool Includes(SemanticVersion v)
{ {
foreach (var c in Comparators) foreach (var c in Constraints)
{ {
if (!c.Includes(v)) if (!c.Includes(v))
{ {