Created
November 6, 2013 19:25
-
-
Save HakanL/7342550 to your computer and use it in GitHub Desktop.
Class that handles merging of two lists, returning the difference as InsertList, DeleteList, MatchKeyList and ModifiedList. Also handles comparison of two different types of lists, and composite keys
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
namespace Haukcode | |
{ | |
/// <summary> | |
/// Main interface for merger that requires you to set the KeySelector. Supports two different types | |
/// (with comparable key). Supports composite-key | |
/// </summary> | |
/// <typeparam name="T1">Type of ListA for comparison</typeparam> | |
/// <typeparam name="T2">Type of ListB for comparison</typeparam> | |
public interface IMerger<T1, T2> | |
{ | |
/// <summary> | |
/// Specify the function that will be used to generate a key for each of the two types in the merger. | |
/// They should return a type that supports both GetHashCode and Equals. The best option is to return a Tuple | |
/// which supports both of these methods (i.e. Tuple.Create(id1, id2)). | |
/// </summary> | |
/// <param name="keySelectorA">Function to generate a key for type A (existing)</param> | |
/// <param name="keySelectorB">Function to generate a key for type B (new)</param> | |
/// <returns>IComparable type, recommended is Tuple which handles composite keys as well as single keys</returns> | |
IMergerResult<T1, T2> SetKeySelector(Func<T1, IComparable> keySelectorA, Func<T2, IComparable> keySelectorB); | |
} | |
/// <summary> | |
/// Main interface for merger between two lists of the same type. | |
/// </summary> | |
/// <typeparam name="T">Class for comparison</typeparam> | |
public interface IMerger<T> | |
{ | |
/// <summary> | |
/// Specify the function that will be used to generate a key for each item in the lists | |
/// It should return a type that supports both GetHashCode and Equals. The best option is to return a Tuple | |
/// which supports both of these methods (i.e. Tuple.Create(id1, id2)). | |
/// </summary> | |
/// <param name="keySelector">Function to generate a key</param> | |
/// <returns>IComparable type, recommended is Tuple which handles composite keys as well as single keys</returns> | |
IMergerResult<T, T> SetKeySelector(Func<T, IComparable> keySelector); | |
} | |
/// <summary> | |
/// Result that provides three different lists: | |
/// DeleteList (items that don't exist in newList), | |
/// InsertList (items that don't exist in existingList), | |
/// MatchKeyList (items that match on key. Note that they may still have different non-key properties, see SetModifiedComparer) | |
/// </summary> | |
/// <typeparam name="T1">Type of ListA for comparison</typeparam> | |
/// <typeparam name="T2">Type of ListB for comparison</typeparam> | |
public interface IMergerResult<T1, T2> | |
{ | |
/// <summary> | |
/// List of items from listA that are missing from listB | |
/// </summary> | |
List<T1> DeleteList { get; } | |
/// <summary> | |
/// List of items from listB that are missing from listA | |
/// </summary> | |
List<T2> InsertList { get; } | |
/// <summary> | |
/// List of items that have a matching key (but not necessarily matching non-key properties, see SetModifiedComparer) | |
/// </summary> | |
List<Merger<T1, T2>.Objects> MatchKeyList { get; } | |
/// <summary> | |
/// Returns true if there are any mismatches in the merged lists (any items in DeleteList, InsertList or missing from MatchKeyList | |
/// </summary> | |
bool IsDirty { get; } | |
/// <summary> | |
/// Set function to be used to compare properties that aren't part of the key (called when two objects have a matching key). | |
/// If the function returns false then the objects are added to the ModifiedList. | |
/// </summary> | |
/// <param name="isModifiedComparer">Function that compares two objects for matching non-key properties</param> | |
/// <returns>Result that returns a ModifiedList</returns> | |
IMergerResultModified<T1, T2> SetModifiedComparer(Func<T1, T2, bool> isModifiedComparer); | |
} | |
/// <summary> | |
/// Result that also returns objects that are modified (based on the SetModifiedComparer function in IMergerResult) | |
/// </summary> | |
/// <typeparam name="T1">Type of ListA for comparison</typeparam> | |
/// <typeparam name="T2">Type of ListB for comparison</typeparam> | |
public interface IMergerResultModified<T1, T2> : IMergerResult<T1, T2> | |
{ | |
/// <summary> | |
/// List of objects that are modified (SetModifiedComparer function returned false). Both the A and B objects are returned in this list | |
/// </summary> | |
List<Merger<T1, T2>.Objects> ModifiedList { get; } | |
} | |
/// <summary> | |
/// Implementation of the merger interface supporting two different types of lists | |
/// </summary> | |
/// <typeparam name="T1">Type of ListA for comparison</typeparam> | |
/// <typeparam name="T2">Type of ListB for comparison</typeparam> | |
public class Merger<T1, T2> : IMerger<T1, T2>, IMergerResultModified<T1, T2> | |
{ | |
/// <summary> | |
/// Holder for both objects, used by MatchKey and Modified lists | |
/// </summary> | |
public class Objects | |
{ | |
public T1 A { get; private set; } | |
public T2 B { get; private set; } | |
public Objects(T1 a, T2 b) | |
{ | |
this.A = a; | |
this.B = b; | |
} | |
} | |
private bool mergeExecuted; | |
private IEnumerable<T1> listA; | |
private IEnumerable<T2> listB; | |
private List<T1> deleteList; | |
private List<T2> insertList; | |
private List<Objects> matchKeyList; | |
private List<Objects> modifiedList; | |
private bool isDirty; | |
private Func<T1, IComparable> keySelectorA; | |
private Func<T2, IComparable> keySelectorB; | |
private Func<T1, T2, bool> isModifiedComparer; | |
protected Merger(IEnumerable<T1> listA, IEnumerable<T2> listB) | |
{ | |
this.listA = listA; | |
this.listB = listB; | |
this.mergeExecuted = false; | |
} | |
public static IMerger<T1, T2> Create(IEnumerable<T1> listA, IEnumerable<T2> listB) | |
{ | |
return new Merger<T1, T2>(listA, listB); | |
} | |
public IMergerResult<T1, T2> SetKeySelector(Func<T1, IComparable> keySelectorA, Func<T2, IComparable> keySelectorB) | |
{ | |
this.keySelectorA = keySelectorA; | |
this.keySelectorB = keySelectorB; | |
return this; | |
} | |
public IMergerResultModified<T1, T2> SetModifiedComparer(Func<T1, T2, bool> isModifiedComparer) | |
{ | |
this.isModifiedComparer = isModifiedComparer; | |
return this; | |
} | |
private void ExecuteMerge() | |
{ | |
if (this.mergeExecuted) | |
return; | |
this.insertList = new List<T2>(); | |
this.deleteList = new List<T1>(); | |
this.matchKeyList = new List<Objects>(); | |
this.isDirty = false; | |
if (isModifiedComparer != null) | |
this.modifiedList = new List<Objects>(); | |
var usedListB = new HashSet<IComparable>(); | |
var listAKeys = this.listA.ToDictionary(this.keySelectorA); | |
foreach (var newItem in this.listB) | |
{ | |
var key = keySelectorB(newItem); | |
T1 existingItem; | |
if (listAKeys.TryGetValue(key, out existingItem)) | |
{ | |
// Match | |
this.matchKeyList.Add(new Objects(existingItem, newItem)); | |
if (isModifiedComparer != null) | |
{ | |
// Compare | |
if (!isModifiedComparer(existingItem, newItem)) | |
{ | |
this.modifiedList.Add(new Objects(existingItem, newItem)); | |
this.isDirty = true; | |
} | |
} | |
usedListB.Add(key); | |
} | |
else | |
{ | |
this.insertList.Add(newItem); | |
this.isDirty = true; | |
} | |
} | |
// Now add the items that didn't match to the deleteList | |
foreach (var itemA in listA) | |
{ | |
// Call the selector to generate a key for the item. If it hasn't been used (usedListB) then it's added to the DeleteList | |
var key = keySelectorA(itemA); | |
if (!usedListB.Contains(key)) | |
{ | |
this.deleteList.Add(itemA); | |
this.isDirty = true; | |
} | |
} | |
// We only want to execute the merge once | |
this.mergeExecuted = true; | |
} | |
public List<T1> DeleteList | |
{ | |
get | |
{ | |
ExecuteMerge(); | |
return this.deleteList; | |
} | |
} | |
public List<T2> InsertList | |
{ | |
get | |
{ | |
ExecuteMerge(); | |
return this.insertList; | |
} | |
} | |
public List<Objects> MatchKeyList | |
{ | |
get | |
{ | |
ExecuteMerge(); | |
return this.matchKeyList; | |
} | |
} | |
public List<Objects> ModifiedList | |
{ | |
get | |
{ | |
if (this.modifiedList == null) | |
return null; | |
ExecuteMerge(); | |
return this.modifiedList; | |
} | |
} | |
public bool IsDirty | |
{ | |
get | |
{ | |
ExecuteMerge(); | |
return this.isDirty; | |
} | |
} | |
} | |
/// <summary> | |
/// Inherited class that streamlines the interface when the two lists are of the same type | |
/// </summary> | |
/// <typeparam name="T">Type of the items in the list</typeparam> | |
public class Merger<T> : Merger<T, T>, IMerger<T> | |
{ | |
protected Merger(IEnumerable<T> listA, IEnumerable<T> listB) | |
: base(listA, listB) | |
{ | |
} | |
public static new IMerger<T> Create(IEnumerable<T> listA, IEnumerable<T> listB) | |
{ | |
return new Merger<T>(listA, listB); | |
} | |
public IMergerResult<T, T> SetKeySelector(Func<T, IComparable> keySelector) | |
{ | |
base.SetKeySelector(keySelector, keySelector); | |
return this; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment