diff --git a/.forgejo/workflows/default.yml b/.forgejo/workflows/default.yml
index a007574..4d33189 100644
--- a/.forgejo/workflows/default.yml
+++ b/.forgejo/workflows/default.yml
@@ -34,6 +34,10 @@ jobs:
run: |
dotnet pack -p:ContinuousIntegrationBuild=true
+ - name: dotnet format --verify-no-changes
+ run: |
+ dotnet format --no-restore --verify-no-changes --verbosity normal
+
- name: dotnet test
run: |
dotnet test -p:ContinuousIntegrationBuild=true
\ No newline at end of file
diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml
index 603a9d6..0c7b1dc 100644
--- a/.forgejo/workflows/release.yml
+++ b/.forgejo/workflows/release.yml
@@ -25,6 +25,10 @@ jobs:
run: |
dotnet pack -p:ContinuousIntegrationBuild=true
+ - name: dotnet format --verify-no-changes
+ run: |
+ dotnet format --no-restore --verify-no-changes --verbosity normal
+
- name: dotnet test
run: |
dotnet test -p:ContinuousIntegrationBuild=true
diff --git a/src/semver.tests/SemanticVersionRangeTests.cs b/src/semver.tests/SemanticVersionRangeTests.cs
index 8f8de02..f87225a 100644
--- a/src/semver.tests/SemanticVersionRangeTests.cs
+++ b/src/semver.tests/SemanticVersionRangeTests.cs
@@ -1,6 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
+using System.Text;
+
namespace Geekeey.SemVer.Tests;
internal sealed class SemanticVersionRangeTests
@@ -152,7 +154,7 @@ internal sealed class SemanticVersionRangeTests
[Test]
public async Task I_can_serialize_to_json()
{
- var r = SemanticVersionRange.Parse("^1.2.3");
+ var r = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
var json = System.Text.Json.JsonSerializer.Serialize(r);
await Assert.That(json).IsEqualTo("\"^1.2.3\"");
}
@@ -165,4 +167,37 @@ internal sealed class SemanticVersionRangeTests
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
await Assert.That(r.Satisfies(new SemanticVersion(1, 2, 4))).IsTrue();
}
+
+ [Test]
+ public async Task I_use_npm_short_format_by_default()
+ {
+ var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
+ await Assert.That(value.ToString()).IsEqualTo("^1.2.3");
+ }
+
+ [Test]
+ [Arguments("^1.2.3", "m", "[1.2.3,2.0.0)")]
+ [Arguments("~1.2.3", "m", "[1.2.3,1.3.0)")]
+ [Arguments("1.2.3", "m", "[1.2.3]")]
+ [Arguments("[1.2.3,2.0.0)", "n", ">=1.2.3 <2.0.0")]
+ [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("*", "m", "(,)")]
+ public async Task I_can_convert_range_formats(string range, string format, string expected)
+ {
+ var value = SemanticVersionRange.Parse(range);
+ await Assert.That(value.ToString(format, null)).IsEqualTo(expected);
+ }
+
+ [Test]
+ public async Task I_can_format_ranges_to_chars()
+ {
+ var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
+ var destination = new char[32];
+ var success = value.TryFormat(destination, out var charsWritten, "ns", null);
+
+ await Assert.That(success).IsTrue();
+ await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
+ }
}
\ No newline at end of file
diff --git a/src/semver.tests/SemanticVersionTests.cs b/src/semver.tests/SemanticVersionTests.cs
index c497cc6..3494fd0 100644
--- a/src/semver.tests/SemanticVersionTests.cs
+++ b/src/semver.tests/SemanticVersionTests.cs
@@ -13,6 +13,7 @@ internal sealed class SemanticVersionTests
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
+
[Test]
[Arguments(0, 0, 0, 0)]
[Arguments(1, 1, 1, 0)]
@@ -137,17 +138,6 @@ internal sealed class SemanticVersionTests
await Assert.That(result.Metadata).IsEqualTo("build.1");
}
- [Test]
- public async Task I_can_parse_from_utf8()
- {
- var utf8 = "2.0.0-rc.1"u8;
- var result = SemanticVersion.Parse(utf8);
- await Assert.That(result.Major).IsEqualTo(2UL);
- await Assert.That(result.Minor).IsEqualTo(0UL);
- await Assert.That(result.Patch).IsEqualTo(0UL);
- await Assert.That(result.Prerelease).IsEqualTo("rc.1");
- }
-
[Test]
[Arguments(1, 2, 3, null, null, "1.2.3")]
[Arguments(1, 2, 3, "alpha", null, "1.2.3-alpha")]
@@ -172,17 +162,6 @@ internal sealed class SemanticVersionTests
await Assert.That(new string(dest[..charsWritten])).IsEqualTo("1.2.3-beta+456");
}
- [Test]
- public async Task I_can_format_to_utf8()
- {
- var v = new SemanticVersion(1, 2, 3, "beta", "456");
- var dest = new byte[32];
- var success = v.TryFormat(dest, out var bytesWritten, "f", null);
-
- await Assert.That(success).IsTrue();
- await Assert.That(Encoding.UTF8.GetString(dest[..bytesWritten])).IsEqualTo("1.2.3-beta+456");
- }
-
[Test]
[Arguments(1, 2, 3, null, null, "\"1.2.3\"")]
[Arguments(1, 2, 3, "alpha", null, "\"1.2.3-alpha\"")]
@@ -221,10 +200,7 @@ internal sealed class SemanticVersionTests
[Test]
public async Task I_can_serialize_as_part_of_object()
{
- 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);
await Assert.That(json).IsEqualTo("{\"Version\":\"1.0.0-rc.1+metadata\"}");
diff --git a/src/semver/Geekeey.SemVer.csproj b/src/semver/Geekeey.SemVer.csproj
index 2ad4851..3851488 100644
--- a/src/semver/Geekeey.SemVer.csproj
+++ b/src/semver/Geekeey.SemVer.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/semver/SemanticVersion.Comparison.cs b/src/semver/SemanticVersion.Comparison.cs
new file mode 100644
index 0000000..b6e618f
--- /dev/null
+++ b/src/semver/SemanticVersion.Comparison.cs
@@ -0,0 +1,48 @@
+namespace Geekeey.SemVer;
+
+public readonly partial record struct SemanticVersion : IComparable, IComparable
+{
+ ///
+ public int CompareTo(object? obj)
+ {
+ return obj is not SemanticVersion other ? 1 : CompareTo(other);
+ }
+
+ ///
+ public int CompareTo(SemanticVersion other)
+ {
+ return SemanticVersionComparer.Priority.Compare(this, other);
+ }
+
+ ///
+ /// Determines whether one version is less than another version.
+ ///
+ public static bool operator <(SemanticVersion left, SemanticVersion right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ ///
+ /// Determines whether one version is less than or equal to another version.
+ ///
+ public static bool operator <=(SemanticVersion left, SemanticVersion right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ ///
+ /// Determines whether one version is greater than another version.
+ ///
+ public static bool operator >(SemanticVersion left, SemanticVersion right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ ///
+ /// Determines whether one version is greater than or equal to another version.
+ ///
+ public static bool operator >=(SemanticVersion left, SemanticVersion right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+}
diff --git a/src/semver/SemanticVersion.Formatting.cs b/src/semver/SemanticVersion.Formatting.cs
new file mode 100644
index 0000000..c6a0992
--- /dev/null
+++ b/src/semver/SemanticVersion.Formatting.cs
@@ -0,0 +1,114 @@
+using System.Buffers;
+using System.Globalization;
+
+namespace Geekeey.SemVer;
+
+public readonly partial record struct SemanticVersion : ISpanFormattable
+{
+ internal int RequiredBufferSize
+ {
+ get
+ {
+ var length = 0;
+ length += ulong.CountDigits(Major) + 1;
+ length += ulong.CountDigits(Minor) + 1;
+ length += ulong.CountDigits(Patch);
+ length += Prerelease is { Length: > 0 } ? 1 + Prerelease.Length : 0;
+ length += Metadata is { Length: > 0 } ? 1 + Metadata.Length : 0;
+ return length;
+ }
+ }
+
+ #region IFormattable
+
+ ///
+ ///
+ /// s - Default SemVer - [1.2.3-beta.4]
+ /// f - Full SemVer - [1.2.3-beta.4+5]
+ /// r - Just the SemVer part relevant for compatibility comparison - [1.2.3]
+ ///
+ public string ToString(string? format, IFormatProvider? formatProvider)
+ {
+ var capacity = RequiredBufferSize;
+ var shared = capacity > 256 ? ArrayPool.Shared.Rent(capacity) : null;
+ try
+ {
+ scoped var buffer = shared.AsSpan();
+
+ if (shared is null)
+ {
+ buffer = stackalloc char[capacity];
+ }
+
+ _ = TryFormat(buffer, out var bytesWritten, format, formatProvider);
+
+ return new string(buffer[..bytesWritten]);
+ }
+ finally
+ {
+ if (shared is not null)
+ {
+ ArrayPool.Shared.Return(shared);
+ }
+ }
+ }
+
+ #endregion
+
+ #region ISpanFormattable
+
+ ///
+ /// Tries to format the semantic version into the specified span of characters.
+ ///
+ /// ISpanFormattable.TryFormat
+ public bool TryFormat(Span destination, out int charsWritten)
+ {
+ return TryFormat(destination, out charsWritten, default, null);
+ }
+
+ ///
+ public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
+ {
+ if (!destination.TryWrite(NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out charsWritten))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ destination = destination[charsWritten..];
+
+ if (Prerelease is { Length: > 0 } && format is "s" or "f")
+ {
+ destination[0] = '-';
+
+ if (!Prerelease.AsSpan().TryCopyTo(destination[1..]))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ destination = destination[(Prerelease.Length + 1)..];
+ charsWritten += Prerelease.Length + 1;
+ }
+
+ if (Metadata is { Length: > 0 } && format is "f")
+ {
+ destination[0] = '+';
+
+ if (!Metadata.AsSpan().TryCopyTo(destination[1..]))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ destination = destination[(Metadata.Length + 1)..];
+ charsWritten += Metadata.Length + 1;
+ }
+
+ _ = destination;
+
+ return true;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersion.Parsing.cs b/src/semver/SemanticVersion.Parsing.cs
new file mode 100644
index 0000000..fc3a0cf
--- /dev/null
+++ b/src/semver/SemanticVersion.Parsing.cs
@@ -0,0 +1,132 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace Geekeey.SemVer;
+
+public readonly partial record struct SemanticVersion : ISpanParsable
+{
+ #region IParsable
+
+ ///
+ /// Parses a string into a .
+ ///
+ ///
+ public static SemanticVersion Parse(string s)
+ {
+ return Parse(s.AsSpan(), null);
+ }
+
+ ///
+ public static SemanticVersion Parse(string s, IFormatProvider? provider)
+ {
+ return Parse(s.AsSpan(), provider);
+ }
+
+ ///
+ /// Tries to parse a string into a .
+ ///
+ ///
+ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersion result)
+ {
+ return TryParse(s, null, out result);
+ }
+
+ ///
+ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
+ {
+ return TryParse(s.AsSpan(), provider, out result);
+ }
+
+ #endregion
+
+ #region ISpanParsable
+
+ ///
+ /// Parses a span of characters into a .
+ ///
+ ///
+ public static SemanticVersion Parse(ReadOnlySpan s)
+ {
+ return Parse(s, null);
+ }
+
+ ///
+ public static SemanticVersion Parse(ReadOnlySpan s, IFormatProvider? provider)
+ {
+ if (!TryParse(s, provider, out var version))
+ {
+ throw new FormatException($"The input string '{s}' was not in a correct format.");
+ }
+
+ return version;
+ }
+
+ ///
+ /// Tries to parse a span of characters into a .
+ ///
+ ///
+ public static bool TryParse(ReadOnlySpan s, [MaybeNullWhen(false)] out SemanticVersion result)
+ {
+ return TryParse(s, null, out result);
+ }
+
+ ///
+ public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
+ {
+ result = default;
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ var metadata = default(string);
+ var plusIndex = s.IndexOf('+');
+ if (plusIndex >= 0)
+ {
+ metadata = s[(plusIndex + 1)..].ToString();
+ s = s[..plusIndex];
+ }
+
+ var prerelease = default(string);
+ var dashIndex = s.IndexOf('-');
+ if (dashIndex >= 0)
+ {
+ prerelease = s[(dashIndex + 1)..].ToString();
+ s = s[..dashIndex];
+ }
+
+ var firstDot = s.IndexOf('.');
+ if (firstDot < 0)
+ {
+ return false;
+ }
+
+ if (!ulong.TryParse(s[..firstDot], NumberStyles.None, CultureInfo.InvariantCulture, out var major))
+ {
+ return false;
+ }
+
+ s = s[(firstDot + 1)..];
+ var secondDot = s.IndexOf('.');
+ if (secondDot < 0)
+ {
+ return false;
+ }
+
+ if (!ulong.TryParse(s[..secondDot], NumberStyles.None, CultureInfo.InvariantCulture, out var minor))
+ {
+ return false;
+ }
+
+ s = s[(secondDot + 1)..];
+ if (!ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var patch))
+ {
+ return false;
+ }
+
+ result = new SemanticVersion(major, minor, patch, prerelease, metadata);
+ return true;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersion.cs b/src/semver/SemanticVersion.cs
index c156d88..a2a9de5 100644
--- a/src/semver/SemanticVersion.cs
+++ b/src/semver/SemanticVersion.cs
@@ -1,9 +1,4 @@
-using System.Buffers;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Text;
using System.Text.Json.Serialization;
-using System.Text.Unicode;
namespace Geekeey.SemVer;
@@ -117,394 +112,3 @@ public readonly partial record struct SemanticVersion
return ToString(null, null);
}
}
-
-public readonly partial record struct SemanticVersion : IComparable, IComparable
-{
- ///
- public int CompareTo(object? obj)
- {
- return obj is not SemanticVersion other ? 1 : CompareTo(other);
- }
-
- ///
- public int CompareTo(SemanticVersion other)
- {
- return SemanticVersionComparer.Priority.Compare(this, other);
- }
-
- ///
- /// Determines whether one version is less than another version.
- ///
- public static bool operator <(SemanticVersion left, SemanticVersion right)
- {
- return left.CompareTo(right) < 0;
- }
-
- ///
- /// Determines whether one version is less than or equal to another version.
- ///
- public static bool operator <=(SemanticVersion left, SemanticVersion right)
- {
- return left.CompareTo(right) <= 0;
- }
-
- ///
- /// Determines whether one version is greater than another version.
- ///
- public static bool operator >(SemanticVersion left, SemanticVersion right)
- {
- return left.CompareTo(right) > 0;
- }
-
- ///
- /// Determines whether one version is greater than or equal to another version.
- ///
- public static bool operator >=(SemanticVersion left, SemanticVersion right)
- {
- return left.CompareTo(right) >= 0;
- }
-}
-
-public readonly partial record struct SemanticVersion : ISpanParsable, IUtf8SpanParsable
-{
- #region IParsable
-
- ///
- /// Parses a string into a .
- ///
- ///
- public static SemanticVersion Parse(string s)
- {
- return Parse(s.AsSpan(), null);
- }
-
- ///
- public static SemanticVersion Parse(string s, IFormatProvider? provider)
- {
- return Parse(s.AsSpan(), provider);
- }
-
- ///
- /// Tries to parse a string into a .
- ///
- ///
- public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersion result)
- {
- return TryParse(s, null, out result);
- }
-
- ///
- public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
- {
- return TryParse(s.AsSpan(), provider, out result);
- }
-
- #endregion
-
- #region ISpanParsable
-
- ///
- /// Parses a span of characters into a .
- ///
- ///
- public static SemanticVersion Parse(ReadOnlySpan s)
- {
- return Parse(s, null);
- }
-
- ///
- public static SemanticVersion Parse(ReadOnlySpan s, IFormatProvider? provider)
- {
- if (!TryParse(s, provider, out var version))
- {
- throw new FormatException($"The input string '{s}' was not in a correct format.");
- }
-
- return version;
- }
-
- ///
- /// Tries to parse a span of characters into a .
- ///
- ///
- public static bool TryParse(ReadOnlySpan s, [MaybeNullWhen(false)] out SemanticVersion result)
- {
- return TryParse(s, null, out result);
- }
-
- ///
- public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
- {
- result = default;
- if (s.IsEmpty)
- {
- return false;
- }
-
- var metadata = default(string);
- var plusIndex = s.IndexOf('+');
- if (plusIndex >= 0)
- {
- metadata = s[(plusIndex + 1)..].ToString();
- s = s[..plusIndex];
- }
-
- var prerelease = default(string);
- var dashIndex = s.IndexOf('-');
- if (dashIndex >= 0)
- {
- prerelease = s[(dashIndex + 1)..].ToString();
- s = s[..dashIndex];
- }
-
- var firstDot = s.IndexOf('.');
- if (firstDot < 0)
- {
- return false;
- }
-
- if (!ulong.TryParse(s[..firstDot], NumberStyles.None, CultureInfo.InvariantCulture, out var major))
- {
- return false;
- }
-
- s = s[(firstDot + 1)..];
- var secondDot = s.IndexOf('.');
- if (secondDot < 0)
- {
- return false;
- }
-
- if (!ulong.TryParse(s[..secondDot], NumberStyles.None, CultureInfo.InvariantCulture, out var minor))
- {
- return false;
- }
-
- s = s[(secondDot + 1)..];
- if (!ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var patch))
- {
- return false;
- }
-
- result = new SemanticVersion(major, minor, patch, prerelease, metadata);
- return true;
- }
-
- #endregion
-
- #region IUtf8SpanParsable
-
- ///
- /// Parses a span of UTF-8 bytes into a .
- ///
- ///
- public static SemanticVersion Parse(ReadOnlySpan utf8Text)
- {
- return Parse(utf8Text, null);
- }
-
- ///
- public static SemanticVersion Parse(ReadOnlySpan utf8Text, IFormatProvider? provider)
- {
- if (!TryParse(utf8Text, provider, out var version))
- {
- throw new FormatException($"The input string '{Encoding.UTF8.GetString(utf8Text)}' was not in a correct format.");
- }
-
- return version;
- }
-
- ///
- /// Tries to parse a span of UTF-8 bytes into a .
- ///
- ///
- public static bool TryParse(ReadOnlySpan utf8Text, [MaybeNullWhen(false)] out SemanticVersion result)
- {
- return TryParse(utf8Text, null, out result);
- }
-
- ///
- public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
- {
- if (utf8Text.IsEmpty)
- {
- result = default;
- return false;
- }
-
- var charCount = Encoding.UTF8.GetCharCount(utf8Text);
- var chars = charCount <= 256 ? stackalloc char[charCount] : new char[charCount];
- Encoding.UTF8.GetChars(utf8Text, chars);
-
- return TryParse(chars, provider, out result);
- }
-
- #endregion
-}
-
-public readonly partial record struct SemanticVersion : ISpanFormattable, IUtf8SpanFormattable
-{
- internal int RequiredBufferSize
- {
- get
- {
- var length = 0;
- length += ulong.CountDigits(Major) + 1;
- length += ulong.CountDigits(Minor) + 1;
- length += ulong.CountDigits(Patch);
- length += Prerelease is { Length: > 0 } ? 1 + Prerelease.Length : 0;
- length += Metadata is { Length: > 0 } ? 1 + Metadata.Length : 0;
- return length;
- }
- }
-
- #region IFormattable
-
- ///
- ///
- /// s - Default SemVer - [1.2.3-beta.4]
- /// f - Full SemVer - [1.2.3-beta.4+5]
- /// r - Just the SemVer part relevant for compatibility comparison - [1.2.3]
- ///
- public string ToString(string? format, IFormatProvider? formatProvider)
- {
- var capacity = RequiredBufferSize;
- var shared = capacity > 256 ? ArrayPool.Shared.Rent(capacity) : null;
- try
- {
- scoped var buffer = shared.AsSpan();
-
- if (shared is null)
- {
- buffer = stackalloc char[capacity];
- }
-
- _ = TryFormat(buffer, out var bytesWritten, format, formatProvider);
-
- return new string(buffer[..bytesWritten]);
- }
- finally
- {
- if (shared is not null)
- {
- ArrayPool.Shared.Return(shared);
- }
- }
- }
-
- #endregion
-
- #region ISpanFormattable
-
- ///
- /// Tries to format the semantic version into the specified span of characters.
- ///
- /// ISpanFormattable.TryFormat
- public bool TryFormat(Span destination, out int charsWritten)
- {
- return TryFormat(destination, out charsWritten, default, null);
- }
-
- ///
- public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
- {
- if (!destination.TryWrite(NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out charsWritten))
- {
- charsWritten = 0;
- return false;
- }
-
- destination = destination[charsWritten..];
-
- if (Prerelease is { Length: > 0 } && format is "s" or "f")
- {
- destination[0] = '-';
-
- if (!Prerelease.AsSpan().TryCopyTo(destination[1..]))
- {
- charsWritten = 0;
- return false;
- }
-
- destination = destination[(Prerelease.Length + 1)..];
- charsWritten += Prerelease.Length + 1;
- }
-
- if (Metadata is { Length: > 0 } && format is "f")
- {
- destination[0] = '+';
-
- if (!Metadata.AsSpan().TryCopyTo(destination[1..]))
- {
- charsWritten = 0;
- return false;
- }
-
- destination = destination[(Metadata.Length + 1)..];
- charsWritten += Metadata.Length + 1;
- }
-
- _ = destination;
-
- return true;
- }
-
- #endregion
-
- #region IUtf8SpanFormattable
-
- ///
- /// Tries to format the semantic version into the specified span of UTF-8 bytes.
- ///
- /// IUtf8SpanFormattable.TryFormat
- public bool TryFormat(Span utf8Destination, out int bytesWritten)
- {
- return TryFormat(utf8Destination, out bytesWritten, default, null);
- }
-
- ///
- public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider)
- {
- if (!Utf8.TryWrite(utf8Destination, NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out bytesWritten))
- {
- bytesWritten = 0;
- return false;
- }
-
- utf8Destination = utf8Destination[bytesWritten..];
-
- if (Prerelease is { Length: > 0 } && format is "s" or "f")
- {
- utf8Destination[0] = (byte)'-';
-
- if (Utf8.FromUtf16(Prerelease, utf8Destination[1..], out _, out var b) is not OperationStatus.Done)
- {
- bytesWritten = 0;
- return false;
- }
-
- utf8Destination = utf8Destination[(b + 1)..];
- bytesWritten += b + 1;
- }
-
- if (Metadata is { Length: > 0 } && format is "f")
- {
- utf8Destination[0] = (byte)'+';
-
- if (Utf8.FromUtf16(Metadata, utf8Destination[1..], out _, out var b) is not OperationStatus.Done)
- {
- bytesWritten = 0;
- return false;
- }
-
- utf8Destination = utf8Destination[(b + 1)..];
- bytesWritten += b + 1;
- }
-
- _ = utf8Destination;
-
- return true;
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionJsonConverter.cs b/src/semver/SemanticVersionJsonConverter.cs
index 3f2ea50..4921d00 100644
--- a/src/semver/SemanticVersionJsonConverter.cs
+++ b/src/semver/SemanticVersionJsonConverter.cs
@@ -25,7 +25,7 @@ internal sealed class SemanticVersionJsonConverter : JsonConverter StackBufferThreshold ? ArrayPool.Shared.Rent(capacity) : null;
+ var shared = capacity > StackBufferThreshold ? ArrayPool.Shared.Rent(capacity) : null;
try
{
scoped var buffer = shared.AsSpan();
if (shared is null)
{
- buffer = stackalloc byte[capacity];
+ buffer = stackalloc char[capacity];
}
_ = value.TryFormat(buffer, out var bytesWritten, "f", null);
@@ -54,7 +54,7 @@ internal sealed class SemanticVersionJsonConverter : JsonConverter.Shared.Return(shared);
+ ArrayPool.Shared.Return(shared);
}
}
}
diff --git a/src/semver/SemanticVersionRange.Formatting.cs b/src/semver/SemanticVersionRange.Formatting.cs
new file mode 100644
index 0000000..f41b546
--- /dev/null
+++ b/src/semver/SemanticVersionRange.Formatting.cs
@@ -0,0 +1,465 @@
+// Copyright (c) The Geekeey Authors
+// SPDX-License-Identifier: EUPL-1.2
+
+using System.Buffers;
+using System.Text.Unicode;
+
+namespace Geekeey.SemVer;
+
+public readonly partial record struct SemanticVersionRange : ISpanFormattable
+{
+ private const int StackBufferThreshold = 256;
+
+ #region IFormattable
+
+ ///
+ public string ToString(string? format, IFormatProvider? formatProvider)
+ {
+ if (!TryGetFormat(format, out var rangeFormat))
+ {
+ throw new FormatException($"The format string '{format}' was not recognized.");
+ }
+
+ var capacity = GetRequiredBufferSize(rangeFormat);
+ var shared = capacity > StackBufferThreshold ? ArrayPool.Shared.Rent(capacity) : null;
+ try
+ {
+ scoped var buffer = shared.AsSpan();
+
+ if (shared is null)
+ {
+ buffer = stackalloc char[capacity];
+ }
+
+ _ = TryFormatCore(buffer, out var charsWritten, rangeFormat);
+ return new string(buffer[..charsWritten]);
+ }
+ finally
+ {
+ if (shared is not null)
+ {
+ ArrayPool.Shared.Return(shared);
+ }
+ }
+ }
+
+ #endregion
+
+ #region ISpanFormattable
+
+ ///
+ public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
+ {
+ if (!TryGetFormat(format, out var rangeFormat))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ return TryFormatCore(destination, out charsWritten, rangeFormat);
+ }
+
+ #endregion
+
+ private bool TryFormatCore(Span destination, out int charsWritten, RangeStringFormat format)
+ {
+ charsWritten = 0;
+
+ if (_sets is not { Length: > 0 } sets)
+ {
+ return TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "(,)" : "*");
+ }
+
+ for (var i = 0; i < sets.Length; i++)
+ {
+ if (i > 0 && !TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "," : " || "))
+ {
+ charsWritten = 0;
+ return false;
+ }
+
+ if (!TryWriteSet(ref destination, ref charsWritten, sets[i], format))
+ {
+ charsWritten = 0;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private int GetRequiredBufferSize(RangeStringFormat format)
+ {
+ if (_sets is not { Length: > 0 } sets)
+ {
+ return format is RangeStringFormat.Maven ? 3 : 1;
+ }
+
+ var length = 0;
+ for (var i = 0; i < sets.Length; i++)
+ {
+ if (i > 0)
+ {
+ length += format is RangeStringFormat.Maven ? 1 : 4;
+ }
+
+ length += GetSetRequiredBufferSize(sets[i], format);
+ }
+
+ return length;
+ }
+
+ private static int GetSetRequiredBufferSize(VersionConstraint[] set, RangeStringFormat format)
+ {
+ AnalyzeSet(set, out var effectiveCount, out var hasExact, out var exact, out var hasLower, out var lower, out var hasUpper, out var upper, out var hasUnsupported);
+
+ if (hasUnsupported)
+ {
+ return GetConstraintsRequiredBufferSize(set, false);
+ }
+
+ if (effectiveCount is 0)
+ {
+ return format is RangeStringFormat.Maven ? 3 : 1;
+ }
+
+ if (hasExact && effectiveCount is 1)
+ {
+ return exact.Version.RequiredBufferSize + (format is RangeStringFormat.Maven ? 2 : 0);
+ }
+
+ if (format is RangeStringFormat.NpmShort && hasLower && hasUpper && effectiveCount is 2 && TryGetShortNpmOperator(lower, upper, out _))
+ {
+ return lower.Version.RequiredBufferSize + 1;
+ }
+
+ if (format is RangeStringFormat.Maven)
+ {
+ return (hasLower ? lower.Version.RequiredBufferSize : 0) +
+ (hasUpper ? upper.Version.RequiredBufferSize : 0) +
+ 3;
+ }
+
+ return GetConstraintsRequiredBufferSize(set, true);
+ }
+
+ private static int GetConstraintsRequiredBufferSize(VersionConstraint[] set, bool bareEquals)
+ {
+ var length = 0;
+ var written = 0;
+ foreach (var constraint in set)
+ {
+ if (constraint.Comparator is Comparator.Any)
+ {
+ continue;
+ }
+
+ if (written > 0)
+ {
+ length++;
+ }
+
+ length += GetConstraintRequiredBufferSize(constraint, bareEquals);
+ written++;
+ }
+
+ return written is 0 ? 1 : length;
+ }
+
+ private static int GetConstraintRequiredBufferSize(VersionConstraint constraint, bool bareEquals)
+ {
+ return constraint.Comparator switch
+ {
+ Comparator.Equal when bareEquals => constraint.Version.RequiredBufferSize,
+ Comparator.Equal => constraint.Version.RequiredBufferSize + 1,
+ Comparator.GreaterThan => constraint.Version.RequiredBufferSize + 1,
+ Comparator.GreaterThanOrEqual => constraint.Version.RequiredBufferSize + 2,
+ Comparator.LessThan => constraint.Version.RequiredBufferSize + 1,
+ Comparator.LessThanOrEqual => constraint.Version.RequiredBufferSize + 2,
+ Comparator.NotEqual => constraint.Version.RequiredBufferSize + 2,
+ _ => 0
+ };
+ }
+
+ private static bool TryWriteSet(ref Span destination, ref int charsWritten, VersionConstraint[] set, RangeStringFormat format)
+ {
+ AnalyzeSet(set, out var effectiveCount, out var hasExact, out var exact, out var hasLower, out var lower, out var hasUpper, out var upper, out var hasUnsupported);
+
+ if (hasUnsupported)
+ {
+ return TryWriteConstraints(ref destination, ref charsWritten, set, false);
+ }
+
+ if (effectiveCount is 0)
+ {
+ return TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "(,)" : "*");
+ }
+
+ if (hasExact && effectiveCount is 1)
+ {
+ if (format is RangeStringFormat.Maven && !TryWriteLiteral(ref destination, ref charsWritten, "["))
+ {
+ return false;
+ }
+
+ if (!TryWriteVersion(ref destination, ref charsWritten, exact.Version))
+ {
+ return false;
+ }
+
+ return format is not RangeStringFormat.Maven || TryWriteLiteral(ref destination, ref charsWritten, "]");
+ }
+
+ if (format is RangeStringFormat.NpmShort && hasLower && hasUpper && effectiveCount is 2 && TryGetShortNpmOperator(lower, upper, out var shortOperator))
+ {
+ return TryWriteChar(ref destination, ref charsWritten, shortOperator) &&
+ TryWriteVersion(ref destination, ref charsWritten, lower.Version);
+ }
+
+ if (format is RangeStringFormat.Maven)
+ {
+ return TryWriteMavenSet(ref destination, ref charsWritten, hasLower, lower, hasUpper, upper);
+ }
+
+ return TryWriteConstraints(ref destination, ref charsWritten, set, true);
+ }
+
+ private static bool TryWriteMavenSet(ref Span destination, ref int charsWritten, bool hasLower, VersionConstraint lower, bool hasUpper, VersionConstraint upper)
+ {
+ if (!TryWriteChar(ref destination, ref charsWritten, hasLower && lower.Comparator is Comparator.GreaterThanOrEqual ? '[' : '('))
+ {
+ return false;
+ }
+
+ if (hasLower && !TryWriteVersion(ref destination, ref charsWritten, lower.Version))
+ {
+ return false;
+ }
+
+ if (!TryWriteChar(ref destination, ref charsWritten, ','))
+ {
+ return false;
+ }
+
+ if (hasUpper && !TryWriteVersion(ref destination, ref charsWritten, upper.Version))
+ {
+ return false;
+ }
+
+ return TryWriteChar(ref destination, ref charsWritten, hasUpper && upper.Comparator is Comparator.LessThanOrEqual ? ']' : ')');
+ }
+
+ private static bool TryWriteConstraints(ref Span destination, ref int charsWritten, VersionConstraint[] set, bool bareEquals)
+ {
+ var written = 0;
+ foreach (var constraint in set)
+ {
+ if (constraint.Comparator is Comparator.Any)
+ {
+ continue;
+ }
+
+ if (written > 0 && !TryWriteChar(ref destination, ref charsWritten, ' '))
+ {
+ return false;
+ }
+
+ if (!TryWriteConstraint(ref destination, ref charsWritten, constraint, bareEquals))
+ {
+ return false;
+ }
+
+ written++;
+ }
+
+ return written > 0 || TryWriteLiteral(ref destination, ref charsWritten, "*");
+ }
+
+ private static bool TryWriteConstraint(ref Span destination, ref int charsWritten, VersionConstraint constraint, bool bareEquals)
+ {
+ switch (constraint.Comparator)
+ {
+ case Comparator.Equal when bareEquals:
+ return TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.Equal:
+ return TryWriteLiteral(ref destination, ref charsWritten, "=") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.GreaterThan:
+ return TryWriteLiteral(ref destination, ref charsWritten, ">") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.GreaterThanOrEqual:
+ return TryWriteLiteral(ref destination, ref charsWritten, ">=") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.LessThan:
+ return TryWriteLiteral(ref destination, ref charsWritten, "<") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.LessThanOrEqual:
+ return TryWriteLiteral(ref destination, ref charsWritten, "<=") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.NotEqual:
+ return TryWriteLiteral(ref destination, ref charsWritten, "!=") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ default:
+ return false;
+ }
+ }
+
+ private static bool TryGetShortNpmOperator(VersionConstraint lower, VersionConstraint upper, out char shortOperator)
+ {
+ if (lower.Comparator is Comparator.GreaterThanOrEqual && upper.Comparator is Comparator.LessThan)
+ {
+ if (upper.Version == GetCaretUpperBound(lower.Version))
+ {
+ shortOperator = '^';
+ return true;
+ }
+
+ if (upper.Version == GetTildeUpperBound(lower.Version))
+ {
+ shortOperator = '~';
+ return true;
+ }
+ }
+
+ shortOperator = default;
+ return false;
+ }
+
+ private static SemanticVersion GetCaretUpperBound(SemanticVersion version)
+ {
+ return version.Major > 0
+ ? new SemanticVersion(version.Major + 1, 0, 0)
+ : version.Minor > 0
+ ? new SemanticVersion(0, version.Minor + 1, 0)
+ : new SemanticVersion(0, 0, version.Patch + 1);
+ }
+
+ private static SemanticVersion GetTildeUpperBound(SemanticVersion version)
+ {
+ return new SemanticVersion(version.Major, version.Minor + 1, 0);
+ }
+
+ private static void AnalyzeSet(
+ VersionConstraint[] set,
+ out int effectiveCount,
+ out bool hasExact,
+ out VersionConstraint exact,
+ out bool hasLower,
+ out VersionConstraint lower,
+ out bool hasUpper,
+ out VersionConstraint upper,
+ out bool hasUnsupported)
+ {
+ effectiveCount = 0;
+ hasExact = false;
+ exact = default;
+ hasLower = false;
+ lower = default;
+ hasUpper = false;
+ upper = default;
+ hasUnsupported = false;
+
+ foreach (var constraint in set)
+ {
+ if (constraint.Comparator is Comparator.Any)
+ {
+ continue;
+ }
+
+ effectiveCount++;
+ switch (constraint.Comparator)
+ {
+ case Comparator.Equal:
+ hasExact = true;
+ exact = constraint;
+ break;
+ case Comparator.GreaterThan:
+ case Comparator.GreaterThanOrEqual:
+ hasLower = true;
+ lower = constraint;
+ break;
+ case Comparator.LessThan:
+ case Comparator.LessThanOrEqual:
+ hasUpper = true;
+ upper = constraint;
+ break;
+ default:
+ hasUnsupported = true;
+ break;
+ }
+ }
+ }
+
+ private static bool TryWriteVersion(ref Span destination, ref int charsWritten, SemanticVersion version)
+ {
+ if (!version.TryFormat(destination, out var written, "f", null))
+ {
+ return false;
+ }
+
+ destination = destination[written..];
+ charsWritten += written;
+ return true;
+ }
+
+ private static bool TryWriteChar(ref Span destination, ref int charsWritten, char value)
+ {
+ if (destination.IsEmpty)
+ {
+ return false;
+ }
+
+ destination[0] = value;
+ destination = destination[1..];
+ charsWritten++;
+ return true;
+ }
+
+ private static bool TryWriteLiteral(ref Span destination, ref int charsWritten, ReadOnlySpan value)
+ {
+ if (!value.TryCopyTo(destination))
+ {
+ return false;
+ }
+
+ destination = destination[value.Length..];
+ charsWritten += value.Length;
+ return true;
+ }
+
+ private static bool TryGetFormat(string? format, out RangeStringFormat rangeFormat)
+ {
+ return TryGetFormat(format.AsSpan(), out rangeFormat);
+ }
+
+ private static bool TryGetFormat(ReadOnlySpan format, out RangeStringFormat rangeFormat)
+ {
+ if (format.IsEmpty || format is "ns")
+ {
+ rangeFormat = RangeStringFormat.NpmShort;
+ return true;
+ }
+
+ if (format is "m")
+ {
+ rangeFormat = RangeStringFormat.Maven;
+ return true;
+ }
+
+ if (format is "n")
+ {
+ rangeFormat = RangeStringFormat.Npm;
+ return true;
+ }
+
+ rangeFormat = default;
+ return false;
+ }
+}
+
+internal enum RangeStringFormat
+{
+ Maven,
+ Npm,
+ NpmShort
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.Parsing.cs b/src/semver/SemanticVersionRange.Parsing.cs
new file mode 100644
index 0000000..500b111
--- /dev/null
+++ b/src/semver/SemanticVersionRange.Parsing.cs
@@ -0,0 +1,502 @@
+// Copyright (c) The Geekeey Authors
+// SPDX-License-Identifier: EUPL-1.2
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Geekeey.SemVer;
+
+public readonly partial record struct SemanticVersionRange : ISpanParsable
+{
+ #region IParsable
+
+ ///
+ /// Parses a string into a .
+ ///
+ ///
+ public static SemanticVersionRange Parse(string s)
+ {
+ return Parse(s.AsSpan(), null);
+ }
+
+ ///
+ public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
+ {
+ return Parse(s.AsSpan(), provider);
+ }
+
+ ///
+ /// Tries to parse a string into a .
+ ///
+ ///
+ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersionRange result)
+ {
+ return TryParse(s, null, out result);
+ }
+
+ ///
+ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
+ {
+ return TryParse(s.AsSpan(), provider, out result);
+ }
+
+ #endregion
+
+ #region ISpanParsable
+
+ ///
+ /// Parses a span of characters into a .
+ ///
+ public static SemanticVersionRange Parse(ReadOnlySpan s)
+ {
+ return Parse(s, null);
+ }
+
+ ///
+ public static SemanticVersionRange Parse(ReadOnlySpan s, IFormatProvider? provider)
+ {
+ if (!TryParse(s, provider, out var range))
+ {
+ throw new FormatException($"The input string '{s}' was not in a correct format.");
+ }
+
+ return range;
+ }
+
+ ///
+ /// Tries to parse a span of characters into a .
+ ///
+ public static bool TryParse(ReadOnlySpan s, [MaybeNullWhen(false)] out SemanticVersionRange result)
+ {
+ return TryParse(s, null, out result);
+ }
+
+ ///
+ public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
+ {
+ result = default;
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ return s[0] is '[' or '(' ? TryParseJava(s, out result) : TryParseNode(s, out result);
+ }
+
+ #endregion
+
+ private static bool TryParseJava(ReadOnlySpan s, out SemanticVersionRange result)
+ {
+ result = default;
+
+ var alternatives = new List();
+ var start = 0;
+ var bracketLevel = 0;
+
+ for (var i = 0; i < s.Length; i++)
+ {
+ var c = s[i];
+ switch (c)
+ {
+ case '[' or '(':
+ bracketLevel++;
+ break;
+
+ case ']' or ')':
+ if (--bracketLevel < 0)
+ {
+ return false;
+ }
+ break;
+
+ case ',' when bracketLevel == 0:
+ if (!TryParseSingleMavenRange(s[start..i].Trim(), out var constraints))
+ {
+ return false;
+ }
+
+ alternatives.Add(constraints);
+ start = i + 1;
+ break;
+ }
+ }
+
+ if (bracketLevel is not 0 || !TryParseSingleMavenRange(s[start..].Trim(), out var lastConstraints))
+ {
+ return false;
+ }
+
+ alternatives.Add(lastConstraints);
+ result = new SemanticVersionRange([.. alternatives]);
+ return true;
+ }
+
+ private static bool TryParseSingleMavenRange(ReadOnlySpan s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
+ {
+ constraints = null;
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ if (s[0] is not '[' and not '(')
+ {
+ if (!TryParseVersionPartially(s, out var version, out _))
+ {
+ return false;
+ }
+
+ constraints = [new VersionConstraint(Comparator.GreaterThanOrEqual, version)];
+ return true;
+ }
+
+ var last = s.Length - 1;
+ if (s[last] is not ']' and not ')')
+ {
+ return false;
+ }
+
+ var inclusiveStart = s[0] is '[';
+ var inclusiveEnd = s[last] is ']';
+ var inner = s[1..last];
+ var commaIndex = inner.IndexOf(',');
+
+ if (commaIndex < 0)
+ {
+ if (!inclusiveStart || !inclusiveEnd || !TryParseVersionPartially(inner.Trim(), out var exactVersion, out _))
+ {
+ return false;
+ }
+
+ constraints = [new VersionConstraint(Comparator.Equal, exactVersion)];
+ return true;
+ }
+
+ var lowerText = inner[..commaIndex].Trim();
+ var upperText = inner[(commaIndex + 1)..].Trim();
+ var list = new List(2);
+
+ if (!lowerText.IsEmpty)
+ {
+ if (!TryParseVersionPartially(lowerText, out var lowerVersion, out _))
+ {
+ return false;
+ }
+
+ list.Add(new VersionConstraint(inclusiveStart ? Comparator.GreaterThanOrEqual : Comparator.GreaterThan, lowerVersion));
+ }
+
+ if (!upperText.IsEmpty)
+ {
+ if (!TryParseVersionPartially(upperText, out var upperVersion, out _))
+ {
+ return false;
+ }
+
+ list.Add(new VersionConstraint(inclusiveEnd ? Comparator.LessThanOrEqual : Comparator.LessThan, upperVersion));
+ }
+
+ constraints = [.. list];
+ return true;
+ }
+
+ private static bool TryParseNode(ReadOnlySpan s, out SemanticVersionRange result)
+ {
+ result = default;
+ var alternatives = new List();
+ var start = 0;
+
+ while (start < s.Length)
+ {
+ var separator = s[start..].IndexOf("||".AsSpan());
+ var end = separator < 0 ? s.Length : start + separator;
+ var part = s[start..end].Trim();
+
+ if (!part.IsEmpty)
+ {
+ if (!TryParseNpmSet(part, out var constraints))
+ {
+ return false;
+ }
+
+ alternatives.Add(constraints);
+ }
+
+ if (separator < 0)
+ {
+ break;
+ }
+
+ start = end + 2;
+ }
+
+ result = new SemanticVersionRange([.. alternatives]);
+ return true;
+ }
+
+ private static bool TryParseNpmSet(ReadOnlySpan s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
+ {
+ constraints = null;
+ var hyphenIndex = s.IndexOf(" - ".AsSpan());
+ if (hyphenIndex > 0)
+ {
+ var lowerText = s[..hyphenIndex].Trim();
+ var upperText = s[(hyphenIndex + 3)..].Trim();
+ if (!SemanticVersion.TryParse(lowerText, out var lowerVersion) || !SemanticVersion.TryParse(upperText, out var upperVersion))
+ {
+ return false;
+ }
+
+ constraints =
+ [
+ new VersionConstraint(Comparator.GreaterThanOrEqual, lowerVersion),
+ new VersionConstraint(Comparator.LessThanOrEqual, upperVersion)
+ ];
+ return true;
+ }
+
+ var list = new List(2);
+ var current = s;
+ while (TryReadToken(ref current, out var token))
+ {
+ if (IsOperatorOnly(token))
+ {
+ if (!TryReadToken(ref current, out var valueToken) || !TryParseNpmConstraint(token, valueToken, list))
+ {
+ return false;
+ }
+
+ continue;
+ }
+
+ if (!TryParseNpmConstraint(default, token, list))
+ {
+ return false;
+ }
+ }
+
+ constraints = [.. 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 IsOperatorOnly(ReadOnlySpan s)
+ {
+ return s is ">=" or "<=" or ">" or "<" or "~" or "^" or "=";
+ }
+
+ private static bool TryParseNpmConstraint(ReadOnlySpan op, ReadOnlySpan valueText, List constraints)
+ {
+ if (op.IsEmpty)
+ {
+ if (valueText.StartsWith(">="))
+ {
+ op = ">=";
+ valueText = valueText[2..];
+ }
+ else if (valueText.StartsWith("<="))
+ {
+ op = "<=";
+ valueText = valueText[2..];
+ }
+ else if (valueText.StartsWith(">"))
+ {
+ op = ">";
+ valueText = valueText[1..];
+ }
+ else if (valueText.StartsWith("<"))
+ {
+ op = "<";
+ valueText = valueText[1..];
+ }
+ else if (valueText.StartsWith("^"))
+ {
+ op = "^";
+ valueText = valueText[1..];
+ }
+ else if (valueText.StartsWith("~"))
+ {
+ op = "~";
+ valueText = valueText[1..];
+ }
+ else if (valueText.StartsWith("="))
+ {
+ op = "=";
+ valueText = valueText[1..];
+ }
+ }
+
+ valueText = valueText.Trim();
+ if (IsWildcard(valueText))
+ {
+ constraints.Add(new VersionConstraint(Comparator.Any, default));
+ return true;
+ }
+
+ if (!TryParseVersionPartially(valueText, out var version, out var components))
+ {
+ return false;
+ }
+
+ switch (op)
+ {
+ case "^":
+ constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
+ constraints.Add(new VersionConstraint(Comparator.LessThan, GetCaretUpperBound(version)));
+ return true;
+ case "~":
+ constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
+ constraints.Add(new VersionConstraint(Comparator.LessThan, components >= 2 ? GetTildeUpperBound(version) : new SemanticVersion(version.Major + 1, 0, 0)));
+ return true;
+ case ">":
+ constraints.Add(new VersionConstraint(Comparator.GreaterThan, version));
+ return true;
+ case ">=":
+ constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
+ return true;
+ case "<":
+ constraints.Add(new VersionConstraint(Comparator.LessThan, version));
+ return true;
+ case "<=":
+ constraints.Add(new VersionConstraint(Comparator.LessThanOrEqual, version));
+ return true;
+ }
+
+ switch (components)
+ {
+ case 3:
+ constraints.Add(new VersionConstraint(Comparator.Equal, version));
+ return true;
+ case 2:
+ constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
+ constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(version.Major, version.Minor + 1, 0)));
+ return true;
+ case 1:
+ constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
+ constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(version.Major + 1, 0, 0)));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static bool TryParseVersionPartially(ReadOnlySpan s, out SemanticVersion version, out int components)
+ {
+ version = default;
+ components = 0;
+ s = s.Trim();
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ var core = s;
+ ReadOnlySpan prerelease = default;
+ ReadOnlySpan metadata = default;
+ var prereleaseIndex = s.IndexOf('-');
+ var metadataIndex = s.IndexOf('+');
+
+ if (metadataIndex >= 0)
+ {
+ core = s[..metadataIndex];
+ metadata = s[(metadataIndex + 1)..];
+ }
+
+ if (prereleaseIndex >= 0)
+ {
+ if (metadataIndex >= 0 && prereleaseIndex > metadataIndex)
+ {
+ return false;
+ }
+
+ core = s[..prereleaseIndex];
+ var prereleaseEnd = metadataIndex >= 0 ? metadataIndex : s.Length;
+ prerelease = s[(prereleaseIndex + 1)..prereleaseEnd];
+ }
+
+ ulong major = 0;
+ ulong minor = 0;
+ ulong patch = 0;
+ var segmentStart = 0;
+
+ while (segmentStart <= core.Length)
+ {
+ var nextSeparator = core[segmentStart..].IndexOf('.');
+ var segmentEnd = nextSeparator < 0 ? core.Length : segmentStart + nextSeparator;
+ var segment = core[segmentStart..segmentEnd];
+ if (segment.IsEmpty)
+ {
+ return false;
+ }
+
+ if (IsWildcard(segment))
+ {
+ version = components switch
+ {
+ 1 => new SemanticVersion(major, 0, 0),
+ 2 => new SemanticVersion(major, minor, 0),
+ _ => default
+ };
+ return true;
+ }
+
+ if (!ulong.TryParse(segment, out var value))
+ {
+ return false;
+ }
+
+ switch (components)
+ {
+ case 0:
+ major = value;
+ break;
+ case 1:
+ minor = value;
+ break;
+ case 2:
+ patch = value;
+ break;
+ default:
+ return false;
+ }
+
+ components++;
+ if (nextSeparator < 0)
+ {
+ break;
+ }
+
+ segmentStart = segmentEnd + 1;
+ }
+
+ version = new SemanticVersion(major, minor, patch, prerelease.IsEmpty ? null : prerelease.ToString(), metadata.IsEmpty ? null : metadata.ToString());
+ return true;
+ }
+
+ private static bool IsWildcard(ReadOnlySpan s)
+ {
+ return s is "*" or "x" or "X";
+ }
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.cs b/src/semver/SemanticVersionRange.cs
index 2ed0999..e582810 100644
--- a/src/semver/SemanticVersionRange.cs
+++ b/src/semver/SemanticVersionRange.cs
@@ -1,10 +1,7 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
-using System.Diagnostics.CodeAnalysis;
-using System.Text;
using System.Text.Json.Serialization;
-using System.Text.Unicode;
namespace Geekeey.SemVer;
@@ -12,15 +9,13 @@ namespace Geekeey.SemVer;
/// Represents a semantic version range.
///
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
-public readonly partial record struct SemanticVersionRange : ISpanParsable, IUtf8SpanParsable, ISpanFormattable, IUtf8SpanFormattable
+public readonly partial record struct SemanticVersionRange
{
private readonly VersionConstraint[][]? _sets;
- private readonly string? _originalString;
- private SemanticVersionRange(VersionConstraint[][] sets, string originalString)
+ private SemanticVersionRange(VersionConstraint[][] sets)
{
_sets = sets;
- _originalString = originalString;
}
///
@@ -30,14 +25,25 @@ public readonly partial record struct SemanticVersionRange : ISpanParsabletrue if the version satisfies the range; otherwise, false.
public bool Satisfies(SemanticVersion version)
{
- if (_sets == null || _sets.Length == 0)
+ if (_sets?.Length is null or 0)
{
return true;
}
foreach (var set in _sets)
{
- if (IsSatisfiedBySet(set, version))
+ if (version.Prerelease is not null)
+ {
+ if (!set.Any(constraint => constraint.Version.Prerelease is not null &&
+ constraint.Version.Major == version.Major &&
+ constraint.Version.Minor == version.Minor &&
+ constraint.Version.Patch == version.Patch))
+ {
+ continue;
+ }
+ }
+
+ if (set.All(constraint => constraint.Satisfies(version)))
{
return true;
}
@@ -46,571 +52,49 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable
public override string ToString()
{
- return _originalString ?? "*";
+ return ToString(null, null);
}
- internal int RequiredBufferSize => _originalString?.Length ?? 1;
+ internal int RequiredBufferSize => GetRequiredBufferSize(RangeStringFormat.NpmShort);
- #region IFormattable
-
- ///
- public string ToString(string? format, IFormatProvider? formatProvider)
+ internal readonly record struct VersionConstraint
{
- return ToString();
+ public VersionConstraint(Comparator comparator, SemanticVersion version)
+ {
+ Comparator = comparator;
+ Version = version;
+ }
+
+ public Comparator Comparator { get; }
+ public SemanticVersion Version { get; }
+
+ public bool Satisfies(SemanticVersion version)
+ {
+ return Comparator switch
+ {
+ Comparator.Any => true,
+ Comparator.Equal => version.CompareTo(Version) is 0,
+ Comparator.NotEqual => version.CompareTo(Version) is not 0,
+ Comparator.GreaterThan => version.CompareTo(Version) > 0,
+ Comparator.GreaterThanOrEqual => version.CompareTo(Version) >= 0,
+ Comparator.LessThan => version.CompareTo(Version) < 0,
+ Comparator.LessThanOrEqual => version.CompareTo(Version) <= 0,
+ _ => false,
+ };
+ }
}
- #endregion
-
- #region ISpanFormattable
-
- ///
- public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
+ internal enum Comparator
{
- var s = ToString();
- if (s.Length > destination.Length)
- {
- charsWritten = 0;
- return false;
- }
-
- s.AsSpan().CopyTo(destination);
- charsWritten = s.Length;
- return true;
- }
-
- #endregion
-
- #region IUtf8SpanFormattable
-
- ///
- public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider)
- {
- return Utf8.TryWrite(utf8Destination, $"{ToString()}", out bytesWritten);
- }
-
- #endregion
-
- #region IParsable
-
- ///
- public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
- {
- return Parse(s.AsSpan(), provider);
- }
-
- ///
- public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
- {
- return TryParse(s.AsSpan(), provider, out result);
- }
-
- #endregion
-
- #region ISpanParsable
-
- ///
- /// Parses a span of characters into a .
- ///
- public static SemanticVersionRange Parse(ReadOnlySpan s)
- {
- return Parse(s, null);
- }
-
- ///
- public static SemanticVersionRange Parse(ReadOnlySpan s, IFormatProvider? provider)
- {
- if (!TryParse(s, provider, out var range))
- {
- throw new FormatException($"The input string '{s}' was not in a correct format.");
- }
-
- return range;
- }
-
- ///
- /// Tries to parse a span of characters into a .
- ///
- public static bool TryParse(ReadOnlySpan s, [MaybeNullWhen(false)] out SemanticVersionRange result)
- {
- return TryParse(s, null, out result);
- }
-
- ///
- public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
- {
- s = s.Trim();
- if (s.IsEmpty)
- {
- result = new SemanticVersionRange([], "*");
- return true;
- }
-
- return s[0] is '[' or '(' ? TryParseMaven(s, out result) : TryParseNpm(s, out result);
- }
-
- #endregion
-
- #region IUtf8SpanParsable
-
- ///
- /// Parses a span of UTF-8 bytes into a .
- ///
- public static SemanticVersionRange Parse(ReadOnlySpan utf8Text)
- {
- return Parse(utf8Text, null);
- }
-
- ///
- public static SemanticVersionRange Parse(ReadOnlySpan utf8Text, IFormatProvider? provider)
- {
- if (!TryParse(utf8Text, provider, out var range))
- {
- throw new FormatException($"The input string '{Encoding.UTF8.GetString(utf8Text)}' was not in a correct format.");
- }
-
- return range;
- }
-
- ///
- public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
- {
- if (utf8Text.IsEmpty)
- {
- result = new SemanticVersionRange([], "*");
- return true;
- }
-
- var charCount = Encoding.UTF8.GetCharCount(utf8Text);
- var chars = charCount <= 256 ? stackalloc char[charCount] : new char[charCount];
- Encoding.UTF8.GetChars(utf8Text, chars);
-
- return TryParse(chars, provider, out result);
- }
-
- #endregion
-
- private static bool TryParseMaven(ReadOnlySpan s, out SemanticVersionRange result)
- {
- result = default;
- var original = s.ToString();
- var alternatives = new List();
-
- var start = 0;
- var bracketLevel = 0;
- for (var i = 0; i < s.Length; i++)
- {
- if (s[i] is '[' or '(')
- {
- bracketLevel++;
- }
- else if (s[i] is ']' or ')')
- {
- bracketLevel--;
- }
- else if (s[i] is ',' && bracketLevel == 0)
- {
- if (!TryParseSingleMavenRange(s[start..i].Trim(), out var constraints))
- {
- return false;
- }
-
- alternatives.Add(constraints);
- start = i + 1;
- }
- }
-
- if (!TryParseSingleMavenRange(s[start..].Trim(), out var lastConstraints))
- {
- return false;
- }
-
- alternatives.Add(lastConstraints);
-
- result = new SemanticVersionRange([.. alternatives], original);
- return true;
- }
-
- private static bool TryParseSingleMavenRange(ReadOnlySpan s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
- {
- constraints = null;
- if (s.IsEmpty)
- {
- return false;
- }
-
- if (s[0] is not '[' and not '(')
- {
- if (!TryParseVersionPartially(s, out var v, out _))
- {
- return false;
- }
-
- constraints = [new VersionConstraint(Comparator.GreaterThanOrEqual, v)];
- return true;
- }
-
- var last = s.Length - 1;
- if (s[last] is not ']' and not ')')
- {
- return false;
- }
-
- var inclusiveStart = s[0] == '[';
- var inclusiveEnd = s[last] == ']';
- var inner = s[1..last];
- var commaIndex = inner.IndexOf(',');
-
- if (commaIndex < 0)
- {
- if (!inclusiveStart || !inclusiveEnd)
- {
- return false;
- }
-
- if (!TryParseVersionPartially(inner, out var v, out _))
- {
- return false;
- }
-
- constraints = [new VersionConstraint(Comparator.Equal, v)];
- return true;
- }
-
- var startStr = inner[..commaIndex].Trim();
- var endStr = inner[(commaIndex + 1)..].Trim();
- var list = new List();
-
- if (!startStr.IsEmpty)
- {
- if (!TryParseVersionPartially(startStr, out var vStart, out _))
- {
- return false;
- }
-
- list.Add(new VersionConstraint(inclusiveStart ? Comparator.GreaterThanOrEqual : Comparator.GreaterThan, vStart));
- }
-
- if (!endStr.IsEmpty)
- {
- if (!TryParseVersionPartially(endStr, out var vEnd, out _))
- {
- return false;
- }
-
- list.Add(new VersionConstraint(inclusiveEnd ? Comparator.LessThanOrEqual : Comparator.LessThan, vEnd));
- }
-
- constraints = [.. list];
- return true;
- }
-
- private static bool TryParseNpm(ReadOnlySpan s, out SemanticVersionRange result)
- {
- result = default;
- var original = s.ToString();
- var alternatives = new List();
-
- var start = 0;
- while (start < s.Length)
- {
- var orIndex = s[start..].IndexOf("||".AsSpan());
- var part = orIndex < 0 ? s[start..].Trim() : s[start..(start + orIndex)].Trim();
- start = orIndex < 0 ? s.Length : start + orIndex + 2;
-
- if (part.IsEmpty)
- {
- continue;
- }
-
- if (!TryParseNpmSet(part, out var constraints))
- {
- return false;
- }
-
- alternatives.Add(constraints);
- }
-
- result = new SemanticVersionRange([.. alternatives], original);
- return true;
- }
-
- private static bool TryParseNpmSet(ReadOnlySpan s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
- {
- constraints = null;
- var list = new List();
-
- var hyphenIndex = s.IndexOf(" - ".AsSpan());
- if (hyphenIndex > 0)
- {
- var v1Str = s[..hyphenIndex].Trim();
- var v2Str = s[(hyphenIndex + 3)..].Trim();
- if (!SemanticVersion.TryParse(v1Str, out var v1) || !SemanticVersion.TryParse(v2Str, out var v2))
- {
- return false;
- }
-
- constraints =
- [
- new VersionConstraint(Comparator.GreaterThanOrEqual, v1),
- new VersionConstraint(Comparator.LessThanOrEqual, v2)
- ];
- return true;
- }
-
- var current = s;
- while (!current.IsEmpty)
- {
- current = current.TrimStart();
- if (current.IsEmpty)
- {
- break;
- }
-
- var spaceIndex = current.IndexOf(' ');
- var part = spaceIndex < 0 ? current : current[..spaceIndex];
- current = spaceIndex < 0 ? default : current[spaceIndex..];
-
- if (IsOperatorOnly(part))
- {
- current = current.TrimStart();
- var nextSpace = current.IndexOf(' ');
- var nextPart = nextSpace < 0 ? current : current[..nextSpace];
- current = nextSpace < 0 ? default : current[nextSpace..];
-
- if (!TryParseNpmConstraint(part, nextPart, list))
- {
- return false;
- }
- }
- else
- {
- if (!TryParseNpmConstraint(default, part, list))
- {
- return false;
- }
- }
- }
-
- constraints = [.. list];
- return true;
- }
-
- private static bool IsOperatorOnly(ReadOnlySpan s)
- {
- return s is ">=" or "<=" or ">" or "<" or "~" or "^" or "=";
- }
-
- private static bool TryParseNpmConstraint(ReadOnlySpan op, ReadOnlySpan vStr, List constraints)
- {
- if (op.IsEmpty)
- {
- if (vStr.StartsWith(">="))
- {
- op = ">=";
- vStr = vStr[2..];
- }
- else if (vStr.StartsWith("<="))
- {
- op = "<=";
- vStr = vStr[2..];
- }
- else if (vStr.StartsWith(">"))
- {
- op = ">";
- vStr = vStr[1..];
- }
- else if (vStr.StartsWith("<"))
- {
- op = "<";
- vStr = vStr[1..];
- }
- else if (vStr.StartsWith("^"))
- {
- op = "^";
- vStr = vStr[1..];
- }
- else if (vStr.StartsWith("~"))
- {
- op = "~";
- vStr = vStr[1..];
- }
- else if (vStr.StartsWith("="))
- {
- op = "=";
- vStr = vStr[1..];
- }
- }
-
- vStr = vStr.Trim();
- if (vStr is "*" or "x" or "X")
- {
- constraints.Add(new VersionConstraint(Comparator.Any, default));
- return true;
- }
-
- if (!TryParseVersionPartially(vStr, out var v, out var components))
- {
- return false;
- }
-
- if (op is "^")
- {
- constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
- var endV = v.Major > 0 ? new SemanticVersion(v.Major + 1, 0, 0) : (v.Minor > 0 ? new SemanticVersion(0, v.Minor + 1, 0) : new SemanticVersion(0, 0, v.Patch + 1));
- constraints.Add(new VersionConstraint(Comparator.LessThan, endV));
- }
- else if (op is "~")
- {
- constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
- var endV = components >= 2 ? new SemanticVersion(v.Major, v.Minor + 1, 0) : new SemanticVersion(v.Major + 1, 0, 0);
- constraints.Add(new VersionConstraint(Comparator.LessThan, endV));
- }
- else if (op is ">") constraints.Add(new VersionConstraint(Comparator.GreaterThan, v));
- else if (op is ">=") constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
- else if (op is "<") constraints.Add(new VersionConstraint(Comparator.LessThan, v));
- else if (op is "<=") constraints.Add(new VersionConstraint(Comparator.LessThanOrEqual, v));
- else
- {
- if (components == 3)
- {
- constraints.Add(new VersionConstraint(Comparator.Equal, v));
- }
- else if (components == 2)
- {
- constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
- constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(v.Major, v.Minor + 1, 0)));
- }
- else if (components == 1)
- {
- constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
- constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(v.Major + 1, 0, 0)));
- }
- }
-
- return true;
- }
-
- private static bool TryParseVersionPartially(ReadOnlySpan s, out SemanticVersion version, out int components)
- {
- version = default;
- components = 0;
-
- var dashIndex = s.IndexOf('-');
- var metadataIndex = s.IndexOf('+');
- var suffixIndex = dashIndex >= 0 ? (metadataIndex >= 0 ? Math.Min(dashIndex, metadataIndex) : dashIndex) : metadataIndex;
-
- var versionPart = suffixIndex >= 0 ? s[..suffixIndex] : s;
- var prerelease = dashIndex >= 0 ? (metadataIndex > dashIndex ? s[(dashIndex + 1)..metadataIndex].ToString() : s[(dashIndex + 1)..].ToString()) : null;
- var metadata = metadataIndex >= 0 ? s[(metadataIndex + 1)..].ToString() : null;
-
- ulong major = 0, minor = 0, patch = 0;
- var current = versionPart;
- var componentCount = 0;
-
- while (!current.IsEmpty && componentCount < 3)
- {
- var dotIndex = current.IndexOf('.');
- var part = dotIndex < 0 ? current : current[..dotIndex];
-
- if (part is "*" or "x" or "X")
- {
- components = componentCount;
- version = componentCount switch
- {
- 1 => new SemanticVersion(major, 0, 0),
- 2 => new SemanticVersion(major, minor, 0),
- _ => default
- };
- return true;
- }
-
- if (!ulong.TryParse(part, out var value))
- {
- return false;
- }
-
- if (componentCount == 0) major = value;
- else if (componentCount == 1) minor = value;
- else if (componentCount == 2) patch = value;
-
- componentCount++;
- if (dotIndex < 0) break;
- current = current[(dotIndex + 1)..];
- }
-
- components = componentCount;
- version = new SemanticVersion(major, minor, patch, prerelease, metadata);
- return true;
- }
-}
-
-internal enum Comparator
-{
- Any,
- Equal,
- GreaterThan,
- GreaterThanOrEqual,
- LessThan,
- LessThanOrEqual,
- NotEqual
-}
-
-internal readonly record struct VersionConstraint(Comparator Comparator, SemanticVersion Version)
-{
- public bool Satisfies(SemanticVersion version)
- {
- if (Comparator is Comparator.Any)
- {
- return true;
- }
-
- var cmp = version.CompareTo(Version);
- return Comparator switch
- {
- Comparator.Equal => cmp == 0,
- Comparator.NotEqual => cmp != 0,
- Comparator.GreaterThan => cmp > 0,
- Comparator.GreaterThanOrEqual => cmp >= 0,
- Comparator.LessThan => cmp < 0,
- Comparator.LessThanOrEqual => cmp <= 0,
- _ => false
- };
+ Any,
+ Equal,
+ GreaterThan,
+ GreaterThanOrEqual,
+ LessThan,
+ LessThanOrEqual,
+ NotEqual
}
}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRangeJsonConverter.cs b/src/semver/SemanticVersionRangeJsonConverter.cs
index b810f26..b56fc3d 100644
--- a/src/semver/SemanticVersionRangeJsonConverter.cs
+++ b/src/semver/SemanticVersionRangeJsonConverter.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
+using System.Buffers;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -8,6 +9,8 @@ namespace Geekeey.SemVer;
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter
{
+ private const int StackBufferThreshold = 256;
+
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.Null)
@@ -22,16 +25,35 @@ internal sealed class SemanticVersionRangeJsonConverter : JsonConverter StackBufferThreshold ? ArrayPool.Shared.Rent(capacity) : null;
+ try
+ {
+ scoped var buffer = shared.AsSpan();
+ if (shared is null)
+ {
+ buffer = stackalloc char[capacity];
+ }
+
+ _ = value.TryFormat(buffer, out var bytesWritten, default, null);
+ writer.WriteStringValue(buffer[..bytesWritten]);
+ }
+ finally
+ {
+ if (shared is not null)
+ {
+ ArrayPool.Shared.Return(shared);
+ }
+ }
}
}
\ No newline at end of file