diff --git a/src/semver.tests/SemanticVersionRangeTests.cs b/src/semver.tests/SemanticVersionRangeTests.cs
index 2516657..d48cf27 100644
--- a/src/semver.tests/SemanticVersionRangeTests.cs
+++ b/src/semver.tests/SemanticVersionRangeTests.cs
@@ -109,6 +109,16 @@ internal sealed class SemanticVersionRangeTests
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
+ [Test]
+ public async Task I_can_handle_two_constraints_without_combining_them()
+ {
+ var range = SemanticVersionRange.Parse(">=1.0.0 !=2.0.0");
+
+ await Assert.That(range.Contains(SemanticVersion.Parse("1.5.0"))).IsTrue();
+ await Assert.That(range.Contains(SemanticVersion.Parse("2.0.0"))).IsFalse();
+ await Assert.That(range.ToString("n", null)).IsEqualTo(">=1.0.0 !=2.0.0");
+ }
+
[Test]
[Arguments("[1.2.3]", "1.2.3", true)]
[Arguments("[1.2.3]", "1.2.4", false)]
@@ -121,6 +131,8 @@ internal sealed class SemanticVersionRangeTests
[Arguments("[1.2.3,)", "9.9.9", true)]
[Arguments("(,1.4.0]", "1.4.0", true)]
[Arguments("(,1.4.0]", "0.0.0", true)]
+ [Arguments("(,)", "0.0.0", true)]
+ [Arguments("(,)", "9.9.9", true)]
public async Task I_can_satisfy_maven_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
@@ -140,10 +152,10 @@ internal sealed class SemanticVersionRangeTests
}
[Test]
- [Arguments("^1.2.3", "1.2.3-alpha", false)]
+ [Arguments("^1.2.3", "1.2.3-alpha", true)]
[Arguments("^1.2.3-alpha", "1.2.3-beta", true)]
[Arguments("^1.2.3-alpha", "1.2.4", true)]
- [Arguments("^1.2.3-alpha", "1.3.0-alpha", false)]
+ [Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
public async Task I_can_satisfy_prerelease_rules(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
@@ -183,7 +195,9 @@ internal sealed class SemanticVersionRangeTests
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
[Arguments("[1.2.3,1.3.0)", "ns", "~1.2.3")]
[Arguments("[1.2,1.3],[1.5,)", "n", ">=1.2.0 <=1.3.0 || >=1.5.0")]
+ [Arguments(">=1.0.0 !=2.0.0", "m", ">=1.0.0 !=2.0.0")]
[Arguments("*", "m", "(,)")]
+ [Arguments("(,)", "ns", "*")]
public async Task I_can_convert_range_formats(string range, string format, string expected)
{
var value = SemanticVersionRange.Parse(range);
diff --git a/src/semver/SemanticVersion.Formatting.cs b/src/semver/SemanticVersion.Formatting.cs
index f6b23d9..f99bf8c 100644
--- a/src/semver/SemanticVersion.Formatting.cs
+++ b/src/semver/SemanticVersion.Formatting.cs
@@ -4,6 +4,12 @@ namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersion : ISpanFormattable
{
+ ///
+ public override string ToString()
+ {
+ return ToString(null, null);
+ }
+
#region IFormattable
///
@@ -49,37 +55,52 @@ public readonly partial record struct SemanticVersion : ISpanFormattable
///
public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
{
+ charsWritten = 0;
+
if (format.IsEmpty)
{
format = "s";
}
if (format is not "s" and not "f" and not "r")
+ {
+ throw new FormatException($"The format string '{format}' is not supported.");
+ }
+
+ if (!destination.TryWrite($"{Major}.{Minor}.{Patch}", out var written))
{
charsWritten = 0;
return false;
}
- var builder = new SpanStringBuilder(destination, provider);
- builder.AppendFormatted(Major);
- builder.AppendLiteral(".");
- builder.AppendFormatted(Minor);
- builder.AppendLiteral(".");
- builder.AppendFormatted(Patch);
+ destination = destination[written..];
+ charsWritten += written;
if (Prerelease is { Length: > 0 } && format is "s" or "f")
{
- builder.AppendLiteral("-");
- builder.AppendFormatted(Prerelease);
+ if (!destination.TryWrite(provider, $"-{Prerelease}", out written))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ destination = destination[written..];
+ charsWritten += written;
}
if (Metadata is { Length: > 0 } && format is "f")
{
- builder.AppendLiteral("+");
- builder.AppendFormatted(Metadata);
+ if (!destination.TryWrite(provider, $"+{Metadata}", out written))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ destination = destination[written..];
+ charsWritten += written;
}
- return builder.TryComplete(out charsWritten);
+ return true;
}
#endregion
diff --git a/src/semver/SemanticVersion.cs b/src/semver/SemanticVersion.cs
index 84a7ed5..446c982 100644
--- a/src/semver/SemanticVersion.cs
+++ b/src/semver/SemanticVersion.cs
@@ -102,10 +102,4 @@ public readonly partial record struct SemanticVersion
/// details. It does not affect the precedence of the version.
///
public string? Metadata { get; }
-
- ///
- public override string ToString()
- {
- return ToString(null, null);
- }
}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.Formatting.cs b/src/semver/SemanticVersionRange.Formatting.cs
index a86014e..b083ee4 100644
--- a/src/semver/SemanticVersionRange.Formatting.cs
+++ b/src/semver/SemanticVersionRange.Formatting.cs
@@ -7,6 +7,12 @@ namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersionRange : ISpanFormattable
{
+ ///
+ public override string ToString()
+ {
+ return ToString(null, null);
+ }
+
#region IFormattable
///
@@ -38,6 +44,8 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
///
public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
{
+ charsWritten = 0;
+
if (format.IsEmpty)
{
format = "ns";
@@ -45,31 +53,173 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
if (format is not "m" and not "n" and not "ns")
{
- charsWritten = 0;
- return false;
+ throw new FormatException($"The format string '{format}' is not supported.");
}
- var builder = new SpanStringBuilder(destination, provider);
+ var buf = new SpanBuffer(destination);
+ var groups = _groups ?? [];
- if (_sets is not { Length: > 0 } sets)
+ if (format is "m")
{
- builder.AppendLiteral(format is "m" ? "(,)" : "*");
+ for (var i = 0; i < groups.Length; i++)
+ {
+ if (i > 0 && !buf.TryWrite(','))
+ {
+ return false;
+ }
+
+ if (!TryFormatMaven(ref buf, groups[i]))
+ {
+ return false;
+ }
+ }
}
else
{
- for (var i = 0; i < sets.Length; i++)
+ for (var i = 0; i < groups.Length; i++)
{
- if (i > 0)
+ if (i > 0 && !buf.TryWrite(" || "))
{
- builder.AppendLiteral(format is "m" ? "," : " || ");
+ return false;
}
- sets[i].AppendFormatted(ref builder, format);
+ if (format is "ns")
+ {
+ if (!TryFormatSimpleNpm(ref buf, groups[i]))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!TryFormatNormalNpm(ref buf, groups[i]))
+ {
+ return false;
+ }
+ }
}
}
- return builder.TryComplete(out charsWritten);
+ charsWritten = buf.Written;
+ return true;
}
#endregion
+
+ private static bool TryWriteComparator(ref SpanBuffer buf, Comparator c)
+ {
+ ReadOnlySpan op = c.Op switch
+ {
+ ComparatorOp.Neq => "!=",
+ ComparatorOp.Lt => "<",
+ ComparatorOp.Lte => "<=",
+ ComparatorOp.Gt => ">",
+ ComparatorOp.Gte => ">=",
+ _ => []
+ };
+
+ if (!op.IsEmpty && !buf.TryWrite(op))
+ {
+ return false;
+ }
+
+ return buf.TryWrite(c.Version);
+ }
+
+ private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ComparatorGroup group)
+ {
+ var c = group.Comparators;
+ var checkpoint = buf.Written;
+
+ if (c.Length is 0)
+ {
+ return buf.TryWrite('*');
+ }
+
+ if (c is [{ Op: ComparatorOp.Eq }])
+ {
+ return buf.TryWrite(c[0].Version) || buf.Reset(checkpoint);
+ }
+
+ if (c.Length is not 2)
+ {
+ return buf.Reset(checkpoint);
+ }
+
+ var lower = group.Lower!.Value;
+ var upper = group.Upper!.Value;
+
+ if (lower.Op is ComparatorOp.Gte or ComparatorOp.Gt && upper.Op is ComparatorOp.Lt or ComparatorOp.Lte)
+ {
+ var lo = lower.Version;
+ var hi = upper.Version;
+
+ if (upper.Op == ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
+ {
+ if ((lo.Major > 0 && hi.Major == lo.Major + 1) ||
+ (lo is { Major: 0, Minor: > 0 } && hi.Major == 0 && hi.Minor == lo.Minor + 1) ||
+ (lo is { Major: 0, Minor: 0 } && hi is { Major: 0, Minor: 0 } && hi.Patch == lo.Patch + 1))
+ {
+ return (buf.TryWrite('^') && buf.TryWrite(lo)) || buf.Reset(checkpoint);
+ }
+ }
+
+ if (upper.Op == ComparatorOp.Lt &&
+ hi is { Patch: 0, Prerelease: not { Length: > 0 } } &&
+ hi.Major == lo.Major &&
+ hi.Minor == lo.Minor + 1)
+ {
+ return (buf.TryWrite('~') && buf.TryWrite(lo)) || buf.Reset(checkpoint);
+ }
+ }
+
+ return buf.Reset(checkpoint);
+ }
+
+ private static bool TryFormatNormalNpm(ref SpanBuffer buf, ComparatorGroup group)
+ {
+ var comps = group.Comparators;
+ if (comps.Length is 0)
+ {
+ return buf.TryWrite('*');
+ }
+
+ for (var j = 0; j < comps.Length; j++)
+ {
+ if (j > 0 && !buf.TryWrite(' '))
+ {
+ return false;
+ }
+
+ if (!TryWriteComparator(ref buf, comps[j]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool TryFormatMaven(ref SpanBuffer buf, ComparatorGroup group)
+ {
+ if (group.Comparators is [{ Op: ComparatorOp.Eq, Version: var version }])
+ {
+ var exactCheckpoint = buf.Written;
+ return (buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']')) || buf.Reset(exactCheckpoint);
+ }
+
+ if (group.Comparators.Any(c => c.Op is ComparatorOp.Neq) || group.Comparators.Length > 2)
+ {
+ return TryFormatNormalNpm(ref buf, group);
+ }
+
+ var checkpoint = buf.Written;
+
+ return (buf.TryWrite(group.Lower?.Op == ComparatorOp.Gte ? '[' : '(') &&
+ (!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) &&
+ buf.TryWrite(',') &&
+ (!group.Upper.HasValue || buf.TryWrite(group.Upper.Value.Version)) &&
+ buf.TryWrite(group.Upper?.Op == ComparatorOp.Lte ? ']' : ')')) ||
+ buf.Reset(checkpoint);
+ }
}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.Parsing.cs b/src/semver/SemanticVersionRange.Parsing.cs
index 13e436b..ce8cf74 100644
--- a/src/semver/SemanticVersionRange.Parsing.cs
+++ b/src/semver/SemanticVersionRange.Parsing.cs
@@ -82,348 +82,30 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable s, out SemanticVersionRange result)
+ private static bool TryParseNpm(ReadOnlySpan s, out SemanticVersionRange result)
{
- var sets = new List();
-
- foreach (var range in new JavaSetGrouping(s))
- {
- if (s[range] is not { IsEmpty: false } part)
- {
- continue;
- }
-
- if (!TryParseJavaSet(part, out var set))
- {
- result = default;
- return false;
- }
-
- sets.Add(set);
- }
-
- result = new SemanticVersionRange([.. sets]);
- return true;
- }
-
- private static bool TryParseJavaSet(ReadOnlySpan s, out ConstraintSet set)
- {
- set = default;
-
- if (s.IsEmpty)
- {
- return false;
- }
-
- if (s is not ['[' or '(', .., ']' or ')'])
- {
- if (!SemanticVersion.TryParsePartially(s, out var version, out _))
- {
- return false;
- }
-
- set = new ConstraintSet([new Constraint(lower: (version, true), upper: null)]);
- return true;
- }
-
- var inclusiveStart = s[0] is '[';
- var inclusiveEnd = s[^1] is ']';
- var text = s[1..^1];
- var comma = text.IndexOf(',');
-
- if (comma < 0)
- {
- if (!inclusiveStart || !inclusiveEnd || !SemanticVersion.TryParsePartially(text.Trim(), out var version, out _))
- {
- return false;
- }
-
- set = new ConstraintSet([new Constraint(Comparator.Equal, version)]);
- return true;
- }
-
- if (!TryCreateBound(text[..comma].Trim(), inclusiveStart, out var lower) ||
- !TryCreateBound(text[(comma + 1)..].Trim(), inclusiveEnd, out var upper))
- {
- return false;
- }
-
- set = new ConstraintSet(lower is null && upper is null ? [] : [new Constraint(lower: lower, upper: upper)]);
- return true;
- }
-
- private static bool TryCreateBound(ReadOnlySpan s, bool inclusive, out (SemanticVersion Version, bool Inclusive)? bound)
- {
- if (s.IsEmpty)
- {
- bound = null;
- return true;
- }
-
- if (!SemanticVersion.TryParsePartially(s, out var version, out _))
- {
- bound = null;
- return false;
- }
-
- bound = (version, inclusive);
- return true;
- }
-
- private ref struct JavaSetGrouping
- {
- private readonly ReadOnlySpan _span;
- private int _currentStart;
- private int _currentEnd;
-
- public JavaSetGrouping(ReadOnlySpan readOnlySpan)
- {
- _span = readOnlySpan;
- _currentStart = 0;
- _currentEnd = -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 JavaSetGrouping GetEnumerator()
- {
- return this;
- }
- }
-
- private static bool TryParseNode(ReadOnlySpan s, out SemanticVersionRange result)
- {
- var sets = new List();
+ result = default;
+ var groups = new List();
foreach (var range in new NodeSetGrouping(s))
{
- if (s[range] is not { IsEmpty: false } part)
+ if (!TryParseNpmGroup(s[range].Trim(), out var group))
{
- continue;
- }
-
- if (!TryParseNodeSet(part, out var set))
- {
- result = default;
return false;
}
- sets.Add(set);
+ groups.Add(group);
}
- result = new SemanticVersionRange([.. sets]);
+ result = new SemanticVersionRange([.. groups]);
return true;
}
- private static bool TryParseNodeSet(ReadOnlySpan s, out ConstraintSet set)
- {
- if (s.IndexOf(" - ") is not -1 and var hyphen)
- {
- if (!SemanticVersion.TryParse(s[..hyphen].Trim(), out var lower))
- {
- set = default;
- return false;
- }
-
- if (!SemanticVersion.TryParse(s[(hyphen + 3)..].Trim(), out var upper))
- {
- set = default;
- return false;
- }
-
- set = new ConstraintSet([new Constraint(lower: (lower, true), upper: (upper, true))]);
- return true;
- }
-
- var list = new List(2);
- var current = s;
- while (TryReadToken(ref current, out var token))
- {
- scoped ReadOnlySpan instruction = [];
-
- if (token is ">=" or "<=" or ">" or "<" or "~" or "^" or "=" or "!=")
- {
- instruction = token;
-
- if (!TryReadToken(ref current, out token))
- {
- set = default;
- return false;
- }
- }
- else
- {
- if (token.StartsWith(">="))
- {
- instruction = ">=";
- token = token[2..];
- }
- else if (token.StartsWith("<="))
- {
- instruction = "<=";
- token = token[2..];
- }
- else if (token.StartsWith("!="))
- {
- instruction = "!=";
- token = token[2..];
- }
- else if (token.StartsWith(">"))
- {
- instruction = ">";
- token = token[1..];
- }
- else if (token.StartsWith("<"))
- {
- instruction = "<";
- token = token[1..];
- }
- else if (token.StartsWith("^"))
- {
- instruction = "^";
- token = token[1..];
- }
- else if (token.StartsWith("~"))
- {
- instruction = "~";
- token = token[1..];
- }
- else if (token.StartsWith("="))
- {
- instruction = "=";
- token = token[1..];
- }
- }
-
- if (token is "*" or "x" or "X")
- {
- set = new ConstraintSet([default]);
- return true;
- }
-
- if (!TryParseNpmConstraint(instruction, token.Trim(), list))
- {
- set = default;
- return false;
- }
- }
-
- set = new ConstraintSet([.. list]);
- return true;
- }
-
- private static bool TryReadToken(ref ReadOnlySpan s, out ReadOnlySpan token)
- {
- s = s.TrimStart();
- if (s.IsEmpty)
- {
- token = default;
- return false;
- }
-
- var separator = s.IndexOf(' ');
- if (separator < 0)
- {
- token = s;
- s = default;
- return true;
- }
-
- token = s[..separator];
- s = s[separator..];
- return true;
- }
-
- private static bool TryParseNpmConstraint(ReadOnlySpan op, ReadOnlySpan s, ICollection constraints)
- {
- if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out var components))
- {
- return false;
- }
-
- switch (op)
- {
- case "^":
- constraints.Add(new Constraint(lower: (version, true), upper: (GetCaretUpperBound(version, components), false)));
- return true;
- case "~":
- constraints.Add(new Constraint(lower: (version, true), upper: (GetTildeUpperBound(version, components), false)));
- return true;
- case ">":
- constraints.Add(new Constraint(Comparator.Greater, version));
- return true;
- case ">=":
- constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
- return true;
- case "<":
- constraints.Add(new Constraint(Comparator.Less, version));
- return true;
- case "<=":
- constraints.Add(new Constraint(Comparator.LessOrEqual, version));
- return true;
- case "!=":
- constraints.Add(new Constraint(Comparator.NotEqual, version));
- return true;
- default:
- switch (components)
- {
- case 3:
- constraints.Add(new Constraint(Comparator.Equal, version));
- return true;
- case 2:
- constraints.Add(new Constraint(lower: (version, true), upper: (new SemanticVersion(version.Major, version.Minor + 1, 0), false)));
- return true;
- case 1:
- constraints.Add(new Constraint(lower: (version, true), upper: (new SemanticVersion(version.Major + 1, 0, 0), false)));
- return true;
- default:
- return false;
- }
- }
- }
-
private ref struct NodeSetGrouping
{
private readonly ReadOnlySpan _span;
@@ -459,4 +141,549 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable s, out ComparatorGroup group)
+ {
+ group = default;
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ // 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))
+ {
+ return true;
+ }
+
+ // Space-separated AND comparators
+ var comparators = new List();
+ var tokens = 0;
+ while (!s.IsEmpty)
+ {
+ // Skip spaces
+ s = s.TrimStart();
+ if (s.IsEmpty)
+ {
+ break;
+ }
+
+ // Find end of this token (next space)
+ var end = s.IndexOf(' ');
+ var token = end >= 0 ? s[..end] : s;
+ tokens++;
+
+ if (IsWildcardToken(token))
+ {
+ if (!TryExpandWildcardComparators(token, comparators))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!TryParseComparator(token, out var comp))
+ {
+ return false;
+ }
+
+ comparators.Add(comp);
+ }
+
+ s = end >= 0 ? s[(end + 1)..] : [];
+ }
+
+ if (comparators.Count == 0 && tokens == 0)
+ {
+ return false;
+ }
+
+ group = new ComparatorGroup([.. comparators]);
+ return true;
+ }
+
+ private static bool TryParseHyphenRange(ReadOnlySpan s, out ComparatorGroup group)
+ {
+ group = default;
+ // Look for " - " (space-hyphen-space)
+ for (var i = 1; i < s.Length - 1; i++)
+ {
+ if (s[i] == '-' && s[i - 1] == ' ' && i + 1 < s.Length && s[i + 1] == ' ')
+ {
+ var lo = s[..(i - 1)].Trim();
+ var hi = s[(i + 2)..].Trim();
+ if (!SemanticVersion.TryParse(lo, null, out var loV))
+ {
+ return false;
+ }
+
+ if (!SemanticVersion.TryParse(hi, null, out var hiV))
+ {
+ return false;
+ }
+
+ group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, loV), new Comparator(ComparatorOp.Lte, hiV)]);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool TryParseCaretRange(ReadOnlySpan s, out ComparatorGroup group)
+ {
+ group = default;
+ if (s.IsEmpty || s[0] is not '^')
+ {
+ return false;
+ }
+
+ s = s[1..];
+ // if (!TryParseWildcardVersion(s, out var version, out var minorWild, out var patchWild))
+ // {
+ // return false;
+ // }
+ if (!SemanticVersion.TryParsePartially(s, out var version, out var comp))
+ {
+ return false;
+ }
+
+ var lo = comp is 0
+ ? new SemanticVersion(version.Major, 0, 0)
+ : comp is 1
+ ? new SemanticVersion(version.Major, version.Minor, 0)
+ : version;
+ SemanticVersion hi;
+
+ if (version.Major > 0)
+ {
+ hi = new SemanticVersion(version.Major + 1, 0, 0);
+ }
+ else if (comp is not 1 && version.Minor > 0)
+ {
+ hi = new SemanticVersion(0, version.Minor + 1, 0);
+ }
+ else if (comp is not 2 and not 1)
+ {
+ hi = new SemanticVersion(0, 0, version.Patch + 1);
+ }
+ else
+ {
+ hi = new SemanticVersion(0, 1, 0);
+ }
+
+ group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]);
+ return true;
+ }
+
+ private static bool TryParseTildeRange(ReadOnlySpan s, out ComparatorGroup group)
+ {
+ group = default;
+ if (s.IsEmpty || s[0] is not '~')
+ {
+ return false;
+ }
+
+
+ s = s[1..];
+ if (!SemanticVersion.TryParsePartially(s, out var lo, out var comp))
+ {
+ return false;
+ }
+
+ SemanticVersion hi;
+
+ if (comp is 1)
+ {
+ hi = new SemanticVersion(lo.Major + 1, 0, 0);
+ }
+ else
+ {
+ hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
+ }
+
+ group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]);
+ return true;
+ }
+
+ private static bool TryParseComparator(ReadOnlySpan token, out Comparator result)
+ {
+ result = default;
+ token = token.Trim();
+ if (token.IsEmpty)
+ {
+ return false;
+ }
+
+ // Parse operator
+ ComparatorOp op;
+ int opLen;
+ if (token.Length >= 2 && token[0] == '!' && token[1] == '=')
+ {
+ op = ComparatorOp.Neq;
+ opLen = 2;
+ }
+ else if (token.Length >= 2 && token[0] == '>' && token[1] == '=')
+ {
+ op = ComparatorOp.Gte;
+ opLen = 2;
+ }
+ else if (token.Length >= 2 && token[0] == '<' && token[1] == '=')
+ {
+ op = ComparatorOp.Lte;
+ opLen = 2;
+ }
+ else
+ {
+ switch (token[0])
+ {
+ case '>':
+ op = ComparatorOp.Gt;
+ opLen = 1;
+ break;
+ case '<':
+ op = ComparatorOp.Lt;
+ opLen = 1;
+ break;
+ case '=':
+ op = ComparatorOp.Eq;
+ opLen = 1;
+ break;
+ default:
+ op = ComparatorOp.Eq;
+ opLen = 0;
+ break;
+ }
+ }
+
+ var vStr = token[opLen..];
+ if (!SemanticVersion.TryParse(vStr, null, out var version))
+ {
+ return false;
+ }
+
+ result = new Comparator(op, version);
+ return true;
+ }
+
+ private static bool IsWildcardToken(ReadOnlySpan s)
+ {
+ // Skip operator chars
+ var i = 0;
+ while (i < s.Length && (s[i] is '>' or '<' or '=' or '~' or '^'))
+ {
+ i++;
+ }
+
+ for (; i < s.Length; i++)
+ {
+ if (s[i] is '*' or 'x' or 'X')
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static ReadOnlySpan StripOp(ReadOnlySpan s, out ComparatorOp op)
+ {
+ op = ComparatorOp.Eq;
+ var i = 0;
+ if (s.Length >= 2)
+ {
+ if (s[0] == '>' && s[1] == '=')
+ {
+ op = ComparatorOp.Gte;
+ return s[2..];
+ }
+
+ if (s[0] == '<' && s[1] == '=')
+ {
+ op = ComparatorOp.Lte;
+ return s[2..];
+ }
+ }
+
+ while (i < s.Length && (s[i] is '>' or '<' or '='))
+ {
+ op = s[i] switch
+ {
+ '>' => ComparatorOp.Gt,
+ '<' => ComparatorOp.Lt,
+ _ => ComparatorOp.Eq,
+ };
+ i++;
+ }
+
+ return s[i..];
+ }
+
+ private static bool TryExpandWildcardComparators(ReadOnlySpan token, List comparators)
+ {
+ var s = StripOp(token.Trim(), out var op);
+ if (op is not ComparatorOp.Eq)
+ {
+ return false;
+ }
+
+ if (!TryParseWildcardVersion(s, out var version, out var minorWild, out var patchWild))
+ {
+ return false;
+ }
+
+ if (!minorWild && !patchWild)
+ {
+ comparators.Add(new Comparator(ComparatorOp.Eq, version));
+ return true;
+ }
+
+ if (s is ['*' or 'x' or 'X'])
+ {
+ return true;
+ }
+
+ comparators.Add(new Comparator(ComparatorOp.Gte, new SemanticVersion(version.Major, version.Minor, version.Patch)));
+ var upper = minorWild
+ ? new SemanticVersion(version.Major + 1, 0, 0)
+ : new SemanticVersion(version.Major, version.Minor + 1, 0);
+ comparators.Add(new Comparator(ComparatorOp.Lt, upper));
+ return true;
+ }
+
+ private static bool TryParseWildcardVersion(ReadOnlySpan s, out SemanticVersion version, out bool minorWild, out bool patchWild)
+ {
+ version = default;
+ minorWild = patchWild = false;
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ // Handle bare * or x/X
+ if (s is ['*' or 'x' or 'X'])
+ {
+ minorWild = patchWild = true;
+ version = new SemanticVersion(0, 0, 0);
+ return true;
+ }
+
+ ulong major = 0, minor = 0, patch = 0;
+ var pos = 0;
+ if (!TryParseNonNegativeInt(s, ref pos, out major))
+ {
+ return false;
+ }
+
+ if (pos >= s.Length)
+ {
+ minorWild = patchWild = true;
+ version = new SemanticVersion(major, 0, 0);
+ return true;
+ }
+
+ if (s[pos] != '.')
+ {
+ return false;
+ }
+
+ pos++;
+
+ if (pos < s.Length && (s[pos] == '*' || s[pos] == 'x' || s[pos] == 'X'))
+ {
+ minorWild = patchWild = true;
+ pos++;
+ version = new SemanticVersion(major, 0, 0);
+ return pos == s.Length;
+ }
+
+ if (!TryParseNonNegativeInt(s, ref pos, out minor))
+ {
+ return false;
+ }
+
+ if (pos >= s.Length)
+ {
+ patchWild = true;
+ version = new SemanticVersion(major, minor, 0);
+ return true;
+ }
+
+ if (s[pos] != '.')
+ {
+ return false;
+ }
+
+ pos++;
+
+ if (pos < s.Length && (s[pos] == '*' || s[pos] == 'x' || s[pos] == 'X'))
+ {
+ patchWild = true;
+ pos++;
+ version = new SemanticVersion(major, minor, 0);
+ return pos == s.Length;
+ }
+
+ if (!TryParseNonNegativeInt(s, ref pos, out patch))
+ {
+ return false;
+ }
+
+ if (pos == s.Length)
+ {
+ version = new SemanticVersion(major, minor, patch);
+ return true;
+ }
+
+ return (s[pos] == '-' || s[pos] == '+') && SemanticVersion.TryParse(s, null, out version);
+ }
+
+ internal static bool TryParseNonNegativeInt(ReadOnlySpan s, ref int pos, out ulong value)
+ {
+ value = 0;
+ var start = pos;
+ while (pos < s.Length && char.IsAsciiDigit(s[pos]))
+ {
+ pos++;
+ }
+
+ if (pos == start)
+ {
+ return false;
+ }
+
+ if (pos - start > 1 && s[start] == '0')
+ {
+ return false; // no leading zeros
+ }
+
+ return ulong.TryParse(s[start..pos], out value);
+ }
+
+
+ // ── Maven parsing ────────────────────────────────────────────────────────
+
+ private static bool TryParseMaven(ReadOnlySpan s, out SemanticVersionRange result)
+ {
+ result = default;
+ var groups = new List();
+
+ while (!s.IsEmpty)
+ {
+ s = s.Trim();
+ if (s.IsEmpty)
+ {
+ break;
+ }
+
+ if (s[0] is not '[' and not '(')
+ {
+ return false;
+ }
+
+ // Find matching close bracket
+ 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)
+ {
+ return false;
+ }
+
+ result = new SemanticVersionRange([.. groups]);
+ return true;
+ }
+
+ private static int FindMavenClose(ReadOnlySpan s)
+ {
+ for (var i = 1; i < s.Length; i++)
+ {
+ if (s[i] is ']' or ')')
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static bool TryParseMavenGroup(ReadOnlySpan s, out ComparatorGroup group)
+ {
+ group = default;
+ var lInclusive = s[0] == '[';
+ var rInclusive = s[^1] == ']';
+
+ var inner = s[1..^1];
+ var commaIdx = inner.IndexOf(',');
+ if (commaIdx < 0)
+ {
+ if (!lInclusive || !rInclusive || !SemanticVersion.TryParsePartially(inner.Trim(), out var exact, out _))
+ {
+ return false;
+ }
+
+ group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, exact)]);
+ return true;
+ }
+
+ var loStr = inner[..commaIdx].Trim();
+ var hiStr = inner[(commaIdx + 1)..].Trim();
+
+ var comps = new List();
+
+ if (!loStr.IsEmpty)
+ {
+ if (!SemanticVersion.TryParsePartially(loStr, out var lo, out _))
+ {
+ return false;
+ }
+
+ comps.Add(new Comparator(lInclusive ? ComparatorOp.Gte : ComparatorOp.Gt, lo));
+ }
+
+ if (!hiStr.IsEmpty)
+ {
+ if (!SemanticVersion.TryParsePartially(hiStr, out var hi, out _))
+ {
+ return false;
+ }
+
+ comps.Add(new Comparator(rInclusive ? ComparatorOp.Lte : ComparatorOp.Lt, hi));
+ }
+
+ if (comps.Count == 0)
+ {
+ group = new ComparatorGroup([]);
+ return true;
+ }
+
+ // Check for exact match [a,a]
+ if (comps is [{ Op: ComparatorOp.Gte }, { Op: ComparatorOp.Lte }] &&
+ comps[0].Version == comps[1].Version)
+ {
+ group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, comps[0].Version)]);
+ return true;
+ }
+
+ group = new ComparatorGroup([.. comps]);
+ return true;
+ }
}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.cs b/src/semver/SemanticVersionRange.cs
index dd620ad..984d216 100644
--- a/src/semver/SemanticVersionRange.cs
+++ b/src/semver/SemanticVersionRange.cs
@@ -1,586 +1,91 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
-using System.Collections.Immutable;
-
namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersionRange
{
- private readonly ImmutableArray _sets;
+ // OR of AND-groups. null == empty range (matches nothing).
+ private readonly ComparatorGroup[]? _groups;
- private SemanticVersionRange(ImmutableArray sets)
+ internal SemanticVersionRange(ComparatorGroup[] groups)
{
- _sets = sets;
+ _groups = groups;
}
- ///
- /// Determines whether the specified semantic version is contained within the range defined by the current instance.
- ///
- /// The semantic version to check against the range.
- ///
- /// True if the version satisfies at least one of the constraint sets in the range; otherwise, false.
- /// If the range has no constraint sets, the method returns true.
- ///
public bool Contains(SemanticVersion version)
{
- if (_sets.Length is 0)
+ if (_groups is null || _groups.Length == 0)
{
- return true;
+ return false;
}
- foreach (var set in _sets)
- {
- if (set.Satisfies(version))
- {
- return true;
- }
- }
+ return _groups.Any(group => group.Includes(version));
+ }
+}
- return false;
+internal enum ComparatorOp { Eq, Neq, Lt, Lte, Gt, Gte }
+
+internal readonly struct Comparator
+{
+ public readonly ComparatorOp Op;
+ public readonly SemanticVersion Version;
+
+ public Comparator(ComparatorOp op, SemanticVersion version)
+ {
+ Op = op;
+ Version = version;
+ }
+
+ public bool Includes(SemanticVersion v)
+ {
+ return Op switch
+ {
+ ComparatorOp.Eq => v == Version,
+ ComparatorOp.Neq => v != Version,
+ ComparatorOp.Lt => v < Version,
+ ComparatorOp.Lte => v <= Version,
+ ComparatorOp.Gt => v > Version,
+ ComparatorOp.Gte => v >= Version,
+ _ => false
+ };
}
- ///
public override string ToString()
{
- return ToString(null, null);
+ return Op switch
+ {
+ ComparatorOp.Neq => $"!={Version}",
+ ComparatorOp.Lt => $"<{Version}",
+ ComparatorOp.Lte => $"<={Version}",
+ ComparatorOp.Gt => $">{Version}",
+ ComparatorOp.Gte => $">={Version}",
+ _ => $"{Version}",
+ };
}
+}
- private readonly struct ConstraintSet
+// One AND-group of comparators (all must be satisfied).
+internal readonly struct ComparatorGroup(Comparator[] comparators)
+{
+ public readonly Comparator[] Comparators = comparators;
+
+ public Comparator? Upper => Comparators.Where(c => c.Op is ComparatorOp.Lt or ComparatorOp.Lte)
+ .Select(it => (Comparator?)it).FirstOrDefault();
+
+ public Comparator? Lower => Comparators.Where(c => c.Op is ComparatorOp.Gt or ComparatorOp.Gte)
+ .Select(it => (Comparator?)it).FirstOrDefault();
+
+ public bool Includes(SemanticVersion v)
{
- public ConstraintSet(ImmutableArray constraints)
+ foreach (var c in Comparators)
{
- Constraints = constraints;
- }
-
- public ImmutableArray Constraints { get; }
-
- public bool Satisfies(SemanticVersion version)
- {
- if (version.Prerelease is not null)
- {
- var allowsPrerelease = false;
- foreach (var constraint in Constraints)
- {
- if (!constraint.CanMatchPrerelease(version))
- {
- continue;
- }
-
- allowsPrerelease = true;
- break;
- }
-
- if (!allowsPrerelease)
- {
- return false;
- }
- }
-
- foreach (var constraint in Constraints)
- {
- if (!constraint.Satisfies(version))
- {
- return false;
- }
- }
-
- return true;
- }
-
- public void AppendFormatted(ref SpanStringBuilder builder, ReadOnlySpan format)
- {
- var effectiveCount = 0;
- Constraint single = default;
-
- foreach (var constraint in Constraints.Where(constraint => !constraint.IsAny))
- {
- effectiveCount++;
- single = constraint;
- }
-
- if (effectiveCount is 0)
- {
- builder.AppendLiteral(format is "m" ? "(,)" : "*");
- return;
- }
-
- if (effectiveCount is 1)
- {
- var snapshot = builder;
- if (single.TryFormat(ref snapshot, format is "m", format is "ns", bareEquals: format is not "m"))
- {
- builder = snapshot;
- return;
- }
-
- if (!snapshot.Success)
- {
- builder = snapshot;
- return;
- }
-
- AppendConstraints(ref builder, bareEquals: false);
- return;
- }
-
- if (format is "m" or "ns")
- {
- var snapshot = builder;
- if (TryCombineBounds(out var combined) &&
- combined.TryFormat(ref snapshot, format is "m", format is "ns", bareEquals: format is not "m"))
- {
- builder = snapshot;
- return;
- }
-
- if (!snapshot.Success)
- {
- builder = snapshot;
- return;
- }
- }
-
- AppendConstraints(ref builder, bareEquals: format is not "m");
- }
-
- private bool TryCombineBounds(out Constraint combined)
- {
- combined = default;
-
- (SemanticVersion Version, bool Inclusive)? lower = null;
- (SemanticVersion Version, bool Inclusive)? upper = null;
- var effectiveCount = 0;
-
- foreach (var constraint in Constraints)
- {
- if (constraint.IsAny)
- {
- continue;
- }
-
- effectiveCount++;
-
- if (constraint.TryGetLowerBound(out var nextLower))
- {
- if (lower is not null)
- {
- return false;
- }
-
- lower = nextLower;
- continue;
- }
-
- if (constraint.TryGetUpperBound(out var nextUpper))
- {
- if (upper is not null)
- {
- return false;
- }
-
- upper = nextUpper;
- continue;
- }
-
- return false;
- }
-
- if (effectiveCount is not 2 || lower is null && upper is null)
+ if (!c.Includes(v))
{
return false;
}
-
- combined = lower is null && upper is null
- ? default
- : new Constraint(lower: lower, upper: upper);
- return true;
}
- private void AppendConstraints(ref SpanStringBuilder builder, bool bareEquals)
- {
- var written = 0;
- foreach (var constraint in Constraints)
- {
- if (constraint.IsAny)
- {
- continue;
- }
-
- if (written > 0)
- {
- builder.AppendFormatted(' ');
- }
-
- constraint.AppendConstraints(ref builder, bareEquals);
-
- written++;
- }
-
- if (written is 0)
- {
- builder.AppendLiteral("*");
- }
- }
- }
-
- private readonly struct Constraint
- {
- private readonly Comparator _comparator;
- private readonly SemanticVersion _version;
- private readonly (SemanticVersion Version, bool Inclusive)? _lower;
- private readonly (SemanticVersion Version, bool Inclusive)? _upper;
-
- public Constraint(Comparator comparator, SemanticVersion version)
- {
- _comparator = comparator;
- _version = version;
- _lower = null;
- _upper = null;
- }
-
- public Constraint((SemanticVersion Version, bool Inclusive)? lower, (SemanticVersion Version, bool Inclusive)? upper)
- {
- _comparator = Comparator.Any;
- _version = default;
- _lower = lower;
- _upper = upper;
- }
-
- public bool IsAny => _comparator is Comparator.Any && _lower is null && _upper is null;
-
- public bool Satisfies(SemanticVersion version)
- {
- if (IsAny)
- {
- return true;
- }
-
- if (_comparator is not Comparator.Any)
- {
- return _comparator switch
- {
- Comparator.Equal => version.CompareTo(_version) is 0,
- Comparator.NotEqual => version.CompareTo(_version) is not 0,
- Comparator.Greater => version.CompareTo(_version) > 0,
- Comparator.GreaterOrEqual => version.CompareTo(_version) >= 0,
- Comparator.Less => version.CompareTo(_version) < 0,
- Comparator.LessOrEqual => version.CompareTo(_version) <= 0,
- _ => false,
- };
- }
-
- return SatisfiesInterval(version);
- }
-
- public bool CanMatchPrerelease(SemanticVersion version)
- {
- if (_comparator is not Comparator.Any)
- {
- return HasMatchingPrerelease(_version, version);
- }
-
- return (_lower is { } lower && HasMatchingPrerelease(lower.Version, version)) ||
- (_upper is { } upper && HasMatchingPrerelease(upper.Version, version));
- }
-
- public bool TryGetLowerBound(out (SemanticVersion Version, bool Inclusive) lower)
- {
- if (_lower is { } intervalLower)
- {
- lower = intervalLower;
- return true;
- }
-
- if (_comparator is not Comparator.Any)
- {
- switch (_comparator)
- {
- case Comparator.Greater:
- lower = (_version, false);
- return true;
- case Comparator.GreaterOrEqual:
- lower = (_version, true);
- return true;
- }
- }
-
- lower = default;
- return false;
- }
-
- public bool TryGetUpperBound(out (SemanticVersion Version, bool Inclusive) upper)
- {
- if (_upper is { } intervalUpper)
- {
- upper = intervalUpper;
- return true;
- }
-
- if (_comparator is not Comparator.Any)
- {
- switch (_comparator)
- {
- case Comparator.Less:
- upper = (_version, false);
- return true;
- case Comparator.LessOrEqual:
- upper = (_version, true);
- return true;
- }
- }
-
- upper = default;
- return false;
- }
-
- public bool TryFormat(ref SpanStringBuilder builder, bool maven, bool shortNpm, bool bareEquals)
- {
- if (IsAny)
- {
- builder.AppendLiteral(maven ? "(,)" : "*");
- return true;
- }
-
- if (_comparator is not Comparator.Any)
- {
- return maven
- ? TryAppendMaven(ref builder)
- : AppendComparator(ref builder, bareEquals);
- }
-
- if (maven)
- {
- AppendMavenRange(ref builder, _lower, _upper);
- return true;
- }
-
- return shortNpm && TryGetShortNpmOperator(out var shortOperator, out var shortVersion)
- ? AppendShortNpm(ref builder, shortOperator, shortVersion)
- : AppendIntervalConstraints(ref builder);
- }
-
- public void AppendConstraints(ref SpanStringBuilder builder, bool bareEquals)
- {
- if (IsAny)
- {
- builder.AppendLiteral("*");
- return;
- }
-
- if (_comparator is not Comparator.Any)
- {
- _ = AppendComparator(ref builder, bareEquals);
- return;
- }
-
- _ = AppendIntervalConstraints(ref builder);
- }
-
- private bool SatisfiesInterval(SemanticVersion version)
- {
- if (_lower is { } lower)
- {
- var lowerComparison = version.CompareTo(lower.Version);
- if (lowerComparison < 0 || lowerComparison is 0 && !lower.Inclusive)
- {
- return false;
- }
- }
-
- if (_upper is { } upper)
- {
- var upperComparison = version.CompareTo(upper.Version);
- if (upperComparison > 0 || upperComparison is 0 && !upper.Inclusive)
- {
- return false;
- }
- }
-
- return true;
- }
-
- private bool TryAppendMaven(ref SpanStringBuilder builder)
- {
- return _comparator switch
- {
- Comparator.Equal => AppendMavenExact(ref builder, _version),
- Comparator.Greater => AppendMavenRange(ref builder, (_version, false), null),
- Comparator.GreaterOrEqual => AppendMavenRange(ref builder, (_version, true), null),
- Comparator.Less => AppendMavenRange(ref builder, null, (_version, false)),
- Comparator.LessOrEqual => AppendMavenRange(ref builder, null, (_version, true)),
- _ => false,
- };
- }
-
- private bool AppendComparator(ref SpanStringBuilder builder, bool bareEquals)
- {
- switch (_comparator)
- {
- case Comparator.Equal when bareEquals:
- builder.AppendFormatted(_version, "f");
- return true;
- case Comparator.Equal:
- builder.AppendLiteral("=");
- builder.AppendFormatted(_version, "f");
- return true;
- case Comparator.Greater:
- builder.AppendLiteral(">");
- builder.AppendFormatted(_version, "f");
- return true;
- case Comparator.GreaterOrEqual:
- builder.AppendLiteral(">=");
- builder.AppendFormatted(_version, "f");
- return true;
- case Comparator.Less:
- builder.AppendLiteral("<");
- builder.AppendFormatted(_version, "f");
- return true;
- case Comparator.LessOrEqual:
- builder.AppendLiteral("<=");
- builder.AppendFormatted(_version, "f");
- return true;
- case Comparator.NotEqual:
- builder.AppendLiteral("!=");
- builder.AppendFormatted(_version, "f");
- return true;
- default:
- return false;
- }
- }
-
- private bool AppendIntervalConstraints(ref SpanStringBuilder builder)
- {
- var written = 0;
-
- if (_lower is { } lower)
- {
- builder.AppendLiteral(lower.Inclusive ? ">=" : ">");
- builder.AppendFormatted(lower.Version, "f");
-
- written++;
- }
-
- if (_upper is { } upper)
- {
- if (written > 0)
- {
- builder.AppendFormatted(' ');
- }
-
- builder.AppendLiteral(upper.Inclusive ? "<=" : "<");
- builder.AppendFormatted(upper.Version, "f");
-
- written++;
- }
-
- if (written > 0)
- {
- return true;
- }
-
- builder.AppendLiteral("*");
- return true;
- }
-
- private static bool AppendMavenExact(ref SpanStringBuilder builder, SemanticVersion version)
- {
- builder.AppendFormatted('[');
- builder.AppendFormatted(version, "f");
- builder.AppendFormatted(']');
- return true;
- }
-
- private static bool AppendMavenRange(ref SpanStringBuilder builder, (SemanticVersion Version, bool Inclusive)? lower, (SemanticVersion Version, bool Inclusive)? upper)
- {
- builder.AppendFormatted(lower is { Inclusive: true } ? '[' : '(');
-
- if (lower is { } lowerBound)
- {
- builder.AppendFormatted(lowerBound.Version, "f");
- }
-
- builder.AppendFormatted(',');
-
- if (upper is { } upperBound)
- {
- builder.AppendFormatted(upperBound.Version, "f");
- }
-
- builder.AppendFormatted(upper is { Inclusive: true } ? ']' : ')');
- return true;
- }
-
- private static bool AppendShortNpm(ref SpanStringBuilder builder, char shortOperator, SemanticVersion shortVersion)
- {
- builder.AppendFormatted(shortOperator);
- builder.AppendFormatted(shortVersion, "f");
- return true;
- }
-
- private bool TryGetShortNpmOperator(out char shortOperator, out SemanticVersion shortVersion)
- {
- if (_lower is { Inclusive: true } lower && _upper is { Inclusive: false } upper)
- {
- if (upper.Version == GetCaretUpperBound(lower.Version))
- {
- shortOperator = '^';
- shortVersion = lower.Version;
- return true;
- }
-
- if (upper.Version == GetTildeUpperBound(lower.Version))
- {
- shortOperator = '~';
- shortVersion = lower.Version;
- return true;
- }
- }
-
- shortOperator = default;
- shortVersion = default;
- return false;
- }
-
- private static bool HasMatchingPrerelease(SemanticVersion candidate, SemanticVersion version)
- {
- return candidate.Prerelease is not null &&
- candidate.Major == version.Major &&
- candidate.Minor == version.Minor &&
- candidate.Patch == version.Patch;
- }
- }
-
- private static SemanticVersion GetCaretUpperBound(SemanticVersion version, int components = 3)
- {
- return components switch
- {
- 1 => new SemanticVersion(version.Major + 1, 0, 0),
- 2 when version.Major is 0 => new SemanticVersion(0, version.Minor + 1, 0),
- 2 => new SemanticVersion(version.Major + 1, 0, 0),
- _ when version.Major > 0 => new SemanticVersion(version.Major + 1, 0, 0),
- _ when version.Minor > 0 => new SemanticVersion(0, version.Minor + 1, 0),
- _ => new SemanticVersion(0, 0, version.Patch + 1),
- };
- }
-
- private static SemanticVersion GetTildeUpperBound(SemanticVersion version, int components = 3)
- {
- return components switch
- {
- >= 2 => new SemanticVersion(version.Major, version.Minor + 1, 0),
- _ => new SemanticVersion(version.Major + 1, 0, 0),
- };
- }
-
- private enum Comparator
- {
- Any,
- Equal,
- Greater,
- GreaterOrEqual,
- Less,
- LessOrEqual,
- NotEqual,
+ return true;
}
}
\ No newline at end of file
diff --git a/src/semver/SpanBuffer.cs b/src/semver/SpanBuffer.cs
new file mode 100644
index 0000000..8c2c3fa
--- /dev/null
+++ b/src/semver/SpanBuffer.cs
@@ -0,0 +1,50 @@
+// Copyright (c) The Geekeey Authors
+// SPDX-License-Identifier: EUPL-1.2
+
+namespace Geekeey.SemVer;
+
+internal ref struct SpanBuffer(Span buffer)
+{
+ private readonly Span _buffer = buffer;
+ public int Written { get; private set; }
+
+ public bool TryWrite(ReadOnlySpan value)
+ {
+ if (!value.TryCopyTo(_buffer[Written..]))
+ {
+ return false;
+ }
+
+ Written += value.Length;
+ return true;
+ }
+
+ public bool TryWrite(T v) where T : ISpanFormattable
+ {
+ if (!v.TryFormat(_buffer[Written..], out var n, [], null))
+ {
+ return false;
+ }
+
+ Written += n;
+ return true;
+ }
+
+ public bool TryWrite(T v, ReadOnlySpan fmt) where T : ISpanFormattable
+ {
+ if (!v.TryFormat(_buffer[Written..], out var n, fmt, null))
+ {
+ return false;
+ }
+
+ Written += n;
+ return true;
+ }
+
+ // Resets position to a saved checkpoint and returns false — useful in || chains.
+ public bool Reset(int checkpoint)
+ {
+ Written = checkpoint;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/semver/SpanStringBuilder.cs b/src/semver/SpanStringBuilder.cs
deleted file mode 100644
index 66f9afa..0000000
--- a/src/semver/SpanStringBuilder.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) The Geekeey Authors
-// SPDX-License-Identifier: EUPL-1.2
-
-namespace Geekeey.SemVer;
-
-internal ref struct SpanStringBuilder
-{
- private Span _destination;
- private readonly IFormatProvider? _provider;
-
- public SpanStringBuilder(Span destination, IFormatProvider? provider = null)
- {
- _destination = destination;
- _provider = provider;
- CharsWritten = 0;
- Success = true;
- }
-
- public int CharsWritten { get; private set; }
-
- public bool Success { get; private set; }
-
- public readonly bool TryComplete(out int charsWritten)
- {
- charsWritten = Success ? CharsWritten : 0;
- return Success;
- }
-
- public void AppendLiteral(string? value)
- {
- AppendLiteral(value.AsSpan());
- }
-
- public void AppendLiteral(ReadOnlySpan value)
- {
- if (!Success)
- {
- return;
- }
-
- if (!value.TryCopyTo(_destination))
- {
- Success = false;
- return;
- }
-
- _destination = _destination[value.Length..];
- CharsWritten += value.Length;
- }
-
- public void AppendFormatted(string? value)
- {
- AppendLiteral(value.AsSpan());
- }
-
- public void AppendFormatted(ReadOnlySpan value)
- {
- AppendLiteral(value);
- }
-
- public void AppendFormatted(T value) where T : ISpanFormattable
- {
- AppendFormatted(value, null);
- }
-
- public void AppendFormatted(T value, string? format) where T : ISpanFormattable
- {
- if (!Success)
- {
- return;
- }
-
- if (!value.TryFormat(_destination, out var written, format, _provider))
- {
- Success = false;
- return;
- }
-
- _destination = _destination[written..];
- CharsWritten += written;
- }
-}
\ No newline at end of file