Files
Argus/Infrastructure/SemanticVersionUtility.cs
Курнат Андрей 5f80c4bf6a Initial Argus web catalog
2026-06-20 19:55:11 +03:00

241 lines
6.9 KiB
C#

namespace Argus.Infrastructure;
public static class SemanticVersionUtility
{
public static int Compare(string? left, string? right)
{
var leftParsed = SemanticVersion.TryParse(left, out var leftVersion);
var rightParsed = SemanticVersion.TryParse(right, out var rightVersion);
if (leftParsed && rightParsed)
{
return leftVersion.CompareTo(rightVersion);
}
if (leftParsed)
{
return 1;
}
if (rightParsed)
{
return -1;
}
return string.Compare(left, right, StringComparison.OrdinalIgnoreCase);
}
private sealed class SemanticVersion : IComparable<SemanticVersion>
{
private readonly IReadOnlyList<Identifier> _preReleaseIdentifiers;
private SemanticVersion(int major, int minor, int patch, IReadOnlyList<Identifier> preReleaseIdentifiers)
{
Major = major;
Minor = minor;
Patch = patch;
_preReleaseIdentifiers = preReleaseIdentifiers;
}
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public bool IsPreRelease => _preReleaseIdentifiers.Count > 0;
public static bool TryParse(string? value, out SemanticVersion version)
{
version = null!;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
if (trimmed.Length > 1 && (trimmed[0] == 'v' || trimmed[0] == 'V') && char.IsDigit(trimmed[1]))
{
trimmed = trimmed[1..];
}
var buildSeparatorIndex = trimmed.IndexOf('+');
if (buildSeparatorIndex >= 0)
{
trimmed = trimmed[..buildSeparatorIndex];
}
string corePart;
string? preReleasePart = null;
var preReleaseSeparatorIndex = trimmed.IndexOf('-');
if (preReleaseSeparatorIndex >= 0)
{
corePart = trimmed[..preReleaseSeparatorIndex];
preReleasePart = trimmed[(preReleaseSeparatorIndex + 1)..];
}
else
{
corePart = trimmed;
}
var coreSegments = corePart.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (coreSegments.Length is < 1 or > 3)
{
return false;
}
if (!TryParseNumeric(coreSegments[0], out var major))
{
return false;
}
var minor = 0;
if (coreSegments.Length >= 2 && !TryParseNumeric(coreSegments[1], out minor))
{
return false;
}
var patch = 0;
if (coreSegments.Length == 3 && !TryParseNumeric(coreSegments[2], out patch))
{
return false;
}
var preReleaseIdentifiers = new List<Identifier>();
if (!string.IsNullOrWhiteSpace(preReleasePart))
{
foreach (var segment in preReleasePart.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (!Identifier.TryParse(segment, out var identifier))
{
return false;
}
preReleaseIdentifiers.Add(identifier);
}
}
version = new SemanticVersion(major, minor, patch, preReleaseIdentifiers);
return true;
}
public int CompareTo(SemanticVersion? other)
{
if (other is null)
{
return 1;
}
var majorComparison = Major.CompareTo(other.Major);
if (majorComparison != 0)
{
return majorComparison;
}
var minorComparison = Minor.CompareTo(other.Minor);
if (minorComparison != 0)
{
return minorComparison;
}
var patchComparison = Patch.CompareTo(other.Patch);
if (patchComparison != 0)
{
return patchComparison;
}
if (!IsPreRelease && !other.IsPreRelease)
{
return 0;
}
if (!IsPreRelease)
{
return 1;
}
if (!other.IsPreRelease)
{
return -1;
}
var count = Math.Max(_preReleaseIdentifiers.Count, other._preReleaseIdentifiers.Count);
for (var index = 0; index < count; index++)
{
if (index >= _preReleaseIdentifiers.Count)
{
return -1;
}
if (index >= other._preReleaseIdentifiers.Count)
{
return 1;
}
var identifierComparison = _preReleaseIdentifiers[index].CompareTo(other._preReleaseIdentifiers[index]);
if (identifierComparison != 0)
{
return identifierComparison;
}
}
return 0;
}
private static bool TryParseNumeric(string value, out int parsed) =>
int.TryParse(value, out parsed) && parsed >= 0;
private readonly record struct Identifier(bool IsNumeric, long NumericValue, string TextValue) : IComparable<Identifier>
{
public static bool TryParse(string value, out Identifier identifier)
{
identifier = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
foreach (var character in trimmed)
{
if (!char.IsLetterOrDigit(character) && character != '-')
{
return false;
}
}
if (long.TryParse(trimmed, out var numericValue))
{
identifier = new Identifier(true, numericValue, trimmed);
return true;
}
identifier = new Identifier(false, 0, trimmed);
return true;
}
public int CompareTo(Identifier other)
{
if (IsNumeric && other.IsNumeric)
{
return NumericValue.CompareTo(other.NumericValue);
}
if (IsNumeric)
{
return -1;
}
if (other.IsNumeric)
{
return 1;
}
return string.Compare(TextValue, other.TextValue, StringComparison.OrdinalIgnoreCase);
}
}
}
}