File: VersionComparer.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Versioning\NuGet.Versioning.csproj (NuGet.Versioning)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using NuGet.Shared;

namespace NuGet.Versioning
{
    /// <summary>
    /// An IVersionComparer for NuGetVersion and NuGetVersion types.
    /// </summary>
    public sealed class VersionComparer : IVersionComparer
    {
        /// <summary>
        /// Gets an IVersionComparer based on the specified VersionComparison mode.
        /// </summary>
        /// <param name="versionComparison">The version comparison mode to use.</param>
        /// <returns>An IVersionComparer instance.</returns>
        public static IVersionComparer Get(VersionComparison versionComparison)
        {
            return versionComparison switch
            {
                VersionComparison.Default => Default,
                VersionComparison.Version => Version,
                VersionComparison.VersionRelease => VersionRelease,
                VersionComparison.VersionReleaseMetadata => VersionReleaseMetadata,
                _ => new VersionComparer(versionComparison)
            };
        }

        private readonly VersionComparison _mode;

        /// <summary>
        /// Creates a VersionComparer using the default mode.
        /// </summary>
        public VersionComparer()
        {
            _mode = VersionComparison.Default;
        }

        /// <summary>
        /// Creates a VersionComparer that respects the given comparison mode.
        /// </summary>
        /// <param name="versionComparison">comparison mode</param>
        public VersionComparer(VersionComparison versionComparison)
        {
            _mode = versionComparison;
        }

        /// <summary>
        /// Determines if both versions are equal.
        /// </summary>
        public bool Equals(SemanticVersion? x, SemanticVersion? y)
        {
            if (ReferenceEquals(x, y))
            {
                return true;
            }

            if (ReferenceEquals(y, null))
            {
                return false;
            }

            if (ReferenceEquals(x, null))
            {
                return false;
            }

            if (_mode == VersionComparison.Default || _mode == VersionComparison.VersionRelease)
            {
                // Compare the version and release labels
                return (x.Major == y.Major
                    && x.Minor == y.Minor
                    && x.Patch == y.Patch
                    && GetRevisionOrZero(x) == GetRevisionOrZero(y)
                    && AreReleaseLabelsEqual(x, y));
            }

            // Use the full comparer for non-default scenarios
            return Compare(x, y) == 0;
        }

        /// <summary>
        /// Compares the given versions using the VersionComparison mode.
        /// </summary>
        public static int Compare(SemanticVersion? version1, SemanticVersion? version2, VersionComparison versionComparison)
        {
            IVersionComparer comparer = VersionComparer.Get(versionComparison);
#pragma warning disable CS8604 // Possible null reference argument.
            // The BCL is missing nullable annotations on IComparable<T> before net5.0
            return comparer.Compare(version1, version2);
#pragma warning restore CS8604 // Possible null reference argument.
        }

        /// <summary>
        /// Gives a hash code based on the normalized version string.
        /// </summary>
        public int GetHashCode(SemanticVersion version)
        {
            if (ReferenceEquals(version, null))
            {
                return 0;
            }

            var combiner = new HashCodeCombiner();

            combiner.AddObject(version.Major);
            combiner.AddObject(version.Minor);
            combiner.AddObject(version.Patch);

            var nuGetVersion = version as NuGetVersion;
            if (nuGetVersion != null
                && nuGetVersion.Revision > 0)
            {
                combiner.AddObject(nuGetVersion.Revision);
            }

            if (_mode == VersionComparison.Default
                || _mode == VersionComparison.VersionRelease
                || _mode == VersionComparison.VersionReleaseMetadata)
            {
                var labels = GetReleaseLabelsOrNull(version);

                if (labels != null)
                {
                    var comparer = StringComparer.OrdinalIgnoreCase;
                    foreach (var label in labels)
                    {
                        combiner.AddObject(label, comparer);
                    }
                }
            }

            if (_mode == VersionComparison.VersionReleaseMetadata && version.HasMetadata)
            {
                combiner.AddObject(version.Metadata, StringComparer.OrdinalIgnoreCase);
            }

            return combiner.CombinedHash;
        }

        /// <summary>
        /// Compare versions.
        /// </summary>
        public int Compare(SemanticVersion? x, SemanticVersion? y)
        {
            if (ReferenceEquals(x, y))
            {
                return 0;
            }

            if (ReferenceEquals(y, null))
            {
                return 1;
            }

            if (ReferenceEquals(x, null))
            {
                return -1;
            }

            // compare version
            var result = x.Major.CompareTo(y.Major);
            if (result != 0)
            {
                return result;
            }

            result = x.Minor.CompareTo(y.Minor);
            if (result != 0)
            {
                return result;
            }

            result = x.Patch.CompareTo(y.Patch);
            if (result != 0)
            {
                return result;
            }

            var legacyX = x as NuGetVersion;
            var legacyY = y as NuGetVersion;

            result = CompareLegacyVersion(legacyX, legacyY);
            if (result != 0)
            {
                return result;
            }

            if (_mode != VersionComparison.Version)
            {
                // compare release labels
                var xLabels = GetReleaseLabelsOrNull(x);
                var yLabels = GetReleaseLabelsOrNull(y);

                if (xLabels != null
                    && yLabels == null)
                {
                    return -1;
                }

                if (xLabels == null
                    && yLabels != null)
                {
                    return 1;
                }

                if (xLabels != null
                    && yLabels != null)
                {
                    result = CompareReleaseLabels(xLabels, yLabels);
                    if (result != 0)
                    {
                        return result;
                    }
                }

                // compare the metadata
                if (_mode == VersionComparison.VersionReleaseMetadata)
                {
                    result = StringComparer.OrdinalIgnoreCase.Compare(x.Metadata ?? string.Empty, y.Metadata ?? string.Empty);
                    if (result != 0)
                    {
                        return result;
                    }
                }
            }

            return 0;
        }

        /// <summary>
        /// Compares the 4th digit of the version number.
        /// </summary>
        private static int CompareLegacyVersion(NuGetVersion? legacyX, NuGetVersion? legacyY)
        {
            var result = 0;

            // true if one has a 4th version number
            if (legacyX != null
                && legacyY != null)
            {
                result = legacyX.Version.CompareTo(legacyY.Version);
            }
            else if (legacyX != null
                     && legacyX.Version.Revision > 0)
            {
                result = 1;
            }
            else if (legacyY != null
                     && legacyY.Version.Revision > 0)
            {
                result = -1;
            }

            return result;
        }

        /// <summary>
        /// A default comparer that compares metadata as strings.
        /// </summary>
        public static readonly IVersionComparer Default = new VersionComparer(VersionComparison.Default);

        /// <summary>
        /// A comparer that uses only the version numbers.
        /// </summary>
        public static readonly IVersionComparer Version = new VersionComparer(VersionComparison.Version);

        /// <summary>
        /// Compares versions without comparing the metadata.
        /// </summary>
        public static readonly IVersionComparer VersionRelease = new VersionComparer(VersionComparison.VersionRelease);

        /// <summary>
        /// A version comparer that follows SemVer 2.0.0 rules.
        /// </summary>
        public static readonly IVersionComparer VersionReleaseMetadata = new VersionComparer(VersionComparison.VersionReleaseMetadata);

        /// <summary>
        /// Compares sets of release labels.
        /// </summary>
        private static int CompareReleaseLabels(string[] version1, string[] version2)
        {
            var result = 0;

            var count = Math.Max(version1.Length, version2.Length);

            for (var i = 0; i < count; i++)
            {
                var aExists = i < version1.Length;
                var bExists = i < version2.Length;

                if (!aExists && bExists)
                {
                    return -1;
                }

                if (aExists && !bExists)
                {
                    return 1;
                }

                // compare the labels
                result = CompareRelease(version1[i], version2[i]);

                if (result != 0)
                {
                    return result;
                }
            }

            return result;
        }

        /// <summary>
        /// Release labels are compared as numbers if they are numeric, otherwise they will be compared
        /// as strings.
        /// </summary>
        private static int CompareRelease(string version1, string version2)
        {
            var version1Num = 0;
            var version2Num = 0;
            var result = 0;

            // check if the identifiers are numeric
            var v1IsNumeric = int.TryParse(version1, out version1Num);
            var v2IsNumeric = int.TryParse(version2, out version2Num);

            // if both are numeric compare them as numbers
            if (v1IsNumeric && v2IsNumeric)
            {
                result = version1Num.CompareTo(version2Num);
            }
            else if (v1IsNumeric || v2IsNumeric)
            {
                // numeric labels come before alpha labels
                if (v1IsNumeric)
                {
                    result = -1;
                }
                else
                {
                    result = 1;
                }
            }
            else
            {
                // Ignoring 2.0.0 case sensitive compare. Everything will be compared case insensitively as 2.0.1 specifies.
                result = StringComparer.OrdinalIgnoreCase.Compare(version1, version2);
            }

            return result;
        }

        /// <summary>
        /// Returns an array of release labels from the version, or null.
        /// </summary>
        private static string[]? GetReleaseLabelsOrNull(SemanticVersion version)
        {
            string[]? labels = null;

            // Check if labels exist
            if (version.IsPrerelease)
            {
                // Try to use string[] which is how labels are normally stored.
                var enumerable = version.ReleaseLabels;
                labels = enumerable as string[];

                if (labels == null && enumerable != null)
                {
                    // This is not the expected type, enumerate and convert to an array.
                    labels = enumerable.ToArray();
                }
            }

            return labels;
        }

        /// <summary>
        /// Compare release labels
        /// </summary>
        private static bool AreReleaseLabelsEqual(SemanticVersion x, SemanticVersion y)
        {
            var xLabels = GetReleaseLabelsOrNull(x);
            var yLabels = GetReleaseLabelsOrNull(y);

            if (xLabels == null && yLabels != null)
            {
                return false;
            }

            if (xLabels != null && yLabels == null)
            {
                return false;
            }

            if (xLabels != null && yLabels != null)
            {
                // Both versions must have the same number of labels to be equal
                if (xLabels.Length != yLabels.Length)
                {
                    return false;
                }

                // Check if the labels are the same
                for (var i = 0; i < xLabels.Length; i++)
                {
                    if (!StringComparer.OrdinalIgnoreCase.Equals(xLabels[i], yLabels[i]))
                    {
                        return false;
                    }
                }
            }

            // labels are equal
            return true;
        }

        /// <summary>
        /// Returns the fourth version number or zero.
        /// </summary>
        private static int GetRevisionOrZero(SemanticVersion version)
        {
            var nugetVersion = version as NuGetVersion;
            return nugetVersion?.Revision ?? 0;
        }
    }
}