File: VersionRange.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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NuGet.Shared;

namespace NuGet.Versioning
{
    /// <summary>
    /// Represents a range of versions and a preferred order.
    /// </summary>
    /// <remarks>
    /// Immutable, although subclasses may introduce mutable state.
    /// </remarks>
    public partial class VersionRange : VersionRangeBase, IFormattable
    {
        private readonly FloatRange? _floatRange;
        private readonly string? _originalString;

        /// <summary>
        /// Creates a range that is greater than or equal to the minVersion.
        /// </summary>
        /// <param name="minVersion">Lower bound of the version range.</param>
        public VersionRange(NuGetVersion minVersion)
            : this(minVersion, null)
        {
        }

        /// <summary>
        /// Creates a range that is greater than or equal to the minVersion with the given float behavior.
        /// </summary>
        /// <param name="minVersion">Lower bound of the version range.</param>
        /// <param name="floatRange">Floating behavior.</param>
        public VersionRange(NuGetVersion minVersion, FloatRange? floatRange)
            : this(
                  minVersion: minVersion,
                  includeMinVersion: true,
                  maxVersion: null,
                  includeMaxVersion: false,
                  originalString: null,
                  floatRange: floatRange)
        {
        }

        /// <summary>
        /// Clones a version range and applies a new float range.
        /// </summary>
        public VersionRange(VersionRange range, FloatRange floatRange)
            : this(range == null ? throw new ArgumentNullException(nameof(range)) : range.MinVersion,
                  range.IsMinInclusive,
                  range.MaxVersion,
                  range.IsMaxInclusive,
                  floatRange)
        {
        }

        /// <summary>
        /// Creates a VersionRange with the given min and max.
        /// </summary>
        /// <param name="minVersion">Lower bound of the version range.</param>
        /// <param name="includeMinVersion">True if minVersion satisfies the condition.</param>
        /// <param name="maxVersion">Upper bound of the version range.</param>
        /// <param name="includeMaxVersion">True if maxVersion satisfies the condition.</param>
        /// <param name="floatRange">The floating range subset used to find the best version match.</param>
        /// <param name="originalString">The original string being parsed to this object.</param>
        public VersionRange(NuGetVersion? minVersion = null, bool includeMinVersion = true, NuGetVersion? maxVersion = null,
            bool includeMaxVersion = false, FloatRange? floatRange = null, string? originalString = null)
            : base(minVersion, includeMinVersion, maxVersion, includeMaxVersion)
        {
            if (floatRange != null && minVersion == null)
            {
                throw ResourcesFormatter.CannotBeNullWhenParameterIsNull(nameof(minVersion), nameof(floatRange));
            }

            _floatRange = floatRange;
            _originalString = originalString;
        }

        /// <summary>
        /// True if the range has a floating version above the min version.
        /// </summary>
        [MemberNotNullWhen(true, nameof(MinVersion))]
        [MemberNotNullWhen(true, nameof(Float))]
        [MemberNotNullWhen(true, nameof(_floatRange))]
        public bool IsFloating
        {
            get { return Float != null && Float.FloatBehavior != NuGetVersionFloatBehavior.None; }
        }

        /// <inheritdoc cref="VersionRangeBase.MinVersion"/>
        public new NuGetVersion? MinVersion => base.MinVersion;

        /// <inheritdoc cref="VersionRangeBase.MaxVersion"/>
        public new NuGetVersion? MaxVersion => base.MaxVersion;

        /// <inheritdoc cref="VersionRangeBase.HasLowerBound"/>
        [MemberNotNullWhen(true, nameof(MinVersion))]
        public new bool HasLowerBound => base.HasLowerBound;

        /// <inheritdoc cref="VersionRangeBase.IsMinInclusive"/>
        [MemberNotNullWhen(true, nameof(MinVersion))]
        public new bool IsMinInclusive => base.IsMinInclusive;

        /// <inheritdoc cref="VersionRangeBase.HasUpperBound"/>
        [MemberNotNullWhen(true, nameof(MaxVersion))]
        public new bool HasUpperBound => base.HasUpperBound;

        /// <inheritdoc cref="VersionRangeBase.IsMaxInclusive"/>
        [MemberNotNullWhen(true, nameof(MaxVersion))]
        public new bool IsMaxInclusive => base.IsMaxInclusive;

        /// <inheritdoc cref="VersionRangeBase.HasLowerAndUpperBounds"/>
        [MemberNotNullWhen(true, nameof(MinVersion))]
        [MemberNotNullWhen(true, nameof(MaxVersion))]
        public new bool HasLowerAndUpperBounds => base.HasLowerAndUpperBounds;

        /// <summary>
        /// Optional floating range used to determine the best version match.
        /// </summary>
        public FloatRange? Float
        {
            get { return _floatRange; }
        }

        /// <summary>
        /// Original string being parsed to this object.
        /// </summary>
        public string? OriginalString
        {
            get { return _originalString; }
        }

        /// <summary>
        /// Normalized range string.
        /// </summary>
        public override string ToString()
        {
            return ToNormalizedString();
        }

        /// <summary>
        /// Normalized range string.
        /// </summary>
        public virtual string ToNormalizedString()
        {
            return ToString("N", VersionRangeFormatter.Instance);
        }

        /// <summary>
        /// A legacy version range compatible with NuGet 2.8.3
        /// </summary>
        public virtual string ToLegacyString()
        {
            return ToString("D", VersionRangeFormatter.Instance);
        }

        /// <summary>
        /// A short legacy version range compatible with NuGet 2.8.3.
        /// Ex: 1.0.0
        /// </summary>
        public virtual string ToLegacyShortString()
        {
            return ToString("T", VersionRangeFormatter.Instance);
        }

        /// <summary>
        /// Format the version range with an IFormatProvider
        /// </summary>
        public string ToString(string? format, IFormatProvider? formatProvider)
        {
            string? formattedString = null;

            if (formatProvider == null
                || !TryFormatter(format, formatProvider, out formattedString))
            {
                formattedString = ToString();
            }

            return formattedString;
        }

        /// <summary>
        /// Format the range
        /// </summary>
        protected bool TryFormatter(string? format, IFormatProvider formatProvider, [NotNullWhen(true)] out string? formattedString)
        {
            var formatted = false;
            formattedString = null;

            if (formatProvider != null)
            {
                var formatter = formatProvider.GetFormat(GetType()) as ICustomFormatter;
                if (formatter != null)
                {
                    formatted = true;
                    formattedString = formatter.Format(format, this, formatProvider);
                }
            }

            return formatted;
        }

        /// <summary>
        /// Format the version range in Pretty Print format.
        /// </summary>
        public string PrettyPrint()
        {
            return ToString("P", VersionRangeFormatter.Instance);
        }

        /// <summary>
        /// Return the version that best matches the range.
        /// </summary>
        public NuGetVersion? FindBestMatch(IEnumerable<NuGetVersion>? versions)
        {
            NuGetVersion? bestMatch = null;

            if (versions != null)
            {
                foreach (var version in versions)
                {
                    if (IsBetter(bestMatch, version))
                    {
                        bestMatch = version;
                    }
                }
            }

            return bestMatch;
        }

        /// <summary>
        /// Determines if a given version is better suited to the range than a current version.
        /// </summary>
        public bool IsBetter(NuGetVersion? current, NuGetVersion? considering)
        {
            if (ReferenceEquals(current, considering))
            {
                return false;
            }

            // null checks
            if (ReferenceEquals(considering, null))
            {
                return false;
            }

            // If the range contains only stable versions disallow prerelease versions
            if (!HasPrereleaseBounds
                && considering.IsPrerelease
                && _floatRange?.FloatBehavior != NuGetVersionFloatBehavior.Prerelease
                && _floatRange?.FloatBehavior != NuGetVersionFloatBehavior.PrereleaseMajor
                && _floatRange?.FloatBehavior != NuGetVersionFloatBehavior.PrereleaseMinor
                && _floatRange?.FloatBehavior != NuGetVersionFloatBehavior.PrereleasePatch
                && _floatRange?.FloatBehavior != NuGetVersionFloatBehavior.PrereleaseRevision
                && _floatRange?.FloatBehavior != NuGetVersionFloatBehavior.AbsoluteLatest)
            {
                return false;
            }

            if (!Satisfies(considering))
            {
                // keep null over a value outside of the range
                return false;
            }

            if (ReferenceEquals(current, null))
            {
                return true;
            }

            if (IsFloating)
            {
                // check if either version is in the floating range
                var curInRange = _floatRange.Satisfies(current);
                var conInRange = _floatRange.Satisfies(considering);

                if (curInRange && !conInRange)
                {
                    // take the version in the range
                    return false;
                }
                else if (conInRange && !curInRange)
                {
                    // take the version in the range
                    return true;
                }
                else if (curInRange && conInRange)
                {
                    // prefer the highest one if both are in the range
                    return current < considering;
                }
                else
                {
                    // neither are in range
                    var curToLower = current < _floatRange.MinVersion;
                    var conToLower = considering < _floatRange.MinVersion;

                    if (curToLower && !conToLower)
                    {
                        // favor the version above the range
                        return true;
                    }
                    else if (!curToLower && conToLower)
                    {
                        // favor the version above the range
                        return false;
                    }
                    else if (!curToLower
                             && !conToLower)
                    {
                        // favor the lower version if we are above the range
                        return current > considering;
                    }
                    else if (curToLower && conToLower)
                    {
                        // favor the higher version if we are below the range
                        return current < considering;
                    }
                }
            }

            // Favor lower versions
            return current > considering;
        }

        /// <summary>
        /// Removes the floating snapshot part of the minimum version if it exists.
        /// Ex: 1.0.0-* -> 1.0.0
        /// </summary>
        public VersionRange ToNonSnapshotRange()
        {
            // For non-floating versions there is no change
            var result = this;

            if (IsFloating)
            {
                var minVersion = MinVersion;

                if (Float.FloatBehavior == NuGetVersionFloatBehavior.Prerelease)
                {
                    minVersion = GetNonSnapshotVersion(minVersion);
                }

                // Drop the floating range from the new range regardless of the float type
                result = new VersionRange(
                    minVersion,
                    IsMinInclusive,
                    MaxVersion,
                    IsMaxInclusive);
            }

            return result;
        }

        /// <summary>
        /// Remove the snapshot version section of a version
        /// </summary>
        private static NuGetVersion GetNonSnapshotVersion(NuGetVersion version)
        {
            var nonSnapshotVersion = version;

            var lastLabel = version.ReleaseLabels.LastOrDefault() ?? string.Empty;

            var endsWithZero = lastLabel == "0";
            var endsWithDash = lastLabel.EndsWith("-", StringComparison.Ordinal);

            if (endsWithZero || endsWithDash)
            {
                var fixedReleaseLabel = string.Empty;

                if (endsWithDash)
                {
                    if (lastLabel.EndsWith("--", StringComparison.Ordinal))
                    {
                        // For labels such as rc1-* an additional - is added by nuget
                        fixedReleaseLabel = lastLabel.Substring(0, lastLabel.Length - 2);
                    }
                    else
                    {
                        // Remove the - for 1.0.0-* (1.0.0--)
                        fixedReleaseLabel = lastLabel.Substring(0, lastLabel.Length - 1);
                    }
                }

                // Remove the last label and add in the fixed label if one exists.
                var fixedLabels = version.ReleaseLabels.Take(version.ReleaseLabels.Count() - 1).ToList();

                if (!string.IsNullOrEmpty(fixedReleaseLabel))
                {
                    fixedLabels.Add(fixedReleaseLabel);
                }

                nonSnapshotVersion = new NuGetVersion(
                    version.Major,
                    version.Minor,
                    version.Patch,
                    version.Revision,
                    fixedLabels,
                    version.Metadata);
            }

            return nonSnapshotVersion;
        }

        /// <summary>
        /// ToLegacyShortString that also includes floating ranges
        /// </summary>
        public virtual string ToShortString()
        {
            return ToString("A", VersionRangeFormatter.Instance);
        }

        /// <summary>
        /// Equals implementation for VersionRange.
        /// </summary>
        public bool Equals(VersionRange? other)
        {
            if (other == null)
            {
                return false;
            }

            if (ReferenceEquals(this, other))
            {
                return true;
            }

            return base.Equals(other) &&
                IsFloating == other.IsFloating &&
                EqualityUtility.EqualsWithNullCheck(Float, other.Float);
        }

        /// <summary>
        /// Compare the obj as VersionRange.
        /// </summary>
        public override bool Equals(object? obj)
        {
            return Equals(obj as VersionRange);
        }

        /// <summary>
        /// Returns the VersionRangeBase hashcode
        /// </summary>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }
}