using System; using System.Text.RegularExpressions; using Newtonsoft.Json; using StardewModdingAPI.Framework; namespace StardewModdingAPI { /// A semantic version with an optional release tag. public struct Version : IComparable { /********* ** 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; set; } /// The minor version incremented for backwards-compatible changes. public int MinorVersion { get; set; } /// The patch version for backwards-compatible bug fixes. public int PatchVersion { get; set; } /// An optional build tag. public string Build { get; set; } /// Obsolete. [JsonIgnore] [Obsolete("Use " + nameof(Version) + "." + nameof(Version.ToString) + " instead.")] public string VersionString { get { Program.DeprecationManager.Warn($"{nameof(Version)}.{nameof(Version.VersionString)}", "1.0", DeprecationLevel.Notice); return this.ToString(); } } /********* ** 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 Version(int major, int minor, int patch, string build) { this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; this.Build = build; } /// Construct an instance. /// The semantic version string. internal Version(string version) { var match = Version.Regex.Match(version); if (!match.Success) throw new FormatException($"The input '{version}' is not a 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 ? match.Groups["build"].Value : "").Trim(' ', '-', '.'); } /// 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. public int CompareTo(Version other) { // compare version numbers if (this.MajorVersion != other.MajorVersion) return this.MajorVersion - other.MajorVersion; if (this.MinorVersion != other.MinorVersion) return this.MinorVersion - other.MinorVersion; if (this.PatchVersion != other.PatchVersion) return this.PatchVersion - other.PatchVersion; // stable version (without tag) supercedes prerelease (with tag) bool curHasTag = !string.IsNullOrWhiteSpace(this.Build); bool otherHasTag = !string.IsNullOrWhiteSpace(other.Build); if (!curHasTag && otherHasTag) return 1; if (curHasTag && !otherHasTag) return -1; // else compare by string return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); } /// 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.GetNormalisedTag(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) || tag == "0") return null; return tag; } } }