Skip to content

Instantly share code, notes, and snippets.

@josheinstein
Created May 1, 2012 21:17
Show Gist options
  • Save josheinstein/2571476 to your computer and use it in GitHub Desktop.
Save josheinstein/2571476 to your computer and use it in GitHub Desktop.
IComparer for PSObjects in PowerShell
namespace Einstein.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
}
}
@CarstenG2
Copy link

How to use this in Powershell?
E.g. I need to sort a large Array of objects as fast a spossible, where each object has 3 properties and I need to sort by property 1 and 2 in ascending order like in this quite slow sample:

$myArray | sort property1, property2

now I wnat t speed it up by using a syntax like this:
[array]::sort($myArray, $comparer)

How can this be done?

@OtterKring
Copy link

OtterKring commented Aug 18, 2019

Similar here. I have managed to use the Sort() for a GenericList:
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
gci | select name,length| %{$list.add($_)
$list.Sort({$args[0].name.compareto($args[1].name)})

... but I have not yet managed to use the BinarySearch method the same way. Any ideas?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment