using CommunityToolkit.Diagnostics; using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; namespace ZuneModCore { [JsonConverter(typeof(ReleaseVersionJsonConverter))] public sealed class ReleaseVersion : ICloneable, IComparable, IComparable, IEquatable { /// /// Gets the value of the major component of the version number for the current object. /// public int Major { get; set; } /// /// Gets the value of the minor component of the version number for the current object. /// public int Minor { get; set; } /// /// Gets the value of the build component of the version number for the current object. /// public int Build { get; set; } /// /// Gets the value of the revision component of the version number for the current object. /// public int Revision { get; set; } private Phase _Phase = Phase.Unknown; /// /// Gets the value of the release phase component of the version number for the current object. /// public Phase Phase { set => _Phase = value; get { if (_Phase == Phase.Unknown) { #if DEBUG _Phase = Phase.Debug; #else _Phase = Phase.Production; #endif } return _Phase; } } /// /// Initializes a new instance of the class. /// public ReleaseVersion() { } /// /// Initializes a new instance of the class with the specified major, /// minor, build, revision, and release phase. /// /// /// The major version number. /// /// /// The minor version number. /// /// /// The build version number. /// /// /// The revision version number. /// /// /// The release phase. /// /// /// , , , or is less than zero. /// public ReleaseVersion(int major, int minor, int build = 0, int revision = 0, Phase phase = Phase.Unknown) { Guard.IsGreaterThanOrEqualTo(major, 0, nameof(major)); Guard.IsGreaterThanOrEqualTo(minor, 0, nameof(minor)); Guard.IsGreaterThanOrEqualTo(build, 0, nameof(build)); Guard.IsGreaterThanOrEqualTo(revision, 0, nameof(revision)); Major = major; Minor = minor; Build = build; Revision = revision; Phase = phase; } /// /// Returns a new object whose value is the same as the current /// object. /// /// /// A new whose values are a copy of the current object. /// public object Clone() { return new ReleaseVersion(Major, Minor, Build, Revision, Phase); } /// /// Compares the current object to a specified object and returns /// an indication of their relative values. /// /// /// An object to compare, or null. /// /// /// A signed integer that indicates the relative values of the two objects, as shown /// in the following table. /// Return value – Meaning /// Less than zero – The current object is a version before . /// Zero – The current object is the same version as . /// Greater than zero – The current object is a version subsequent /// to , or is null. /// /// /// is not of type . /// public int CompareTo(object? version) { if (version == null) { return 1; } if (version is ReleaseVersion v) { return CompareTo(v); } throw new ArgumentException(nameof(version) + " must be of type " + nameof(ReleaseVersion)); } /// /// Compares the current object to a specified object /// and returns an indication of their relative values. /// /// /// A object to compare to the current object, or null. /// /// /// A signed integer that indicates the relative values of the two objects, as shown /// in the following table. /// Return value – Meaning /// Less than zero – The current object is a version before . /// Zero – The current object is the same version as . /// Greater than zero – The current object is a version subsequent /// to , or is null. /// public int CompareTo(ReleaseVersion? value) { return ReferenceEquals(value, this) ? 0 : value is null ? 1 : Major != value.Major ? (Major > value.Major ? 1 : -1) : Minor != value.Minor ? (Minor > value.Minor ? 1 : -1) : Build != value.Build ? (Build > value.Build ? 1 : -1) : Revision != value.Revision ? (Revision > value.Revision ? 1 : -1) : Phase != value.Phase? ((byte)Phase > (byte)value.Phase ? 1 : -1) : 0; } /// /// Returns a value indicating whether the current object is equal /// to a specified object. /// /// /// An object to compare with the current object, or null. /// /// /// true if the current object and /// are both objects, /// and every component of the current object matches the corresponding /// component of ; otherwise, false. /// public override bool Equals(object? obj) { return Equals(obj as ReleaseVersion); } /// /// Returns a value indicating whether the current object and a specified /// object represent the same value. /// /// /// A object to compare to the current object, or null. /// /// /// true if every component of the current object matches the corresponding /// component of the parameter; otherwise, false. /// public bool Equals(ReleaseVersion? obj) { return ReferenceEquals(obj, this) || (obj is not null && Major == obj.Major && Minor == obj.Minor && Build == obj.Build && Revision == obj.Revision && Phase == obj.Phase); } /// /// Returns a hash code for the current object. /// /// /// A 32-bit signed integer hash code. /// public override int GetHashCode() { return HashCode.Combine(Major, Minor, Build, Revision, Phase); } /// /// Determines whether two specified objects are equal. /// /// /// The first object. /// /// /// The second object. /// /// /// true if v1 equals v2; otherwise, false. /// // Force inline as the true/false ternary takes it above ALWAYS_INLINE size even though the asm ends up smaller [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(ReleaseVersion? v1, ReleaseVersion? v2) { // Test "right" first to allow branch elimination when inlined for null checks (== null) // so it can become a simple test if (v2 is null) { // return true/false not the test result https://github.com/dotnet/runtime/issues/4207 return (v1 is null) ? true : false; } // Quick reference equality test prior to calling the virtual Equality return ReferenceEquals(v2, v1) ? true : v2.Equals(v1); } /// /// Determines whether the first specified object is greater than /// the second specified object. /// /// /// The first object. /// /// /// The second object. /// /// /// true if v1 is greater than v2; otherwise, false. /// public static bool operator >(ReleaseVersion? v1, ReleaseVersion? v2) => v2 < v1; /// /// Determines whether the first specified object is greater than /// or equal to the second specified object. /// /// /// The first object. /// /// /// The second object. /// /// /// true if v1 is greater than or equal to v2; otherwise, false. /// public static bool operator >=(ReleaseVersion? v1, ReleaseVersion? v2) => v2 <= v1; /// /// Determines whether two specified objects are not equal. /// /// /// The first object. /// /// /// The second object. /// /// /// true if v1 does not equal v2; otherwise, false. /// public static bool operator !=(ReleaseVersion? v1, ReleaseVersion? v2) => !(v1 == v2); /// /// Determines whether the first specified object is less than the /// second specified object. /// /// /// The first object. /// /// /// The second object. /// /// /// true if v1 is less than v2; otherwise, false. /// public static bool operator <(ReleaseVersion? v1, ReleaseVersion? v2) { if (v1 is null) { return !(v2 is null); } return v1.CompareTo(v2) < 0; } /// /// Determines whether the first specified object is less than or /// equal to the second object. /// /// /// The first object. /// /// /// The second object. /// /// /// true if v1 is less than or equal to v2; otherwise, false. /// public static bool operator <=(ReleaseVersion? v1, ReleaseVersion? v2) { if (v1 is null) { return true; } return v1.CompareTo(v2) <= 0; } /// /// Converts the string representation of a version number to an equivalent /// object. /// /// /// A string containing the major, minor, build, and revision numbers, where each /// number is delimited with a period character ('.'), followed by a dash character /// ('-') and the release phase. /// /// /// An object that is equivalent to the version number specified in the input parameter. /// public static ReleaseVersion Parse(string input) { ReleaseVersion version = new(); string versionPart = input; int phaseIdx = input.IndexOf('-'); if (phaseIdx != -1) { versionPart = input[..phaseIdx]; string phaseComp = input[(phaseIdx + 1)..]; if (phaseComp.ToLowerInvariant() == "rc") version.Phase = Phase.ReleaseCandidate; else version.Phase = Enum.Parse(phaseComp, true); } string[] components = versionPart.Split('.'); Guard.IsGreaterThanOrEqualTo(components.Length, 2, nameof(components)); if (components.Length >= 2) { version.Major = int.Parse(components[0]); version.Minor = int.Parse(components[1]); if (components.Length >= 3) { version.Build = int.Parse(components[2]); if (components.Length >= 4) { version.Revision = int.Parse(components[3]); } } } return version; } /// /// Converts the value of the current object to its equivalent /// representation. /// /// /// The string representation of the values of the major, minor, build, and /// revision components of the current object, as depicted in the /// following format. Each component is separated by a period character ('.'), /// and the release phase is appended to the end after a dash character ('-'). Square /// brackets ('[' and ']') indicate a component that will not appear in the return /// value if the component is not defined: major.minor[.build[.revision]][-phase] For example, /// if you create a object using the constructor Version(1, 1), the /// returned string is "1.1". If you create a object using the constructor /// Version(1, 3, 4, 2, Phase.Beta), the returned string is "1.3.4.2-beta". /// public override string ToString() => ToString(true); /// /// /// When set to true, the string will be formatted as major.minor.build.revision-phase, /// even if an optional component is not present. /// public string ToString(bool includeAllComponents) { const char numDelim = '.'; const char phaseDelim = '-'; string output = Major.ToString() + numDelim + Minor.ToString(); if (Build > 0 || includeAllComponents) { output += numDelim + Build.ToString(); if (Revision > 0 || includeAllComponents) { output += numDelim + Revision.ToString(); } } if ((Phase != Phase.Unknown && Phase != Phase.Production) || includeAllComponents) { output += phaseDelim + (Phase == Phase.ReleaseCandidate ? "rc" : Phase.ToString().ToLowerInvariant()); } return output; } /// /// Tries to convert the string representation of a version number to an equivalent /// object, and returns a value that indicates whether the conversion /// succeeded. /// /// /// A string that contains a version number to convert. /// /// /// When this method returns, contains the equivalent of the number /// that is contained in input, if the conversion succeeded. If input is null, , /// or if the conversion fails, result is null when the method returns. /// /// /// true if the input parameter was converted successfully; otherwise, false. /// public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out ReleaseVersion? result) { try { Guard.IsNotNull(input, nameof(input)); result = Parse(input); return true; } catch { result = null; return false; } } } public enum Phase : byte { Unknown, Debug, Alpha, Beta, ReleaseCandidate, Production } internal class ReleaseVersionJsonConverter : JsonConverter { public override ReleaseVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return ReleaseVersion.Parse(reader.GetString()!); } public override void Write(Utf8JsonWriter writer, ReleaseVersion value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } } }