using System;
using System.Text.RegularExpressions;
namespace StardewModdingAPI
{
/// A semantic version with an optional release tag.
public class SemanticVersion : ISemanticVersion
{
/*********
** Properties
*********/
/// A regular expression matching a semantic version string.
/// Derived from https://github.com/maxhauser/semver.
private static readonly Regex Regex = new Regex(@"^(?\d+)(\.(?\d+))?(\.(?\d+))?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
/*********
** Accessors
*********/
/// The major version incremented for major API changes.
public int MajorVersion { get; }
/// The minor version incremented for backwards-compatible changes.
public int MinorVersion { get; }
/// The patch version for backwards-compatible bug fixes.
public int PatchVersion { get; }
/// An optional build tag.
public string Build { get; }
/*********
** Public methods
*********/
/// Construct an instance.
/// The major version incremented for major API changes.
/// The minor version incremented for backwards-compatible changes.
/// The patch version for backwards-compatible bug fixes.
/// An optional build tag.
public SemanticVersion(int major, int minor, int patch, string build = null)
{
this.MajorVersion = major;
this.MinorVersion = minor;
this.PatchVersion = patch;
this.Build = this.GetNormalisedTag(build);
}
/// Construct an instance.
/// The semantic version string.
/// The is not a valid semantic version.
public SemanticVersion(string version)
{
var match = SemanticVersion.Regex.Match(version);
if (!match.Success)
throw new FormatException($"The input '{version}' is not a valid semantic version.");
this.MajorVersion = int.Parse(match.Groups["major"].Value);
this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
this.Build = match.Groups["build"].Success ? this.GetNormalisedTag(match.Groups["build"].Value) : null;
}
/// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.
/// The version to compare with this instance.
/// The implementation is defined by Semantic Version 2.0 (http://semver.org/).
public int CompareTo(ISemanticVersion other)
{
const int same = 0;
const int curNewer = 1;
const int curOlder = -1;
// compare stable versions
if (this.MajorVersion != other.MajorVersion)
return this.MajorVersion.CompareTo(other.MajorVersion);
if (this.MinorVersion != other.MinorVersion)
return this.MinorVersion.CompareTo(other.MinorVersion);
if (this.PatchVersion != other.PatchVersion)
return this.PatchVersion.CompareTo(other.PatchVersion);
if (this.Build == other.Build)
return same;
// stable supercedes pre-release
bool curIsStable = string.IsNullOrWhiteSpace(this.Build);
bool otherIsStable = string.IsNullOrWhiteSpace(other.Build);
if (curIsStable)
return curNewer;
if (otherIsStable)
return curOlder;
// compare two pre-release tag values
string[] curParts = this.Build.Split('.');
string[] otherParts = other.Build.Split('.');
for (int i = 0; i < curParts.Length; i++)
{
// longer prerelease tag supercedes if otherwise equal
if (otherParts.Length <= i)
return curNewer;
// compare if different
if (curParts[i] != otherParts[i])
{
// compare numerically if possible
{
int curNum, otherNum;
if (int.TryParse(curParts[i], out curNum) && int.TryParse(otherParts[i], out otherNum))
return curNum.CompareTo(otherNum);
}
// else compare lexically
return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase);
}
}
// fallback (this should never happen)
return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
/// Get whether this version is older than the specified version.
/// The version to compare with this instance.
public bool IsOlderThan(ISemanticVersion other)
{
return this.CompareTo(other) < 0;
}
/// Get whether this version is newer than the specified version.
/// The version to compare with this instance.
public bool IsNewerThan(ISemanticVersion other)
{
return this.CompareTo(other) > 0;
}
/// Get a string representation of the version.
public override string ToString()
{
// version
string result = this.PatchVersion != 0
? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}"
: $"{this.MajorVersion}.{this.MinorVersion}";
// tag
string tag = this.Build;
if (tag != null)
result += $"-{tag}";
return result;
}
/*********
** Private methods
*********/
/// Get a normalised build tag.
/// The tag to normalise.
private string GetNormalisedTag(string tag)
{
tag = tag?.Trim().Trim('-', '.');
if (string.IsNullOrWhiteSpace(tag))
return null;
if (tag == "0")
return null; // from incorrect examples in old SMAPI documentation
return tag;
}
}
}