// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Text.RegularExpressions;
namespace EpicGames.Core
{
///
/// Wrapper for a semver version string (https://semver.org/)
///
public struct SemVer : IEquatable, IComparable
{
ref struct Field
{
public readonly int Pos;
public readonly ReadOnlySpan Text;
public Field(int pos, ReadOnlySpan text)
{
Pos = pos;
Text = text;
}
public int End => Pos + Text.Length;
}
readonly string _text;
readonly int _length;
ReadOnlySpan Span => _text.AsSpan(0, _length);
// Regex taken from official site.
static readonly Regex s_pattern = new Regex(@"^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$");
///
/// Constructor
///
private SemVer(string text, int length)
{
_text = text;
_length = length;
}
///
/// Parses the given string as a semantic version
///
/// Text to parse
/// Resulting semver object
public static SemVer Parse(string text)
{
SemVer result;
if (!TryParse(text, out result))
{
throw new ArgumentException($"String '{text}' is not a valid semantic version string. See https://semver.org/.", nameof(text));
}
return result;
}
///
/// Attempt to parse the given string as a semantic version
///
/// Text to parse
/// Resulting version
/// True on success
public static bool TryParse(string text, out SemVer semVer)
{
Match match = s_pattern.Match(text);
if (match.Success)
{
semVer = new SemVer(text, match.Groups[1].Length);
return true;
}
else
{
semVer = default;
return false;
}
}
///
public override int GetHashCode() => String.GetHashCode(_text.AsSpan(0, _length), StringComparison.Ordinal);
///
public override bool Equals(object? obj) => obj is SemVer other && Equals(other);
///
public bool Equals(SemVer other) => Span.SequenceEqual(other.Span);
///
public int CompareTo(SemVer other) => Compare(Span, other.Span);
///
public override string ToString() => _text;
///
/// Compare two versions
///
/// First version to compare
/// Second version to compare
/// A number indicating the order of the two version strings
public static int Compare(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span);
static int Compare(ReadOnlySpan lhs, ReadOnlySpan rhs)
{
// Compare major fields
Field lhsField = GetField(lhs, 0);
Field rhsField = GetField(rhs, 0);
int diff = CompareNumericFields(lhsField.Text, rhsField.Text);
if (diff != 0)
{
return diff;
}
// Compare minor fields
lhsField = GetField(lhs, lhsField.End + 1);
rhsField = GetField(rhs, rhsField.End + 1);
diff = CompareNumericFields(lhsField.Text, rhsField.Text);
if (diff != 0)
{
return diff;
}
// Compare patch fields
lhsField = GetField(lhs, lhsField.End + 1);
rhsField = GetField(rhs, rhsField.End + 1);
diff = CompareNumericFields(lhsField.Text, rhsField.Text);
if (diff != 0)
{
return diff;
}
// If either is complete, it takes precedence
if (lhsField.End == lhs.Length || rhsField.End == rhs.Length)
{
return ((lhsField.End == lhs.Length) ? 1 : 0) - ((rhsField.End == rhs.Length) ? 1 : 0);
}
// Otherwise compare each pre-release field
while (lhsField.End < lhs.Length && rhsField.End < rhs.Length)
{
lhsField = GetField(lhs, lhsField.End + 1);
rhsField = GetField(rhs, rhsField.End + 1);
diff = CompareFields(lhsField.Text, rhsField.Text);
if (diff != 0)
{
return diff;
}
}
// Give precedence to the longer version string
return ((lhsField.End == lhs.Length) ? 0 : 1) - ((rhsField.End == rhs.Length) ? 0 : 1);
}
static Field GetField(ReadOnlySpan text, int pos)
{
int end = pos + 1;
while (end < text.Length && text[end] != '.' && text[end] != '-' && text[end] != '+')
{
end++;
}
return new Field(pos, text.Slice(pos, end - pos));
}
static int CompareFields(ReadOnlySpan lhs, ReadOnlySpan rhs)
{
// Rules:
// Identifiers consisting of only digits are compared numerically.
// Identifiers with letters or hyphens are compared lexically in ASCII sort order.
// Numeric identifiers always have lower precedence than non-numeric identifiers.
if (IsNumeric(lhs))
{
return IsNumeric(rhs) ? CompareNumericFields(lhs, rhs) : -1;
}
else
{
return IsNumeric(rhs) ? +1 : lhs.SequenceCompareTo(rhs);
}
}
static int CompareNumericFields(ReadOnlySpan lhs, ReadOnlySpan rhs)
{
return (lhs.Length == rhs.Length) ? lhs.SequenceCompareTo(rhs) : (lhs.Length - rhs.Length);
}
static bool IsNumeric(ReadOnlySpan field)
{
for (int i = 0; i < field.Length; i++)
{
if (field[i] < '0' || field[i] > '9')
{
return false;
}
}
return true;
}
///
/// Compares two versions for equality
///
public static bool operator ==(SemVer lhs, SemVer rhs) => lhs.Equals(rhs);
///
/// Compares two versions for inequality
///
public static bool operator !=(SemVer lhs, SemVer rhs) => !lhs.Equals(rhs);
///
/// Tests if one version has lower precedence than another
///
public static bool operator <(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) < 0;
///
/// Tests if one version has lower or equal precedence to another
///
public static bool operator <=(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) <= 0;
///
/// Tests if one version has greater precedence than another
///
public static bool operator >(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) > 0;
///
/// Tests if one version has greater or equal precedence to another
///
public static bool operator >=(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) >= 0;
}
}