Source/PSObjectComparer.cs

namespace Einstein.PowerShell.LINQ
{
 
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics.Contracts;
    using System.Globalization;
    using System.Linq;
    using System.Management.Automation;
    using System.Threading;
 
    /// <summary>
    /// Compares two objects using PowerShell language semantics.
    /// </summary>
    public class PSObjectComparer : IComparer, IEqualityComparer, IComparer<PSObject>, IEqualityComparer<PSObject>, IComparer<object>, IEqualityComparer<object>
    {
 
        private readonly StringComparer _StringComparer;
        private static readonly Lazy<PSObjectComparer> _Default = new Lazy<PSObjectComparer>();
 
        #region Constructors
 
        /// <summary>
        /// Initializes a new instance of the <see cref="T:PSObjectComparer"/> class.
        /// </summary>
        public PSObjectComparer( )
            : this( true, null )
        {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="T:PSObjectComparer"/> class.
        /// </summary>
        /// <param name="ignoreCase">True to ignore case when comparing strings, otherwise false.</param>
        /// <param name="ascending">True to sort in ascending order, otherwise false.</param>
        /// <param name="cultureInfo">The culture info.</param>
        public PSObjectComparer( bool ignoreCase, CultureInfo cultureInfo = null )
        {
 
            IgnoreCase = ignoreCase;
            CultureInfo = cultureInfo ?? Thread.CurrentThread.CurrentCulture;
 
            _StringComparer = StringComparer.Create( CultureInfo, IgnoreCase );
 
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// A shared default case-insensitive instance of the comparer.
        /// </summary>
        public static PSObjectComparer Default
        {
            get
            {
                return _Default.Value;
            }
        }
 
        /// <summary>
        /// True to ignore case when comparing strings, otherwise false.
        /// </summary>
        public bool IgnoreCase
        {
            get;
            private set;
        }
 
        /// <summary>
        /// Culture information for the thread.
        /// </summary>
        public CultureInfo CultureInfo
        {
            get;
            private set;
        }
 
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// A signed integer that indicates the relative values of <paramref name="x"/> and <paramref name="y"/>, as shown in the following table.
        /// Less than zero <paramref name="x"/> is less than <paramref name="y"/>.
        /// Zero <paramref name="x"/> equals <paramref name="y"/>.
        /// Greater than zero <paramref name="x"/> is greater than <paramref name="y"/>.
        /// </returns>
        /// <exception cref="T:ArgumentException">
        /// Neither <paramref name="x"/> nor <paramref name="y"/> implements the <see cref="T:IComparable"/> interface.
        /// -or-
        /// <paramref name="x"/> and <paramref name="y"/> are of different types and neither one can handle comparisons with the other.
        /// </exception>
        public int Compare( object x, object y )
        {
 
            // Short circuit attempt
            if ( ReferenceEquals( x, y ) ) { return 0; }
            if ( ReferenceEquals( x, null ) ) { return -1; }
            if ( ReferenceEquals( y, null ) ) { return 1; }
 
            // Unwrap PSObject x
            var psX = x as PSObject;
            if ( psX != null ) { x = psX.BaseObject; }
 
            // Unwrap PSObject y
            var psY = y as PSObject;
            if ( psY != null ) { y = psY.BaseObject; }
 
            // Are these custom PSObject's with dynamic members?
            var pscX = x as PSCustomObject;
            var pscY = y as PSCustomObject;
            if ( pscX != null && pscY != null ) {
 
                // Compare each member individually.
                // If the two objects don't contain the exact same properties
                // in the set (order does not matter) then their equality
                // will be affected.
                var allProperties = psX.Properties.Select( p => p.Name ).Union( psY.Properties.Select( p => p.Name ), _StringComparer );
                foreach ( string propertyName in allProperties ) {
 
                    var propX = psX.Properties[propertyName];
                    var propY = psY.Properties[propertyName];
 
                    if ( propX == null ) { return -1; }
                    if ( propY == null ) { return 1; }
 
                    // Defer to the LanguagePrimitives class which can handle type coersion
                    // and powershell semantics for handling strings and other special cases
                    int c = LanguagePrimitives.Compare( propX.Value, propY.Value, IgnoreCase, CultureInfo );
                    if ( c != 0 ) {
                        return c;
                    }
 
                }
 
                return 0;
 
            }
            else {
 
                // Objects x and y are .NET objects or scalar types (as opposed to PSCustomObject)
                // Defer to the LanguagePrimitives class which can handle type coersion
                // and powershell semantics for handling strings and other special cases
 
                return LanguagePrimitives.Compare( x, y, IgnoreCase, CultureInfo );
 
            }
 
        }
 
        /// <summary>
        /// Determines whether the specified <see cref="T:Object"/> is equal to this instance.
        /// </summary>
        /// <param name="x">The <see cref="T:Object"/> to compare with this instance.</param>
        /// <param name="y">The y.</param>
        /// <returns>
        /// <c>true</c> if the specified <see cref="T:Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:ArgumentException">
        /// <paramref name="x"/> and <paramref name="y"/> are of different types and neither one can handle comparisons with the other.
        /// </exception>
        public new bool Equals( object x, object y )
        {
 
            if ( ReferenceEquals( x, y ) ) {
                return true;
            }
 
            if ( ReferenceEquals( x, null ) ) {
                return false;
            }
 
            if ( ReferenceEquals( y, null ) ) {
                return false;
            }
 
            return Compare( x, y ) == 0;
 
        }
 
        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// </returns>
        /// <exception cref="T:ArgumentNullException">The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.</exception>
        public int GetHashCode( object obj )
        {
 
            // Unwrap PSObject
            var psObj = obj as PSObject;
            if ( psObj != null ) {
                obj = psObj.BaseObject;
            }
 
            if (obj == null) {
                return 0;
            }
 
            var strObj = obj as String;
            if (strObj != null) {
                return _StringComparer.GetHashCode(strObj);
            }
 
            var pscObj = obj as PSCustomObject;
            if ( pscObj != null ) {
 
                // Object is a custom PSObject.
                // Dynamic members, no GetHashCode implementation.
 
                // Seed the hash with a prime
                int hash = 13;
 
                // Include all properties in the hash
                foreach ( var prop in psObj.Properties ) {
                    hash = GetHashCode( hash, prop );
                }
 
                return hash;
 
            }
             
            // Object can provide its own hash code (we hope)
            return obj.GetHashCode( );
 
        }
 
        /// <summary>
        /// Gets the hash code of a PSPropertyInfo by hashing both its name and value.
        /// </summary>
        /// <param name="hashCode"></param>
        /// <param name="prop"></param>
        /// <returns></returns>
        private int GetHashCode( int hashCode, PSPropertyInfo prop )
        {
 
            // Hash the property name
            int nameHash = _StringComparer.GetHashCode( prop.Name );
             
            int valueHash = 0;
 
            if ( prop.IsGettable ) {
 
                object value = prop.Value;
 
                string valueAsString = value as String;
                if ( valueAsString != null ) {
                     
                    // Hash the string property value
                    valueHash = _StringComparer.GetHashCode( valueAsString );
                }
                else if ( value != null ) {
                     
                    // Hash other types
                    valueHash = value.GetHashCode( );
 
                }
 
            }
 
            // Combine the hashes along with any previously calculated hash
            int hash = hashCode;
            hash = ( hash * 7 ) + nameHash;
            hash = ( hash * 7 ) + valueHash;
 
            return hash;
 
        }
 
        #endregion
 
        #region IComparer<PSObject> Members
 
        /// <summary>
        /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// Value
        /// Condition
        /// Less than zero
        /// <paramref name="x"/> is less than <paramref name="y"/>.
        /// Zero
        /// <paramref name="x"/> equals <paramref name="y"/>.
        /// Greater than zero
        /// <paramref name="x"/> is greater than <paramref name="y"/>.
        /// </returns>
        int IComparer<PSObject>.Compare( PSObject x, PSObject y )
        {
            return Compare( x, y );
        }
 
        #endregion
 
        #region IEqualityComparer<PSObject> Members
 
        /// <summary>
        /// Determines whether the specified objects are equal.
        /// </summary>
        /// <param name="x">The first object of type <paramref name="T"/> to compare.</param>
        /// <param name="y">The second object of type <paramref name="T"/> to compare.</param>
        /// <returns>
        /// true if the specified objects are equal; otherwise, false.
        /// </returns>
        bool IEqualityComparer<PSObject>.Equals( PSObject x, PSObject y )
        {
            return Equals( x, y );
        }
 
        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException">
        /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.
        /// </exception>
        int IEqualityComparer<PSObject>.GetHashCode( PSObject obj )
        {
            return GetHashCode( obj );
        }
 
        #endregion
 
        #region IComparer<object> Members
 
        /// <summary>
        /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// A signed integer that indicates the relative values of <paramref name="x"/> and <paramref name="y"/>, as shown in the following table.Value Meaning Less than zero <paramref name="x"/> is less than <paramref name="y"/>. Zero <paramref name="x"/> equals <paramref name="y"/>. Greater than zero <paramref name="x"/> is greater than <paramref name="y"/>.
        /// </returns>
        /// <exception cref="T:System.ArgumentException">Neither <paramref name="x"/> nor <paramref name="y"/> implements the <see cref="T:System.IComparable"/> interface.-or- <paramref name="x"/> and <paramref name="y"/> are of different types and neither one can handle comparisons with the other. </exception>
        int IComparer<object>.Compare( object x, object y )
        {
            return Compare( x, y );
        }
 
        #endregion
 
        #region IEqualityComparer<object> Members
 
        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="x">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <param name="y">The y.</param>
        /// <returns>
        /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:System.ArgumentException">
        /// <paramref name="x"/> and <paramref name="y"/> are of different types and neither one can handle comparisons with the other.</exception>
        bool IEqualityComparer<object>.Equals( object x, object y )
        {
            return Equals( x, y );
        }
 
        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException">The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.</exception>
        int IEqualityComparer<object>.GetHashCode( object obj )
        {
            return GetHashCode( obj );
        }
 
        #endregion
 
    }
 
}