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 { private readonly IReadOnlyList _preReleaseIdentifiers; private SemanticVersion(int major, int minor, int patch, IReadOnlyList 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(); 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 { 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); } } } }