2012-09-04 38 views
6

Ya he pensado en cómo voy a resolver esto lanzando mi propia solución, pero me pregunto si .NET ya tiene la funcionalidad para lo que Estoy tratando de lograr, si es así, prefiero usar algo incorporado.Combinar dos listas en C# y fusionar objetos con el mismo ID en un elemento de lista

Supongamos que tengo dos instancias de un objeto Widget, llamémoslos PartA y PartB. La información de cada uno ha sido obtenida de dos servicios web diferentes, pero ambos tienen ID coincidentes.

PartA 
{ 
    ID: 19, 
    name: "Percy", 
    taste: "", 
    colour: "Blue", 
    shape: "", 
    same_same: "but different" 
} 

PartB 
{ 
    ID: 19, 
    name: "", 
    taste: "Sweet", 
    colour: "", 
    shape: "Hexagon", 
    same_same: "but not the same" 
} 

que desea combinar estos para crear la siguiente:

Result 
{ 
    ID: 19, 
    name: "Percy", 
    taste: "Sweet", 
    colour: "Blue", 
    shape: "Hexagon", 
    same_same: "but different" 
} 

Aviso cómo el valor de same_same diferencia entre cada uno, pero consideramos que la Parte A del maestro, por lo que el resultado conserva el valor but different.

Ahora Para complicar las cosas:

Supongamos que tenemos dos listas:

List<Widget> PartA = getPartA(); 
List<Widget> PartB = getPartB(); 

Ahora aquí hay algo de pseudocódigo que describe lo que quiero hacer:

List<Widget> Result = PartA.MergeWith(PartB).MergeObjectsOn(Widget.ID).toList(); 
+0

Simplemente una manera no inteligente de hacerlo. Completamente manual: recorra cada lista y cree una nueva lista de objetos combinados, que también he fusionado manualmente (es decir, con codificación fija). Hace el trabajo, pero realmente apesta y será molesto de mantener. Estoy buscando una mejor manera. –

Respuesta

13

Se puede escribir su propia extensión método (s), algo como esto:

static class Extensions 
{ 
    public static IEnumerable<T> MergeWith<T>(this IEnumerable<T> source, IEnumerable<T> other) where T : ICanMerge 
    { 
     var otherItems = other.ToDictionary(x => x.Key); 
     foreach (var item in source) 
     { 
      yield return (T)item.MergeWith(otherItems[item.Key]); 
     } 
    } 
    public static string AsNullIfEmpty(this string s) 
    { 
     if (string.IsNullOrEmpty(s)) 
      return null; 
     else 
      return s; 
    } 
} 

Dónde ICanMerge es como:

public interface ICanMerge 
{ 
    object Key { get; } 
    ICanMerge MergeWith(ICanMerge other); 
} 

Implementado por ejemplo como:

public class Widget : ICanMerge 
{ 
    object ICanMerge.Key { get { return this.ID; } } 
    int ID {get;set;} 
    string taste {get;set;} 
    public ICanMerge MergeWith(ICanMerge other) 
    { 
     var merged = new Widget(); 
     var otherWidget = (Widget)other; 
     merged.taste = this.taste.AsNullIfEmpty() ?? otherWidget.taste; 
     //... 
     return merged; 
    } 
} 

Entonces es tan simple como PartA.MergeWith(PartB).ToList().

+0

¡Cómo hacer un esfuerzo adicional, Tim! Esperaba un punto en la dirección correcta, pero parece que me proporcionó una solución (y mucho mejor de lo que estoy usando actualmente). Muchas gracias :).Voy a probar esto y contactarte con un "aceptar". –

+0

Cuando probé esto, solo me permite hacer MergeWith contra el objeto (por ejemplo, Widget), no contra toda la lista, lo que significaría hacer un foreach y hacer la fusión para cada elemento (después de encontrar una coincidencia para cada uno). Estoy bastante seguro de que no se debe usar, pero ¿por qué Merge no tiene una de mis opciones como extensión para las listas de IEnumerable? – mppowe

+0

@mppowe ¿Lo cambiaste a 'this T source' en lugar de' this IEnumerable source'? Es lo único que se me ocurre que explicaría lo que describes. –

2

Si sus listas son una para una (es decir, el mismo número de elementos y cada elemento de la lista PartA tiene una coincidencia en la lista PartB), entonces consideraría el método de extensión Zip. Tenga en cuenta que Zip realmente no requiere que cada lista tenga la misma cantidad de elementos. Sin embargo, si no puede confiar en los elementos de "emparejamiento" con ID coincidentes, entonces mi enfoque simplista no funcionará.

se podría hacer algo como esto:

var alist = GetPartAWidgets().OrderBy(w => w.ID); 
var blist = GetPartBWidgets().OrderBy(w => w.ID); 
var merged = alist.Zip(blist, (a,b) => new Widget() 
      { 
       ID = a.ID, 
       Name = string.IsNullOrEmpty(a.Name) ? b.Name : a.Name, 
       //etc. 
      }); 

Si desea que su LINQ al aspecto más limpio, que podría encapsular el widget del individuo fusión de la lógica en un método de la función o la extensión y el uso que en lugar del delegado en línea.

0
public interface IMerge<out T> 
{ 
    IEnumerable<IMergeMatched<T>> Matched(); 

    IEnumerable<IMergeMatched<T>> Matched(Func<T, T, bool> predicate); 

    IEnumerable<T> NotMatchedBySource(); 

    IEnumerable<T> NotMatchedBySource(Func<T, bool> predicate); 

    IEnumerable<T> NotMatchedByTarget(); 

    IEnumerable<T> NotMatchedByTarget(Func<T, bool> predicate); 
} 

public interface IMergeMatched<out T> 
{ 
    T Source { get; } 

    T Target { get; } 
} 

public static class Enumerable 
{ 
    public static IMerge<TSource> Merge<TSource>(this IEnumerable<TSource> source, IEnumerable<TSource> target, 
              Func<TSource, TSource, bool> predicate) 
    { 
     return new Merge<TSource>(source, target, predicate); 
    } 
} 

public class Merge<T> : IMerge<T> 
{ 
    private readonly Func<T, T, bool> _predicate; 
    private readonly IEnumerable<T> _source; 
    private readonly IEnumerable<T> _target; 
    private IEnumerable<IMergeMatched<T>> _matcheds; 
    private IEnumerable<T> _notMatchedBySource; 
    private IEnumerable<T> _notMatchedByTarget; 

    public Merge(IEnumerable<T> source, IEnumerable<T> taget, Func<T, T, bool> predicate) 
    { 
     _source = source; 
     _target = taget; 
     _predicate = predicate; 
    } 

    public IEnumerable<IMergeMatched<T>> Matched() 
    { 
     if (_matcheds == null) 
     { 
      Analize(); 
     } 
     return _matcheds; 
    } 

    public IEnumerable<IMergeMatched<T>> Matched(Func<T, T, bool> predicate) 
    { 
     return Matched() 
      .Where(t => predicate.Invoke(t.Source, t.Target)) 
      .ToArray(); 
    } 

    public IEnumerable<T> NotMatchedBySource() 
    { 
     if (_notMatchedBySource == null) 
     { 
      Analize(); 
     } 
     return _notMatchedBySource; 
    } 

    public IEnumerable<T> NotMatchedBySource(Func<T, bool> predicate) 
    { 
     return NotMatchedBySource() 
      .Where(predicate) 
      .ToArray(); 
    } 

    public IEnumerable<T> NotMatchedByTarget() 
    { 
     if (_notMatchedByTarget == null) 
     { 
      Analize(); 
     } 
     return _notMatchedByTarget; 
    } 

    public IEnumerable<T> NotMatchedByTarget(Func<T, bool> predicate) 
    { 
     return NotMatchedByTarget() 
      .Where(predicate) 
      .ToArray(); 
    } 

    private void Analize() 
    { 
     var macheds = new List<MergeMached<T>>(); 
     var notMachedBySource = new List<T>(_source); 
     var notMachedByTarget = new List<T>(_target); 

     foreach (var source in _source) 
     { 
      foreach (var target in _target) 
      { 
       var macth = _predicate.Invoke(source, target); 
       if (!macth) continue; 

       macheds.Add(new MergeMached<T>(source, target)); 
       notMachedBySource.Remove(source); 
       notMachedByTarget.Remove(target); 
      } 
     } 

     _matcheds = macheds.ToArray(); 
     _notMatchedBySource = notMachedBySource.ToArray(); 
     _notMatchedByTarget = notMachedByTarget.ToArray(); 
    } 
} 

public class MergeMached<T> : IMergeMatched<T> 
{ 
    public MergeMached(T source, T target) 
    { 
     Source = source; 
     Target = target; 
    } 

    public T Source { get; private set; } 

    public T Target { get; private set; } 
} 

¿Cómo usar?

var source = new List<MediaFolder> 
      { 
       new MediaFolder 
        { 
         Id = "Id1", 
         Name = "Name1", 
         Path = "Path1" 
        }, 
       new MediaFolder 
        { 
         Id = "Id2", 
         Name = "Name2", 
         Path = "Path2" 
        }, 
       new MediaFolder 
        { 
         Id = "Id3", 
         Name = "Name3", 
         Path = "Path3" 
        }, 
       new MediaFolder 
        { 
         Id = "Id4", 
         Name = "Name4", 
         Path = "Path4" 
        }, 
       new MediaFolder 
        { 
         Id = "Id5", 
         Name = "Name5", 
         Path = "Path5" 
        }, 
       new MediaFolder 
        { 
         Id = "Id6", 
         Name = "Name6", 
         Path = "Path6" 
        } 
      }; 

     var target = new List<MediaFolder> 
      { 
       new MediaFolder 
        { 
         Id = "Id1", 
         Name = "Actualizado en el objeto", 
         Path = "Path1" 
        }, 
        //Id2 eliminado 
       new MediaFolder 
        { 
         Id = "Id3", 
         Name = "Name3", 
         Path = "Actualizado tambien" 
        }, 
       new MediaFolder 
        { 
         Id = "Id4", 
         Name = "Name4", 
         Path = "Path4" 
        }, 
       new MediaFolder 
        { 
         Id = "Id5", 
         Name = "Name5", 
         Path = "Path5" 
        }, 
       new MediaFolder 
        { 
         Id = "Id6", 
         Name = "Name6", 
         Path = "Path6" 
        }, 
        new MediaFolder 
        { 
         Id = "Id7", 
         Name = "Nuevo Item 7", 
         Path = "Nuevo Item 7" 
        } 
      }; 

     var merge = source.Merge(target, (x, y) => x.Id == y.Id); 

     var toUpdate = merge.Matched((x, y) => x.Name != y.Name | x.Path != y.Path) 
      .ToArray(); 

     var toDelete = merge.NotMatchedBySource(); 
     var toInsert = merge.NotMatchedByTarget(); 

     Assert.AreEqual(2, toUpdate.Count()); 
     Assert.IsTrue(toUpdate.Count(x => x.Source.Id == "Id1" & x.Target.Id == "Id1") > 0); 
     Assert.IsTrue(toUpdate.Count(x => x.Source.Id == "Id3" & x.Target.Id == "Id3") > 0); 

     Assert.AreEqual("Id7", toInsert.First().Id); 
     Assert.AreEqual("Id2", toDelete.First().Id); 
Cuestiones relacionadas