Skip to content

Instantly share code, notes, and snippets.

@mavaddat
Forked from josheinstein/PSObjectComparer.cs
Last active November 28, 2021 01:39
Show Gist options
  • Save mavaddat/7ad22531abcfa88e0cc134667810eb3a to your computer and use it in GitHub Desktop.
Save mavaddat/7ad22531abcfa88e0cc134667810eb3a to your computer and use it in GitHub Desktop.
IComparer for PSObjects in PowerShell
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;
}
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;
}
else if ( obj != null ) {
// Object can provide its own hash code (we hope)
return obj.GetHashCode( );
}
else {
// Null
return 0;
}
}
/// <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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment