File: src\nuget-client\build\Shared\EqualityUtility.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace NuGet.Shared
{
    internal static class EqualityUtility
    {
        /// <summary>
        /// Compares two enumerables for equality, ordered according to the specified key and optional comparer. Handles null values gracefully.
        /// </summary>
        /// <typeparam name="TSource">The type of the list</typeparam>
        /// <typeparam name="TKey">The type of the sorting key</typeparam>
        /// <param name="self">This list</param>
        /// <param name="other">The other list</param>
        /// <param name="keySelector">The function to extract the key from each item in the list</param>
        /// <param name="orderComparer">An optional comparer for comparing keys</param>
        /// <param name="sequenceComparer">An optional comparer for sequences</param>
        internal static bool OrderedEquals<TSource, TKey>(this IEnumerable<TSource>? self, IEnumerable<TSource>? other, Func<TSource, TKey> keySelector, IComparer<TKey>? orderComparer = null, IEqualityComparer<TSource>? sequenceComparer = null)
        {
            Debug.Assert(orderComparer != null || typeof(TKey) != typeof(string), "Argument " + "orderComparer" + " must be provided if " + "TKey" + " is a string.");
            Debug.Assert(sequenceComparer != null || typeof(TSource) != typeof(string), "Argument " + "sequenceComparer" + " must be provided if " + "TSource" + " is a string.");

            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            return self
                .OrderBy(keySelector, orderComparer)
                .SequenceEqual(other.OrderBy(keySelector, orderComparer), sequenceComparer);
        }

        /// <summary>
        /// Compares two collections for equality, ordered according to the specified key and optional comparer. Handles null values gracefully.
        /// </summary>
        /// <typeparam name="TSource">The type of the list</typeparam>
        /// <typeparam name="TKey">The type of the sorting key</typeparam>
        /// <param name="self">This list</param>
        /// <param name="other">The other list</param>
        /// <param name="keySelector">The function to extract the key from each item in the list</param>
        /// <param name="orderComparer">An optional comparer for comparing keys</param>
        /// <param name="sequenceComparer">An optional comparer for sequences</param>
        internal static bool OrderedEquals<TSource, TKey>(this ICollection<TSource>? self, ICollection<TSource>? other, Func<TSource, TKey> keySelector, IComparer<TKey>? orderComparer = null, IEqualityComparer<TSource>? sequenceComparer = null)
        {
            Debug.Assert(orderComparer != null || typeof(TKey) != typeof(string), "Argument " + "orderComparer" + " must be provided if " + "TKey" + " is a string.");
            Debug.Assert(sequenceComparer != null || typeof(TSource) != typeof(string), "Argument " + "sequenceComparer" + " must be provided if " + "TSource" + " is a string.");

            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            if (self.Count != other.Count)
            {
                return false;
            }

            if (self.Count == 0)
            {
                return true;
            }

            if (self.Count == 1)
            {
                sequenceComparer ??= EqualityComparer<TSource>.Default;
                return sequenceComparer.Equals(self.First(), other.First());
            }

            return self
                .OrderBy(keySelector, orderComparer)
                .SequenceEqual(other.OrderBy(keySelector, orderComparer), sequenceComparer);
        }

        /// <summary>
        /// Compares two lists for equality, ordered according to the specified key and optional comparer. Handles null values gracefully.
        /// </summary>
        /// <typeparam name="TSource">The type of the list</typeparam>
        /// <typeparam name="TKey">The type of the sorting key</typeparam>
        /// <param name="self">This list</param>
        /// <param name="other">The other list</param>
        /// <param name="keySelector">The function to extract the key from each item in the list</param>
        /// <param name="orderComparer">An optional comparer for comparing keys</param>
        /// <param name="sequenceComparer">An optional comparer for sequences</param>
        internal static bool OrderedEquals<TSource, TKey>(this IList<TSource>? self, IList<TSource>? other, Func<TSource, TKey> keySelector, IComparer<TKey>? orderComparer = null, IEqualityComparer<TSource>? sequenceComparer = null)
        {
            Debug.Assert(orderComparer != null || typeof(TKey) != typeof(string), "Argument " + "orderComparer" + " must be provided if " + "TKey" + " is a string.");
            Debug.Assert(sequenceComparer != null || typeof(TSource) != typeof(string), "Argument " + "sequenceComparer" + " must be provided if " + "TSource" + " is a string.");

            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            if (self.Count != other.Count)
            {
                return false;
            }

            if (self.Count == 0)
            {
                return true;
            }

            if (self.Count == 1)
            {
                return (sequenceComparer ?? EqualityComparer<TSource>.Default).Equals(self[0], other[0]);
            }

            return self
                .OrderBy(keySelector, orderComparer)
                .SequenceEqual(other.OrderBy(keySelector, orderComparer), sequenceComparer);
        }

        /// <summary>
        /// Compares two sequence for equality, allowing either sequence to be null. If one is null, both have to be
        /// null for equality.
        /// </summary>
        internal static bool SequenceEqualWithNullCheck<T>(
            this IEnumerable<T>? self,
            IEnumerable<T>? other,
            IEqualityComparer<T>? comparer = null)
        {
            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            if (comparer == null)
            {
                comparer = EqualityComparer<T>.Default;
            }

            return self.SequenceEqual(other, comparer);
        }

        /// <summary>
        /// Compares two collections for equality, allowing either collection to be null. If one is null, both have to be
        /// null for equality.
        /// </summary>
        internal static bool SequenceEqualWithNullCheck<T>(
            this ICollection<T>? self,
            ICollection<T>? other,
            IEqualityComparer<T>? comparer = null)
        {
            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            if (self.Count != other.Count)
            {
                return false;
            }

            if (self.Count == 0)
            {
                return true;
            }

            if (comparer == null)
            {
                comparer = EqualityComparer<T>.Default;
            }

            return self.SequenceEqual(other, comparer);
        }

        /// <summary>
        /// Compares two lists for equality, allowing either list to be null. If one is null, both have to be
        /// null for equality.
        /// </summary>
        internal static bool SequenceEqualWithNullCheck<T>(
            this IList<T>? self,
            IList<T>? other,
            IEqualityComparer<T>? comparer = null)
        {
            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            if (self.Count != other.Count)
            {
                return false;
            }

            if (self.Count == 0)
            {
                return true;
            }

            if (comparer == null)
            {
                comparer = EqualityComparer<T>.Default;
            }

            if (self.Count == 1)
            {
                return comparer.Equals(self[0], other[0]);
            }

            return self.SequenceEqual(other, comparer);
        }

        /// <summary>
        /// Compares two sets for equality, allowing either sequence to be null.
        /// If one is null, both have to be null for equality.
        /// </summary>
        internal static bool SetEqualsWithNullCheck<T>(
            this ISet<T>? self,
            ISet<T>? other,
            IEqualityComparer<T>? comparer = null)
        {
            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            // Verify they could be equal by count
            if (self.Count != other.Count)
            {
                return false;
            }

            if (self.Count == 0)
            {
                return true;
            }

            if (comparer == null)
            {
                comparer = EqualityComparer<T>.Default;
            }

            var set = new HashSet<T>(self, comparer);

            return set.SetEquals(other);
        }

        internal static bool DictionaryEquals<TKey, TValue>(
            IDictionary<TKey, TValue> self,
            IDictionary<TKey, TValue> other,
            Func<TValue, TValue, bool>? compareValues = null)
            where TKey : notnull
        {
            var comparer = EqualityComparer<TValue>.Default;
            Func<TValue, TValue, bool> comparerFunc = (s, o) => comparer.Equals(s, o);
            compareValues = compareValues ?? comparerFunc;

            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            // Verify they could be equal by count
            if (self.Count != other.Count)
            {
                return false;
            }

            if (self.Count == 0)
            {
                return true;
            }

            if (!self.Keys.OrderedEquals(
                other.Keys,
                s => s,
                orderComparer: Comparer<TKey>.Default,
                sequenceComparer: EqualityComparer<TKey>.Default))
            {
                return false;
            }

            foreach (var key in self.Keys)
            {
                if (!compareValues(self[key], other[key]))
                {
                    return false;
                }
            }

            return true;
        }

        internal static bool EqualsWithNullCheck<T>(T self, T other)
        {
            bool identityEquals;
            if (TryIdentityEquals(self, other, out identityEquals))
            {
                return identityEquals;
            }

            return self?.Equals(other) ?? false;
        }

        /// <summary>
        /// Determines if the current string contains a value equal "false".  Leading and trailing whitespace are trimmed and the comparison is case-insensitive
        /// </summary>
        /// <param name="value">The string to compare.</param>
        /// <returns><see langword="true" /> if the current string is equal to a value of "false", otherwise <see langword="false" />.</returns>
        internal static bool EqualsFalse(this string value)
        {
            return !string.IsNullOrWhiteSpace(value) && bool.FalseString.Equals(value.Trim(), StringComparison.OrdinalIgnoreCase);
        }

        private static bool TryIdentityEquals<T>(
            [NotNullWhen(returnValue: false)] T? self,
            [NotNullWhen(returnValue: false)] T? other,
            out bool equals)
        {
            // Are they the same instance? This handles the case where both are null.
            if (ReferenceEquals(self, other))
            {
                equals = true;
                return true;
            }

            // Is only one of the sequences null?
            if (self == null || other == null)
            {
                equals = false;
                return true;
            }

            // Inconclusive.
            equals = false;
            return false;
        }
    }
}