Taoffi's blog

prisonniers du temps

Comparable sets

When we manipulate various collections of items, you often need to compare them.

Functionally speaking, comparing two collections (set1 and set2), in its simplest form, may mean:

  • Get the collection of items in set1 that are not in set2 (or vice versa)
  • Get the collection of identical items
  • Get the collection of different items.

 

In my case, I needed to obtain such compared collections in several projects (Xml files, Open Xml packages, swagger files, ms build project settings… I talked about in previous posts).

Several interfaces that come with .net may be of help in many contexts (think about IComparable, IEquitable… etc.). Though quite efficient for sorting and equality tests, they do not seem to be the right choice for solving this particular need for comparing collections.

 

For a collection to produce what we need (identical items, different items and items not in 'another' collection), its items must be able to provide answers to several basic questions.

For an item to tell if it is 'different' from or 'identical' to another, they must first be 'comparable'.

In the case of an Xml node for instance, we might consider that two nodes having the same name are comparable. They then can be different if their values are different. And can be identical if their values are identical.

Let us assume an object that provides such answers:

 

public interface IComparableItem
{
   bool IsComparable(IComparableItem other);
   bool IsDifferent(IComparableItem other);
   bool IsIdentical(IComparableItem other);
}

 

And a generic collection of such items

public class ComparableSet<T> : List<T> where T: IComparableItem

 

In such collection, we can then extract items matching our needs:

 

// items not in the other collection
public List<T> ItemsNotIn(ComparableSet<T> otherSet)

{
   List<T>      list   = new List<T>();

   if(otherSet == null)
      return list;

   foreach(var item in this)
   {
      // get an item of the other collection that is comparable to this item
      var comparable   = otherSet.FirstOrDefault(i => i.IsComparable(item));

      if(comparable == null)
         list.Add(item);
   }

   return list;
}

 

Sample implementation - The comparable item

 

public class ComparableSetItem : IComparableItem
{
   public string Name      { get; set; }
   public string Value      { get; set; }

   public bool IsIdentical(IComparableItem other)
   {
      ComparableSetItem   obj   = other == null ? null : other as ComparableSetItem;
      if(obj == null)
         return false;

      if(obj == this)
         return true;

      return obj.Name == this.Name && obj.Value == this.Value;
   }

   public bool IsDifferent(IComparableItem other)
   {
      ComparableSetItem   obj   = other == null ? null : other as ComparableSetItem;

      if(obj == null || obj == this)
         return false;

      return obj.Name == this.Name && obj.Value != this.Value;
   }

   public bool IsComparable(IComparableItem other)
   {
      ComparableSetItem   obj   = other == null ? null : other as ComparableSetItem;

      if(obj == null || obj == this)
         return false;

      return obj.Name == this.Name;
   }
}

 

The comparable generic collection

 

public class ComparableSet<T> : List<T> where T: IComparableItem
{
   // items not in the other collection
   public List<T> ItemsNotIn(ComparableSet<T> otherSet)
   {
      List<T>      list   = new List<T>();

      if(otherSet == null)
         return list;

      foreach(var item in this)
      {
         var comparable   = otherSet.FirstOrDefault(i => i.IsComparable(item));

         if(comparable == null)
            list.Add(item);
      }

      return list;
   }

 

 

 

   public List<T> IdenticalItems(ComparableSet<T> otherSet)
   {
      List<T>      list   = new List<T>();

      if(otherSet == null)
         return list;

      foreach(var item in this)
      {
         var identicals   = otherSet.Where(i => i.IsIdentical(item));

         if(identicals == null)
            continue;

         foreach(var itemx in identicals)
            list.Add(itemx);
      }

      return list;
   }

 

   public List<T> DifferentItems(ComparableSet<T> otherSet)
   {
      List<T>      list   = new List<T>();

      if(otherSet == null)
         return list;

      foreach(var item in this)
      {
         var      diffs   = otherSet.Where(i => i.IsComparable(item));

         if(diffs == null)
            continue;

         foreach(var otherItem in diffs)
         {

            if(item.IsDifferent(otherItem))
               list.Add(otherItem);
         }
      }

      return list;
   }
}

 

A simple console method to test this implementation

 

public static void TestComparableSets()
{
   
ComparableSet<ComparableSetItem>   list1   = new ComparableSet<ComparableSetItem>();
   
ComparableSet<ComparableSetItem>   list2   = new ComparableSet<ComparableSetItem>();

   list1.Add(new ComparableSetItem() { Name = "name1",    Value = "value1" });
   list1.Add(new ComparableSetItem() { Name = "name2",    Value = "value2" });
   list1.Add(new ComparableSetItem() { Name = "name3",    Value = "value3" });
   list1.Add(new ComparableSetItem() { Name = "name4",    Value = "value4" });
   list1.Add(new ComparableSetItem() { Name = "name5",    Value = "value5" });

   list2.Add(new ComparableSetItem() { Name = "name1",    Value = "value10" });
   list2.Add(new ComparableSetItem() { Name = "name2",    Value = "value2" });
   list2.Add(new ComparableSetItem() { Name = "name3",    Value = "value30" });
   list2.Add(new ComparableSetItem() { Name = "name30",   Value = "value300" });
   list2.Add(new ComparableSetItem() { Name = "name40",   Value = "value40" });
   list2.Add(new ComparableSetItem() { Name = "name5",    Value = "value5" });
   list2.Add(new ComparableSetItem() { Name = "name50",   Value = "value50" });
   list2.Add(new ComparableSetItem() { Name = "name6",    Value = "value60" });

   var      list1NotIn2 = list1.ItemsNotIn(list2);       // {n4/v4}
   var      list2NotIn1 = list2.ItemsNotIn(list1);       // {n30/v30},  {n40/v40},
                                                         // {n50/v50},  {n6/v60}
   var      list1Diff2  = list1.DifferentItems(list2);   // {n1/v10},   {n3/v30}
   var      list2Diff1  = list2.DifferentItems(list1);   // {n1/v1},    {n3/v3}
   var      ident12     = list1.IdenticalItems(list2);   // {n2/v2},    {n5/v5}
   var      ident21     = list2.IdenticalItems(list1);   // {n2/v2},    {n5/v5}
}

 

Using the strategy pattern

The above implementation assumes that you have control upon the collection items structures and are able to change their code.

If that is not the case (like, for instance, when using objects defined in a third party's library) you may use the strategy design pattern in a way similar to the IComparer interface.

 

public interface ICollectionItemComparer
{
   bool IsComparable(object obj1, object obj2);
   bool IsDifferent(object obj1, object obj2);
   bool IsIdentical(object obj1, object obj2);
}

 

The generic collection may then be defined in a way similar to the following:

 

public class ComparableSet<T> : List<T> where T: class
{
   // sample items not in the other collection using strategy pattern
   public List<T> ItemsNotIn(ComparableSet<T> otherSet, ICollectionItemComparer comparer)
   {
      List<T>      list   = new List<T>();

      if(otherSet == null)
         return list;

      foreach(var item in this)
      {
         var comparable   = otherSet.FirstOrDefault(i => comparer.IsComparable(item, i));

         if(comparable == null)
            list.Add(item);
      }

      return list;
   }