Skip to content

Instantly share code, notes, and snippets.

@sweko
Last active December 29, 2015 10:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sweko/7654887 to your computer and use it in GitHub Desktop.
Save sweko/7654887 to your computer and use it in GitHub Desktop.
I always need to determine which objects from a collection that was round tripped to the UI were added, removed and changed. The code is usually straight-forward, but repetitive. The ListDifference class is a helper with a simple usage that makes it easy to do that.
/// <summary>
/// Given two collections, determines added, removed and changed items.
/// </summary>
public class ListDifference<T>
{
/// <summary>
/// The items that were added to the collection
/// </summary>
public IEnumerable<T> AddedItems { get; private set; }
/// <summary>
/// The items that were removed from the collection
/// </summary>
public IEnumerable<T> RemovedItems { get; private set; }
/// <summary>
/// The items that are changed in the collection, according to the specified comparer.
/// </summary>
public IEnumerable<Tuple<T, T>> ChangedItems { get; private set; }
/// <summary>
/// The items that are not changed in the collection, according to the specified comparer.
/// If no comparer is specified, all items with matching keys are considered unchanged.
/// </summary>
public IEnumerable<Tuple<T, T>> UnchangedItems { get; private set; }
/// <summary>
/// Creates a new differences object between an original collection and an updated version
/// </summary>
/// <param name="original">The original values of the collection</param>
/// <param name="updated">The updated values of the collection</param>
/// <param name="keyExtractor">An expression that extracts the key from an object</param>
/// <param name="comparer">An expression that compares two objects</param>
public ListDifference(IEnumerable<T> original, IEnumerable<T> updated, Func<T, object> keyExtractor = null, Func<T, T, bool> comparer = null)
{
if (keyExtractor == null)
keyExtractor = t => t;
AddedItems = updated.Difference(original, keyExtractor);
RemovedItems = original.Difference(updated, keyExtractor);
var matches = from u in updated
from o in original
where keyExtractor(u).Equals(keyExtractor(o))
select Tuple.Create(o, u);
if (comparer == null)
{
//we are unable to detect changed items
ChangedItems = null;
UnchangedItems = matches;
}
else
{
ChangedItems = matches.Where(t => !comparer(t.Item1, t.Item2));
UnchangedItems = matches.Where(t => comparer(t.Item1, t.Item2));
}
}
}
/// <summary>
/// Given two collections, determines added, removed and changed items.
/// Static version for using type inference when calling.
/// </summary>
public static class ListDifference
{
/// <summary>
/// Given two collections, determines added, removed and changed items.
/// </summary>
/// <typeparam name="T">The type of the elements of the collections</typeparam>
/// <param name="original">The original values of the collection</param>
/// <param name="updated">The updated values of the collection</param>
/// <param name="keyExtractor">An expression that extracts the key from an object</param>
/// <param name="comparer">An expression that compares two objects</param>
/// <returns>The differences object between an original collection and an updated version</returns>
public static ListDifference<T> Difference<T>(IEnumerable<T> original, IEnumerable<T> updated, Func<T, object> keyExtractor = null, Func<T, T, bool> comparer = null)
{
return new ListDifference<T>(original, updated, keyExtractor, comparer);
}
}
void Main()
{
Console.WriteLine("Persons example");
var csiOriginal = new List<Person>
{
new Person {ID = 1, FirstName = "William", LastName = "Petersen"},
new Person {ID = 2, FirstName = "Marg", LastName = "Helgenberger"},
new Person {ID = 3, FirstName = "Jorja", LastName = "Fox"},
new Person {ID = 4, FirstName = "Gary", LastName = "Dourdan"},
new Person {ID = 5, FirstName = "Eric", LastName = "Szmanda"},
};
var csiUpdated = new List<Person>
{
new Person {ID = 3, FirstName = "Jorja-An", LastName = "Fox"},
new Person {ID = 5, FirstName = "Eric", LastName = "Szmanda"},
new Person {ID = 6, FirstName = "Ted", LastName = "Danson"},
new Person {ID = 7, FirstName = "Elisabeth", LastName = "Shue"},
};
//detect changes in first name, as well as added and removed items
var differences = ListDifference.Difference(csiOriginal, csiUpdated, p => p.ID, (f,s) => f.FirstName.Equals(s.FirstName));
Console.WriteLine("Added persons");
foreach(Person p in differences.AddedItems)
{
Console.WriteLine(p.FirstName);
}
Console.WriteLine();
Console.WriteLine("Removed persons");
foreach(Person p in differences.RemovedItems)
{
Console.WriteLine(p.FirstName);
}
Console.WriteLine();
Console.WriteLine("Changed persons");
foreach(var tuple in differences.ChangedItems)
{
Console.WriteLine(string.Format("First: {0}, Second: {1}", tuple.Item1.FirstName, tuple.Item2.FirstName));
}
Console.WriteLine();
Console.WriteLine("Unchanged persons");
foreach(var tuple in differences.UnchangedItems)
{
Console.WriteLine(string.Format("First: {0}, Second: {1}", tuple.Item1.FirstName, tuple.Item2.FirstName));
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Integers example");
int[] integersOriginal = {1, 2, 3, 4, 5};
int[] integersUpdated = {2, 4, 6, 8};
//whole object is used for comparison
var intDifferences = ListDifference.Difference(integersOriginal, integersUpdated);
Console.WriteLine("Added items");
foreach(int i in intDifferences.AddedItems)
{
Console.WriteLine(i);
}
Console.WriteLine();
Console.WriteLine("Removed items");
foreach(int i in intDifferences.RemovedItems)
{
Console.WriteLine(i);
}
Console.WriteLine();
Console.WriteLine("Unchanged items");
foreach(var tuple in intDifferences.UnchangedItems)
{
Console.WriteLine(string.Format("First: {0}, Second: {1}", tuple.Item1, tuple.Item2));
}
}
public class Person
{
public int ID {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
}
/// <summary>
/// IEnumerable extensions.
/// </summary>
public static class IEnumerableExtensions
{
/// <summary>
/// Produces the set difference of two sequences by using the key extractor to compare keys.
/// </summary>
/// <typeparam name="T">The type of the elements of the input sequences</typeparam>
/// <param name="first">A collection whose elements that are not matched in second will be returned.</param>
/// <param name="second">A collection whose elements that match in the first sequence will cause those elements to be removed from the returned sequence.</param>
/// <param name="keyExtractor">The key extractor that return the key to be matched.</param>
/// <returns>A sequence that contains the set difference of the elements of two sequences</returns>
public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, TKey> keyExtractor)
{
return first.Except(second, new KeyEqualityComparer<T, TKey>(keyExtractor));
}
/// <summary>
/// Produces the set difference of two sequences by using the key extractor to compare keys. Alias for the Except method
/// </summary>
/// <typeparam name="T">The type of the elements of the input sequences</typeparam>
/// <param name="first">A collection whose elements that are not matched in second will be returned.</param>
/// <param name="second">A collection whose elements that match in the first sequence will cause those elements to be removed from the returned sequence.</param>
/// <param name="keyExtractor">The key extractor that return the key to be matched.</param>
/// <returns>A sequence that contains the set difference of the elements of two sequences</returns>
public static IEnumerable<T> Difference<T, TKey>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, TKey> keyExtractor)
{
return Except(first, second, keyExtractor);
}
}
/// <summary>
/// Generic implementation of IEqualityComparer<T> that enables wrapping delegates in IEqualityComparer
/// Based on http://blog.lavablast.com/post/2010/05/05/Lambda-IEqualityComparer3cT3e.aspx
/// </summary>
public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
private readonly Func<T, TKey> _keyExtractor;
/// <summary>
/// Initializes a new instance of the <see cref="KeyEqualityComparer&lt;T&gt;"/> class.
/// Allows us to simply specify the key to compare with: x => x.ID
/// </summary>
/// <param name="keyExtractor">The key extractor.</param>
public KeyEqualityComparer(Func<T, TKey> keyExtractor)
{
_keyExtractor = keyExtractor;
}
/// <summary>
/// Determines whether the specified objects have equal keys.
/// </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>
public bool Equals(T x, T y)
{
return _keyExtractor(x).Equals(_keyExtractor(y));
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <param name="obj">The object.</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>
public int GetHashCode(T obj)
{
if (_keyExtractor == null)
return obj.GetHashCode();
else
{
var val = _keyExtractor(obj);
return val.GetHashCode();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment