This commit is contained in:
Louis Seubert 2026-05-20 20:29:53 +02:00
commit fdf1019b7a
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
8 changed files with 775 additions and 896 deletions

View file

@ -109,6 +109,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_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] [Test]
[Arguments("[1.2.3]", "1.2.3", true)] [Arguments("[1.2.3]", "1.2.3", true)]
[Arguments("[1.2.3]", "1.2.4", false)] [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.2.3,)", "9.9.9", true)]
[Arguments("(,1.4.0]", "1.4.0", true)] [Arguments("(,1.4.0]", "1.4.0", true)]
[Arguments("(,1.4.0]", "0.0.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) public async Task I_can_satisfy_maven_ranges(string range, string version, bool expected)
{ {
var r = SemanticVersionRange.Parse(range); var r = SemanticVersionRange.Parse(range);
@ -140,10 +152,10 @@ internal sealed class SemanticVersionRangeTests
} }
[Test] [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.3-beta", true)]
[Arguments("^1.2.3-alpha", "1.2.4", 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) public async Task I_can_satisfy_prerelease_rules(string range, string version, bool expected)
{ {
var r = SemanticVersionRange.Parse(range); 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,2.0.0)", "ns", "^1.2.3")]
[Arguments("[1.2.3,1.3.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.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("*", "m", "(,)")]
[Arguments("(,)", "ns", "*")]
public async Task I_can_convert_range_formats(string range, string format, string expected) public async Task I_can_convert_range_formats(string range, string format, string expected)
{ {
var value = SemanticVersionRange.Parse(range); var value = SemanticVersionRange.Parse(range);

View file

@ -4,6 +4,12 @@ namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersion : ISpanFormattable public readonly partial record struct SemanticVersion : ISpanFormattable
{ {
/// <inheritdoc />
public override string ToString()
{
return ToString(null, null);
}
#region IFormattable #region IFormattable
/// <inheritdoc /> /// <inheritdoc />
@ -49,37 +55,52 @@ public readonly partial record struct SemanticVersion : ISpanFormattable
/// </remarks> /// </remarks>
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{ {
charsWritten = 0;
if (format.IsEmpty) if (format.IsEmpty)
{ {
format = "s"; format = "s";
} }
if (format is not "s" and not "f" and not "r") 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; charsWritten = 0;
return false; return false;
} }
var builder = new SpanStringBuilder(destination, provider); destination = destination[written..];
builder.AppendFormatted(Major); charsWritten += written;
builder.AppendLiteral(".");
builder.AppendFormatted(Minor);
builder.AppendLiteral(".");
builder.AppendFormatted(Patch);
if (Prerelease is { Length: > 0 } && format is "s" or "f") if (Prerelease is { Length: > 0 } && format is "s" or "f")
{ {
builder.AppendLiteral("-"); if (!destination.TryWrite(provider, $"-{Prerelease}", out written))
builder.AppendFormatted(Prerelease); {
charsWritten = 0;
return false;
}
destination = destination[written..];
charsWritten += written;
} }
if (Metadata is { Length: > 0 } && format is "f") if (Metadata is { Length: > 0 } && format is "f")
{ {
builder.AppendLiteral("+"); if (!destination.TryWrite(provider, $"+{Metadata}", out written))
builder.AppendFormatted(Metadata); {
charsWritten = 0;
return false;
}
destination = destination[written..];
charsWritten += written;
} }
return builder.TryComplete(out charsWritten); return true;
} }
#endregion #endregion

View file

@ -102,10 +102,4 @@ public readonly partial record struct SemanticVersion
/// details. It does not affect the precedence of the version. /// details. It does not affect the precedence of the version.
/// </summary> /// </summary>
public string? Metadata { get; } public string? Metadata { get; }
/// <inheritdoc />
public override string ToString()
{
return ToString(null, null);
}
} }

View file

@ -7,6 +7,12 @@ namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersionRange : ISpanFormattable public readonly partial record struct SemanticVersionRange : ISpanFormattable
{ {
/// <inheritdoc />
public override string ToString()
{
return ToString(null, null);
}
#region IFormattable #region IFormattable
/// <inheritdoc /> /// <inheritdoc />
@ -38,6 +44,8 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
/// <inheritdoc /> /// <inheritdoc />
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{ {
charsWritten = 0;
if (format.IsEmpty) if (format.IsEmpty)
{ {
format = "ns"; format = "ns";
@ -45,31 +53,173 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
if (format is not "m" and not "n" and not "ns") if (format is not "m" and not "n" and not "ns")
{ {
charsWritten = 0; throw new FormatException($"The format string '{format}' is not supported.");
return false;
} }
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 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 #endregion
private static bool TryWriteComparator(ref SpanBuffer buf, Comparator c)
{
ReadOnlySpan<char> 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);
}
} }

View file

@ -82,348 +82,30 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return false; return false;
} }
return s[0] is '[' or '(' ? TryParseJava(s, out result) : TryParseNode(s, out result); return s[0] is '[' or '(' ? TryParseMaven(s, out result) : TryParseNpm(s, out result);
} }
#endregion #endregion
private static bool TryParseJava(ReadOnlySpan<char> s, out SemanticVersionRange result) private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
{ {
var sets = new List<ConstraintSet>(); result = default;
var groups = new List<ComparatorGroup>();
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<char> 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<char> 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<char> _span;
private int _currentStart;
private int _currentEnd;
public JavaSetGrouping(ReadOnlySpan<char> 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<char> s, out SemanticVersionRange result)
{
var sets = new List<ConstraintSet>();
foreach (var range in new NodeSetGrouping(s)) 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; return false;
} }
sets.Add(set); groups.Add(group);
} }
result = new SemanticVersionRange([.. sets]); result = new SemanticVersionRange([.. groups]);
return true; return true;
} }
private static bool TryParseNodeSet(ReadOnlySpan<char> 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<Constraint>(2);
var current = s;
while (TryReadToken(ref current, out var token))
{
scoped ReadOnlySpan<char> 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<char> s, out ReadOnlySpan<char> 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<char> op, ReadOnlySpan<char> s, ICollection<Constraint> 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 ref struct NodeSetGrouping
{ {
private readonly ReadOnlySpan<char> _span; private readonly ReadOnlySpan<char> _span;
@ -459,4 +141,549 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
return this; return this;
} }
} }
private static bool TryParseNpmGroup(ReadOnlySpan<char> 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<Comparator>();
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<char> 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<char> 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<char> 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<char> 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<char> 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<char> StripOp(ReadOnlySpan<char> 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<char> token, List<Comparator> 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<char> 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<char> 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<char> s, out SemanticVersionRange result)
{
result = default;
var groups = new List<ComparatorGroup>();
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<char> s)
{
for (var i = 1; i < s.Length; i++)
{
if (s[i] is ']' or ')')
{
return i;
}
}
return -1;
}
private static bool TryParseMavenGroup(ReadOnlySpan<char> 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<Comparator>();
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;
}
} }

View file

@ -1,586 +1,91 @@
// Copyright (c) The Geekeey Authors // Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
using System.Collections.Immutable;
namespace Geekeey.SemVer; namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersionRange public readonly partial record struct SemanticVersionRange
{ {
private readonly ImmutableArray<ConstraintSet> _sets; // OR of AND-groups. null == empty range (matches nothing).
private readonly ComparatorGroup[]? _groups;
private SemanticVersionRange(ImmutableArray<ConstraintSet> sets) internal SemanticVersionRange(ComparatorGroup[] groups)
{ {
_sets = sets; _groups = groups;
} }
/// <summary>
/// Determines whether the specified semantic version is contained within the range defined by the current instance.
/// </summary>
/// <param name="version">The semantic version to check against the range.</param>
/// <returns>
/// 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.
/// </returns>
public bool Contains(SemanticVersion version) 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) return _groups.Any(group => group.Includes(version));
{ }
if (set.Satisfies(version)) }
{
return true;
}
}
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
};
} }
/// <inheritdoc />
public override string ToString() 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<Constraint> constraints) foreach (var c in Comparators)
{ {
Constraints = constraints; if (!c.Includes(v))
}
public ImmutableArray<Constraint> 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<char> 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)
{ {
return false; 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) return true;
{
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,
} }
} }

50
src/semver/SpanBuffer.cs Normal file
View file

@ -0,0 +1,50 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.SemVer;
internal ref struct SpanBuffer(Span<char> buffer)
{
private readonly Span<char> _buffer = buffer;
public int Written { get; private set; }
public bool TryWrite(ReadOnlySpan<char> value)
{
if (!value.TryCopyTo(_buffer[Written..]))
{
return false;
}
Written += value.Length;
return true;
}
public bool TryWrite<T>(T v) where T : ISpanFormattable
{
if (!v.TryFormat(_buffer[Written..], out var n, [], null))
{
return false;
}
Written += n;
return true;
}
public bool TryWrite<T>(T v, ReadOnlySpan<char> 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;
}
}

View file

@ -1,82 +0,0 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.SemVer;
internal ref struct SpanStringBuilder
{
private Span<char> _destination;
private readonly IFormatProvider? _provider;
public SpanStringBuilder(Span<char> 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<char> 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<char> value)
{
AppendLiteral(value);
}
public void AppendFormatted<T>(T value) where T : ISpanFormattable
{
AppendFormatted(value, null);
}
public void AppendFormatted<T>(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;
}
}