diff --git a/src/semver/SemanticVersion.Formatting.cs b/src/semver/SemanticVersion.Formatting.cs
index c6a0992..f6b23d9 100644
--- a/src/semver/SemanticVersion.Formatting.cs
+++ b/src/semver/SemanticVersion.Formatting.cs
@@ -1,56 +1,29 @@
-using System.Buffers;
-using System.Globalization;
+using System.Runtime.CompilerServices;
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]
- ///
+ ///
+ ///
+ /// - 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
+ if (format is not null and not "s" and not "f" and not "r")
{
- 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);
- }
+ throw new FormatException($"The format string '{format}' is not supported.");
}
+
+ var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
+ handler.AppendFormatted(this, format);
+ return handler.ToStringAndClear();
}
#endregion
@@ -67,47 +40,46 @@ public readonly partial record struct SemanticVersion : ISpanFormattable
}
///
+ ///
+ ///
+ /// - 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 bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
{
- if (!destination.TryWrite(NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out charsWritten))
+ if (format.IsEmpty)
+ {
+ format = "s";
+ }
+
+ if (format is not "s" and not "f" and not "r")
{
charsWritten = 0;
return false;
}
- destination = destination[charsWritten..];
+ var builder = new SpanStringBuilder(destination, provider);
+ builder.AppendFormatted(Major);
+ builder.AppendLiteral(".");
+ builder.AppendFormatted(Minor);
+ builder.AppendLiteral(".");
+ builder.AppendFormatted(Patch);
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;
+ builder.AppendLiteral("-");
+ builder.AppendFormatted(Prerelease);
}
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;
+ builder.AppendLiteral("+");
+ builder.AppendFormatted(Metadata);
}
- _ = destination;
-
- return true;
+ return builder.TryComplete(out charsWritten);
}
#endregion
diff --git a/src/semver/SemanticVersion.JsonConverter.cs b/src/semver/SemanticVersion.JsonConverter.cs
new file mode 100644
index 0000000..d08c40a
--- /dev/null
+++ b/src/semver/SemanticVersion.JsonConverter.cs
@@ -0,0 +1,41 @@
+// Copyright (c) The Geekeey Authors
+// SPDX-License-Identifier: EUPL-1.2
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Geekeey.SemVer;
+
+[JsonConverter(typeof(SemanticVersionJsonConverter))]
+public readonly partial record struct SemanticVersion
+{
+ internal sealed class SemanticVersionJsonConverter : JsonConverter
+ {
+ public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType is JsonTokenType.Null)
+ {
+ return default;
+ }
+
+ if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
+ {
+ throw new JsonException("Expected string");
+ }
+
+ try
+ {
+ return Parse(value);
+ }
+ catch (FormatException exception)
+ {
+ throw new JsonException(exception.Message, exception);
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString("f", null));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersion.Parsing.cs b/src/semver/SemanticVersion.Parsing.cs
index fc3a0cf..f53dbcb 100644
--- a/src/semver/SemanticVersion.Parsing.cs
+++ b/src/semver/SemanticVersion.Parsing.cs
@@ -53,12 +53,12 @@ public readonly partial record struct SemanticVersion : ISpanParsable
public static SemanticVersion Parse(ReadOnlySpan s, IFormatProvider? provider)
{
- if (!TryParse(s, provider, out var version))
+ if (!TryParse(s, provider, out var result))
{
throw new FormatException($"The input string '{s}' was not in a correct format.");
}
- return version;
+ return result;
}
///
@@ -73,60 +73,76 @@ public readonly partial record struct SemanticVersion : ISpanParsable
public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
{
- result = default;
+ return TryParsePartially(s, out result, out var components) && components is 3;
+ }
+
+ #endregion
+
+ internal static bool TryParsePartially(ReadOnlySpan s, out SemanticVersion version, out int components)
+ {
+ version = default;
+ components = 0;
+
if (s.IsEmpty)
{
return false;
}
var metadata = default(string);
- var plusIndex = s.IndexOf('+');
- if (plusIndex >= 0)
+ if (s.IndexOf('+') is >= 0 and var metadataIndex)
{
- metadata = s[(plusIndex + 1)..].ToString();
- s = s[..plusIndex];
+ if (s[(metadataIndex + 1)..] is not { IsEmpty: false } value)
+ {
+ return false;
+ }
+
+ metadata = new string(value);
+ s = s[..metadataIndex];
}
var prerelease = default(string);
- var dashIndex = s.IndexOf('-');
- if (dashIndex >= 0)
+ if (s.IndexOf('-') is >= 0 and var prereleaseIndex)
{
- prerelease = s[(dashIndex + 1)..].ToString();
- s = s[..dashIndex];
+ if (s[(prereleaseIndex + 1)..] is not { IsEmpty: false } value)
+ {
+ return false;
+ }
+
+ prerelease = new string(value);
+ s = s[..prereleaseIndex];
}
- var firstDot = s.IndexOf('.');
- if (firstDot < 0)
+ Span destination = stackalloc Range[3];
+ Span component = stackalloc ulong[3];
+
+ foreach (var range in destination[..s.Split(destination, '.')])
{
- return false;
+ if (s[range] is not { IsEmpty: false } segment)
+ {
+ return false;
+ }
+
+ if (segment is "*" or "x" or "X")
+ {
+ version = components switch
+ {
+ 0 => default,
+ 1 => new SemanticVersion(component[0], 0, 0),
+ 2 => new SemanticVersion(component[0], component[1], 0),
+ _ => throw new InvalidOperationException(),
+ };
+ return true;
+ }
+
+ if (!ulong.TryParse(segment, NumberStyles.None, CultureInfo.InvariantCulture, out var value))
+ {
+ return false;
+ }
+
+ component[components++] = value;
}
- 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);
+ version = new SemanticVersion(component[0], component[1], component[2], prerelease, metadata);
return true;
}
-
- #endregion
}
\ No newline at end of file
diff --git a/src/semver/SemanticVersion.cs b/src/semver/SemanticVersion.cs
index a2a9de5..84a7ed5 100644
--- a/src/semver/SemanticVersion.cs
+++ b/src/semver/SemanticVersion.cs
@@ -1,11 +1,8 @@
-using System.Text.Json.Serialization;
-
namespace Geekeey.SemVer;
///
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
///
-[JsonConverter(typeof(SemanticVersionJsonConverter))]
public readonly partial record struct SemanticVersion
{
///
@@ -111,4 +108,4 @@ public readonly partial record struct SemanticVersion
{
return ToString(null, null);
}
-}
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionJsonConverter.cs b/src/semver/SemanticVersionJsonConverter.cs
deleted file mode 100644
index 4921d00..0000000
--- a/src/semver/SemanticVersionJsonConverter.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) The Geekeey Authors
-// SPDX-License-Identifier: EUPL-1.2
-
-using System.Buffers;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Geekeey.SemVer;
-
-internal sealed class SemanticVersionJsonConverter : JsonConverter
-{
- private const int StackBufferThreshold = 256;
-
- public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType is JsonTokenType.Null)
- {
- return default;
- }
-
- if (reader.TokenType is not JsonTokenType.String)
- {
- throw new JsonException("Expected string");
- }
-
- try
- {
- return SemanticVersion.Parse(reader.GetString().AsSpan());
- }
- catch (FormatException e)
- {
- throw new JsonException(e.Message, e);
- }
- }
-
- public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
- {
- var capacity = value.RequiredBufferSize;
- var shared = capacity > 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, "f", null);
-
- writer.WriteStringValue(buffer[..bytesWritten]);
- }
- finally
- {
- if (shared is not null)
- {
- ArrayPool.Shared.Return(shared);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.Formatting.cs b/src/semver/SemanticVersionRange.Formatting.cs
deleted file mode 100644
index f41b546..0000000
--- a/src/semver/SemanticVersionRange.Formatting.cs
+++ /dev/null
@@ -1,465 +0,0 @@
-// 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.JsonConverter.cs b/src/semver/SemanticVersionRange.JsonConverter.cs
new file mode 100644
index 0000000..85a2fb0
--- /dev/null
+++ b/src/semver/SemanticVersionRange.JsonConverter.cs
@@ -0,0 +1,41 @@
+// Copyright (c) The Geekeey Authors
+// SPDX-License-Identifier: EUPL-1.2
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Geekeey.SemVer;
+
+[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
+public readonly partial record struct SemanticVersionRange
+{
+ internal sealed class SemanticVersionRangeJsonConverter : JsonConverter
+ {
+ public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType is JsonTokenType.Null)
+ {
+ return default;
+ }
+
+ if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
+ {
+ throw new JsonException("Expected string");
+ }
+
+ try
+ {
+ return Parse(value);
+ }
+ catch (FormatException exception)
+ {
+ throw new JsonException(exception.Message, exception);
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRange.Parsing.cs b/src/semver/SemanticVersionRange.Parsing.cs
deleted file mode 100644
index 500b111..0000000
--- a/src/semver/SemanticVersionRange.Parsing.cs
+++ /dev/null
@@ -1,502 +0,0 @@
-// 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 e582810..20df5c2 100644
--- a/src/semver/SemanticVersionRange.cs
+++ b/src/semver/SemanticVersionRange.cs
@@ -1,36 +1,29 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
-using System.Text.Json.Serialization;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
namespace Geekeey.SemVer;
-///
-/// Represents a semantic version range.
-///
-[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
public readonly partial record struct SemanticVersionRange
{
- private readonly VersionConstraint[][]? _sets;
+ private readonly ImmutableArray> _constraints;
- private SemanticVersionRange(VersionConstraint[][] sets)
+ private SemanticVersionRange(ImmutableArray> constraints)
{
- _sets = sets;
+ _constraints = constraints;
}
- ///
- /// Determines whether the specified version satisfies the range.
- ///
- /// The version to check.
- /// true if the version satisfies the range; otherwise, false.
public bool Satisfies(SemanticVersion version)
{
- if (_sets?.Length is null or 0)
+ if (_constraints.Length is 0)
{
return true;
}
- foreach (var set in _sets)
+ foreach (var set in _constraints)
{
if (version.Prerelease is not null)
{
@@ -43,7 +36,17 @@ public readonly partial record struct SemanticVersionRange
}
}
- if (set.All(constraint => constraint.Satisfies(version)))
+ if (set.All(constraint => constraint.Comparator switch
+ {
+ Comparator.Any => true,
+ Comparator.Equal => version.CompareTo(constraint.Version) is 0,
+ Comparator.NotEqual => version.CompareTo(constraint.Version) is not 0,
+ Comparator.Greater => version.CompareTo(constraint.Version) > 0,
+ Comparator.GreaterOrEqual => version.CompareTo(constraint.Version) >= 0,
+ Comparator.Less => version.CompareTo(constraint.Version) < 0,
+ Comparator.LessOrEqual => version.CompareTo(constraint.Version) <= 0,
+ _ => false,
+ }))
{
return true;
}
@@ -58,43 +61,818 @@ public readonly partial record struct SemanticVersionRange
return ToString(null, null);
}
- internal int RequiredBufferSize => GetRequiredBufferSize(RangeStringFormat.NpmShort);
-
- internal readonly record struct VersionConstraint
+ private readonly struct Constraint
{
- public VersionConstraint(Comparator comparator, SemanticVersion version)
+ public Constraint(Comparator comparator, SemanticVersion version)
{
Comparator = comparator;
Version = version;
}
public Comparator Comparator { get; }
- public SemanticVersion Version { get; }
- public bool Satisfies(SemanticVersion version)
+ public SemanticVersion Version { get; }
+ }
+
+ private enum Comparator
+ {
+ Any,
+ Equal,
+ Greater,
+ GreaterOrEqual,
+ Less,
+ LessOrEqual,
+ NotEqual,
+ }
+}
+
+public readonly partial record struct SemanticVersionRange : ISpanFormattable
+{
+ #region IFormattable
+
+ ///
+ public string ToString(string? format, IFormatProvider? formatProvider)
+ {
+ var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
+ handler.AppendFormatted(this, format);
+ return handler.ToStringAndClear();
+ }
+
+ #endregion
+
+ #region ISpanFormattable
+
+ ///
+ /// Tries to format the semantic version range 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)
+ {
+ return TryFormatCore(destination, out charsWritten, format);
+ }
+
+ #endregion
+
+ private bool TryFormatCore(Span destination, out int charsWritten, ReadOnlySpan format)
+ {
+ charsWritten = 0;
+
+ if (_constraints is not { Length: > 0 } sets)
{
- return Comparator switch
+ 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 ? "," : " || "))
{
- 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,
+ charsWritten = 0;
+ return false;
+ }
+
+ if (!TryWriteSet(ref destination, ref charsWritten, sets[i], format))
+ {
+ charsWritten = 0;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool TryWriteSet(ref Span destination, ref int charsWritten, ImmutableArray 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, Constraint lower, bool hasUpper, Constraint upper)
+ {
+ if (!TryWriteChar(ref destination, ref charsWritten, hasLower && lower.Comparator is Comparator.GreaterOrEqual ? '[' : '('))
+ {
+ 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.LessOrEqual ? ']' : ')');
+ }
+
+ private static bool TryWriteConstraints(ref Span destination, ref int charsWritten, ImmutableArray 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, Constraint 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.Greater:
+ return TryWriteLiteral(ref destination, ref charsWritten, ">") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.GreaterOrEqual:
+ return TryWriteLiteral(ref destination, ref charsWritten, ">=") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.Less:
+ return TryWriteLiteral(ref destination, ref charsWritten, "<") &&
+ TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
+ case Comparator.LessOrEqual:
+ 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(Constraint lower, Constraint upper, out char shortOperator)
+ {
+ if (lower.Comparator is Comparator.GreaterOrEqual && upper.Comparator is Comparator.Less)
+ {
+ 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(
+ ImmutableArray set,
+ out int effectiveCount,
+ out bool hasExact,
+ out Constraint exact,
+ out bool hasLower,
+ out Constraint lower,
+ out bool hasUpper,
+ out Constraint 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.Greater:
+ case Comparator.GreaterOrEqual:
+ hasLower = true;
+ lower = constraint;
+ break;
+ case Comparator.Less:
+ case Comparator.LessOrEqual:
+ 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;
+ }
+}
+
+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 result))
+ {
+ throw new FormatException($"The input string '{s}' was not in a correct format.");
+ }
+
+ return result;
+ }
+
+ ///
+ /// 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)
+ {
+ var sets = new List>();
+
+ foreach (var range in new JavaSetGrouping(s))
+ {
+ if (s[range] is not { IsEmpty: false } part)
+ {
+ continue;
+ }
+
+ if (!TryParseJavaSet(part, out var constraints))
+ {
+ result = default;
+ return false;
+ }
+
+ sets.Add(constraints);
+ }
+
+ result = new SemanticVersionRange([..sets]);
+ return true;
+ }
+
+ private static bool TryParseJavaSet(ReadOnlySpan s, [NotNullWhen(true)] out ImmutableArray constraints)
+ {
+ constraints = [];
+
+ if (s.IsEmpty)
+ {
+ return false;
+ }
+
+ if (s is not ['[' or '(', .., ']' or ')'])
+ {
+ if (!SemanticVersion.TryParsePartially(s, out var version, out _))
+ {
+ return false;
+ }
+
+ constraints = [new Constraint(Comparator.GreaterOrEqual, version)];
+ 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;
+ }
+
+ constraints = [new Constraint(Comparator.Equal, version)];
+ return true;
+ }
+
+ var result = new List(2);
+
+ var lowerComparator = inclusiveStart ? Comparator.GreaterOrEqual : Comparator.Greater;
+ if (!TryAddConstraint(text[..comma].Trim(), lowerComparator, result))
+ {
+ return false;
+ }
+
+ var upperComparator = inclusiveEnd ? Comparator.LessOrEqual : Comparator.Less;
+ if (!TryAddConstraint(text[(comma + 1)..].Trim(), upperComparator, result))
+ {
+ return false;
+ }
+
+ constraints = [.. result];
+ return true;
+
+ static bool TryAddConstraint(ReadOnlySpan text, Comparator comparator, ICollection result)
+ {
+ if (text.IsEmpty)
+ {
+ return true;
+ }
+
+ if (!SemanticVersion.TryParsePartially(text, out var version, out _))
+ {
+ return false;
+ }
+
+ result.Add(new Constraint(comparator, version));
+ return true;
+ }
+ }
+
+ private ref struct JavaSetGrouping
+ {
+ private readonly ReadOnlySpan _span;
+ private int _currentStart;
+ private int _currentEnd;
+
+ public JavaSetGrouping(ReadOnlySpan readOnlySpan)
+ {
+ _span = readOnlySpan;
+ _currentStart = 0;
+ _currentEnd = -1;
+ }
+
+ public readonly Range Current => _currentStart.._currentEnd;
+
+ public bool MoveNext()
+ {
+ _currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
+
+ if (_currentStart >= _span.Length)
+ {
+ return false;
+ }
+
+ var depth = 0;
+ var i = _currentStart;
+
+ while (i < _span.Length)
+ {
+ var ch = _span[i];
+
+ if (ch is '[' or '(')
+ {
+ depth++;
+ }
+ else if (ch is ']' or ')')
+ {
+ depth--;
+ }
+ else if (ch is ',' && depth is 0)
+ {
+ _currentEnd = i;
+ return true;
+ }
+
+ i++;
+ }
+
+ _currentEnd = _span.Length;
+ return true;
+ }
+
+ public readonly JavaSetGrouping GetEnumerator()
+ {
+ return this;
+ }
+ }
+
+ private static bool TryParseNode(ReadOnlySpan s, out SemanticVersionRange result)
+ {
+ var sets = new List>();
+
+ foreach (var range in new NodeSetGrouping(s))
+ {
+ if (s[range] is not { IsEmpty: false } part)
+ {
+ continue;
+ }
+
+ if (!TryParseNodeSet(part, out var constraints))
+ {
+ result = default;
+ return false;
+ }
+
+ sets.Add(constraints);
+ }
+
+ result = new SemanticVersionRange([..sets]);
+ return true;
+ }
+
+ private static bool TryParseNodeSet(ReadOnlySpan s, [NotNullWhen(true)] out ImmutableArray constraints)
+ {
+ if (s.IndexOf(" - ") is not -1 and var hyphen)
+ {
+ if (!SemanticVersion.TryParse(s[..hyphen].Trim(), out var lowerVersion))
+ {
+ constraints = default;
+ return false;
+ }
+
+ if (!SemanticVersion.TryParse(s[(hyphen + 3)..].Trim(), out var upperVersion))
+ {
+ constraints = default;
+ return false;
+ }
+
+ constraints =
+ [
+ new Constraint(Comparator.GreaterOrEqual, lowerVersion),
+ new Constraint(Comparator.LessOrEqual, upperVersion),
+ ];
+ return true;
+ }
+
+ var list = new List(2);
+ var current = s;
+ while (TryReadToken(ref current, out var token))
+ {
+ scoped ReadOnlySpan instruction = [];
+
+ if (token is ">=" or "<=" or ">" or "<" or "~" or "^" or "=")
+ {
+ instruction = token;
+
+ if (!TryReadToken(ref current, out token))
+ {
+ constraints = 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[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")
+ {
+ constraints = [new Constraint(Comparator.Any, default)];
+ return true;
+ }
+
+ if (!TryParseNpmConstraint(instruction, token.Trim(), list))
+ {
+ constraints = default;
+ 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 TryParseNpmConstraint(ReadOnlySpan op, ReadOnlySpan s, ICollection constraints)
+ {
+ if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out var components))
+ {
+ return false;
+ }
+
+ switch (op)
+ {
+ case "^":
+ constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
+ constraints.Add(new Constraint(Comparator.Less, GetCaretUpperBound(version, components)));
+ return true;
+ case "~":
+ constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
+ constraints.Add(new Constraint(Comparator.Less, GetTildeUpperBound(version, components)));
+ 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;
+ default:
+ switch (components)
+ {
+ case 3:
+ constraints.Add(new Constraint(Comparator.Equal, version));
+ return true;
+ case 2:
+ constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
+ constraints.Add(new Constraint(Comparator.Less, new SemanticVersion(version.Major, version.Minor + 1, 0)));
+ return true;
+ case 1:
+ constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
+ constraints.Add(new Constraint(Comparator.Less, new SemanticVersion(version.Major + 1, 0, 0)));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static SemanticVersion GetCaretUpperBound(SemanticVersion version, int components)
+ {
+ 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),
+ };
+ }
+
+ static SemanticVersion GetTildeUpperBound(SemanticVersion version, int components)
+ {
+ return components switch
+ {
+ >= 2 => new SemanticVersion(version.Major, version.Minor + 1, 0),
+ _ => new SemanticVersion(version.Major + 1, 0, 0),
};
}
}
- internal enum Comparator
+ private ref struct NodeSetGrouping
{
- Any,
- Equal,
- GreaterThan,
- GreaterThanOrEqual,
- LessThan,
- LessThanOrEqual,
- NotEqual
+ private readonly ReadOnlySpan _span;
+ private int _currentStart;
+ private int _currentEnd;
+
+ public NodeSetGrouping(ReadOnlySpan span)
+ {
+ _span = span;
+ _currentStart = 0;
+ _currentEnd = -1;
+ }
+
+ public readonly Range Current => _currentStart.._currentEnd;
+
+ public bool MoveNext()
+ {
+ _currentStart = _currentEnd is -1 ? 0 : _currentEnd + 2;
+
+ if (_currentStart >= _span.Length)
+ {
+ return false;
+ }
+
+ var index = _span[_currentStart..].IndexOf("||");
+ _currentEnd = index >= 0 ? _currentStart + index : _span.Length;
+
+ return true;
+ }
+
+ public readonly NodeSetGrouping GetEnumerator()
+ {
+ return this;
+ }
}
}
\ No newline at end of file
diff --git a/src/semver/SemanticVersionRangeJsonConverter.cs b/src/semver/SemanticVersionRangeJsonConverter.cs
deleted file mode 100644
index b56fc3d..0000000
--- a/src/semver/SemanticVersionRangeJsonConverter.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) The Geekeey Authors
-// SPDX-License-Identifier: EUPL-1.2
-
-using System.Buffers;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-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)
- {
- return default;
- }
-
- if (reader.TokenType is not JsonTokenType.String)
- {
- throw new JsonException("Expected string");
- }
-
- try
- {
- return SemanticVersionRange.Parse(reader.GetString().AsSpan());
- }
- catch (FormatException exception)
- {
- throw new JsonException(exception.Message, exception);
- }
- }
-
- public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
- {
- var capacity = value.RequiredBufferSize;
- var shared = capacity > 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
diff --git a/src/semver/SpanStringBuilder.cs b/src/semver/SpanStringBuilder.cs
new file mode 100644
index 0000000..66f9afa
--- /dev/null
+++ b/src/semver/SpanStringBuilder.cs
@@ -0,0 +1,82 @@
+// Copyright (c) The Geekeey Authors
+// SPDX-License-Identifier: EUPL-1.2
+
+namespace Geekeey.SemVer;
+
+internal ref struct SpanStringBuilder
+{
+ private Span _destination;
+ private readonly IFormatProvider? _provider;
+
+ public SpanStringBuilder(Span destination, IFormatProvider? provider = null)
+ {
+ _destination = destination;
+ _provider = provider;
+ CharsWritten = 0;
+ Success = true;
+ }
+
+ public int CharsWritten { get; private set; }
+
+ public bool Success { get; private set; }
+
+ public readonly bool TryComplete(out int charsWritten)
+ {
+ charsWritten = Success ? CharsWritten : 0;
+ return Success;
+ }
+
+ public void AppendLiteral(string? value)
+ {
+ AppendLiteral(value.AsSpan());
+ }
+
+ public void AppendLiteral(ReadOnlySpan value)
+ {
+ if (!Success)
+ {
+ return;
+ }
+
+ if (!value.TryCopyTo(_destination))
+ {
+ Success = false;
+ return;
+ }
+
+ _destination = _destination[value.Length..];
+ CharsWritten += value.Length;
+ }
+
+ public void AppendFormatted(string? value)
+ {
+ AppendLiteral(value.AsSpan());
+ }
+
+ public void AppendFormatted(ReadOnlySpan value)
+ {
+ AppendLiteral(value);
+ }
+
+ public void AppendFormatted(T value) where T : ISpanFormattable
+ {
+ AppendFormatted(value, null);
+ }
+
+ public void AppendFormatted(T value, string? format) where T : ISpanFormattable
+ {
+ if (!Success)
+ {
+ return;
+ }
+
+ if (!value.TryFormat(_destination, out var written, format, _provider))
+ {
+ Success = false;
+ return;
+ }
+
+ _destination = _destination[written..];
+ CharsWritten += written;
+ }
+}
\ No newline at end of file
diff --git a/src/semver/_internal/FormattingHelpers.cs b/src/semver/_internal/FormattingHelpers.cs
deleted file mode 100644
index d4a6be9..0000000
--- a/src/semver/_internal/FormattingHelpers.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (c) The Geekeey Authors
-// SPDX-License-Identifier: EUPL-1.2
-
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-namespace Geekeey.SemVer;
-
-// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs
-internal static class FormattingHelpers
-{
- extension(ulong)
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int CountDigits(ulong value)
- {
- // Map the log2(value) to a power of 10.
- ReadOnlySpan log2ToPow10 =
- [
- 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
- 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
- 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15,
- 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20
- ];
- Debug.Assert(log2ToPow10.Length == 64);
-
- nint elementOffset = log2ToPow10[(int)ulong.Log2(value)];
-
- // Read the associated power of 10.
- ReadOnlySpan powersOf10 =
- [
- 0, // unused entry to avoid needing to subtract
- 0,
- 10,
- 100,
- 1000,
- 10000,
- 100000,
- 1000000,
- 10000000,
- 100000000,
- 1000000000,
- 10000000000,
- 100000000000,
- 1000000000000,
- 10000000000000,
- 100000000000000,
- 1000000000000000,
- 10000000000000000,
- 100000000000000000,
- 1000000000000000000,
- 10000000000000000000,
- ];
- Debug.Assert((elementOffset + 1) <= powersOf10.Length);
- var powerOf10 = Unsafe.Add(ref MemoryMarshal.GetReference(powersOf10), elementOffset);
-
- // Return the number of digits based on the power of 10, shifted by 1
- // if it falls below the threshold.
- var index = (int)elementOffset;
- return index - (value < powerOf10 ? 1 : 0);
- }
- }
-
- extension(uint)
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int CountDigits(uint value)
- {
- ReadOnlySpan table =
- [
- 4294967296,
- 8589934582,
- 8589934582,
- 8589934582,
- 12884901788,
- 12884901788,
- 12884901788,
- 17179868184,
- 17179868184,
- 17179868184,
- 21474826480,
- 21474826480,
- 21474826480,
- 21474826480,
- 25769703776,
- 25769703776,
- 25769703776,
- 30063771072,
- 30063771072,
- 30063771072,
- 34349738368,
- 34349738368,
- 34349738368,
- 34349738368,
- 38554705664,
- 38554705664,
- 38554705664,
- 41949672960,
- 41949672960,
- 41949672960,
- 42949672960,
- 42949672960,
- ];
- Debug.Assert(table.Length == 32, "Every result of uint.Log2(value) needs a long entry in the table.");
-
- var tableValue = table[(int)uint.Log2(value)];
- return (int)((value + tableValue) >> 32);
- }
- }
-}
\ No newline at end of file