【问题标题】:How to merge two lists of objects based on properties and merge duplicates into new object如何根据属性合并两个对象列表并将重复项合并到新对象中
【发布时间】:2019-10-07 14:08:55
【问题描述】:

我正在努力寻找一个简单的解决方案来解决我的问题: 我有两个对象列表,想根据一个属性(串行)比较它们,并创建一个包含两个列表中的对象的新列表。如果对象仅在列表一中,我想将其标记为已删除(状态),如果它仅在列表二中,则应将其标记为新(状态)。如果它在两者中我想将其标记为已更改(状态)并存储旧值和新值(金额/新金额)。

所以它看起来像这样:

名单一:

[
    { 
        serial: 63245-8,
        amount:  10
    },
    { 
        serial: 08657-5,
        amount:  100
    }
    ,
    { 
        serial: 29995-0,
        amount:  500
    }
]

清单二:

[
    { 
        serial: 63245-8,
        amount:  100
    },
    { 
        serial: 67455-1,
        amount:  100
    }
    ,
    { 
        serial: 44187-10,
        amount:  50
    }
]

输出:

[
    { 
        serial: 63245-8,
        amount:  10,
        newAmount:  100
        status: "changed"
    },
    { 
        serial: 08657-5,
        amount:  100
        status: "deleted"
    },
    { 
        serial: 29995-0,
        amount:  500,
        status: "deleted"
    }
    { 
        serial: 67455-1,
        amount:  100
        status: "new"
    }
    ,
    { 
        serial: 44187-10,
        amount:  50
        status: "new"
    }
]

除了遍历两个列表并与另一个列表进行比较,构建三个不同的列表并将它们合并到最后,甚至最终对它们进行排序之外,我想不出任何好的解决方案。 我很确定有更好的解决方案,甚至可能使用 AutoMapper ? 谁能帮帮我?

谢谢!

编辑:因为问题出现在评论中。 如果项目在两个列表中,则状态可以是“已更改”或“未更改”。这对实现并不重要,因为我显示对象的新旧数量,只需要特别标记已删除和新对象。不过,状态“未更改”将是一个不错的选择,以供将来参考。

【问题讨论】:

  • 您是否至少为您的列表将保存的数据类型创建了任何类?
  • 看看IEnumerable.ExceptIEnumerable.Intersect。应该能够结合上述方法想出一个解决方案。
  • @Innat3 是的,我已经有存储在数据库中的实体,我围绕它构建了一个用于查询该数据的 webAPI。我只是不想在这里使问题复杂化并粘贴不必要的代码
  • @dymanoid 因为我的列表来自存储在我的数据库中的实体
  • 但是您的列表是 JSON,而不是 DTO。您应该选择考虑“这个问题是什么”而不是“这个问题包含什么”的标签。

标签: c# list .net-core


【解决方案1】:

这是列表的双向比较,可以通过使用Linq的IEnumerable.Except()IEnumerable.Intersect()来实现。

您应该做的第一件事是编写一个类来保存数据项:

sealed class Data
{
    public string Serial { get; }
    public int    Amount { get; }

    public Data(string serial, int amount)
    {
        Serial = serial;
        Amount = amount;
    }
}

接下来你需要写一个IEqualityComparer<T>,你可以用它来比较项目(你需要这个来使用Intersect()Except()

sealed class DataComparer : IEqualityComparer<Data>
{
    public bool Equals(Data x, Data y)
    {
        return x.Serial.Equals(y.Serial);
    }

    public int GetHashCode(Data obj)
    {
        return obj.Serial.GetHashCode();
    }
}

现在写一个类来接收比较数据:

enum ComparisonState
{
    Unchanged,
    Changed,
    New,
    Deleted
}

sealed class ComparedData
{
    public Data            Data            { get; }
    public int             PreviousAmount  { get; }
    public ComparisonState ComparisonState { get; }

    public ComparedData(Data data, ComparisonState comparisonState, int previousAmount)
    {
        Data            = data;
        ComparisonState = comparisonState;
        PreviousAmount  = previousAmount;
    }

    public override string ToString()
    {
        if (ComparisonState == ComparisonState.Changed)
            return $"Serial: {Data.Serial}, Amount: {PreviousAmount}, New amount: {Data.Amount}, Status: Changed";
        else
            return $"Serial: {Data.Serial}, Amount: {Data.Amount}, Status: {ComparisonState}";
    }
}

(为方便起见,我在该类中添加了ToString()。)

现在您可以按如下方式使用 Linq。阅读 cmets 以了解其工作原理:

class Program
{
    public static void Main()
    {
        var list1 = new List<Data>
        {
            new Data("63245-8",  10),
            new Data("08657-5", 100),
            new Data("29995-0", 500),
            new Data("12345-0",  42)
        };

        var list2 = new List<Data>
        {
            new Data("63245-8", 100),
            new Data("12345-0",  42),
            new Data("67455-1", 100),
            new Data("44187-10", 50),
        };

        var comparer = new DataComparer();

        var newItems     = list2.Except(list1, comparer);    // The second list without items from the first list = new items.
        var deletedItems = list1.Except(list2, comparer);    // The first list without items from the second list = deleted items.
        var keptItems    = list2.Intersect(list1, comparer); // Items in both lists = kept items (but note: Amount may have changed).

        List<ComparedData> result = new List<ComparedData>();

        result.AddRange(newItems    .Select(item => new ComparedData(item, ComparisonState.New,     0)));
        result.AddRange(deletedItems.Select(item => new ComparedData(item, ComparisonState.Deleted, 0)));

        // For each item in the kept list, determine if it changed by comparing it to the first list.
        // Note that the "list1.Find()` is an O(N) operation making this quite slow.
        // You could speed it up for large collections by putting list1 into a dictionary and looking items up in it -
        // but this is unlikely to be needed for smaller collections.

        result.AddRange(keptItems.Select(item =>
        {
            var previous = list1.Find(other => other.Serial == item.Serial);
            return new ComparedData(item, item.Amount == previous.Amount ? ComparisonState.Unchanged : ComparisonState.Changed, previous.Amount);
        }));

        // Print the result, for illustration.

        foreach (var item in result)
            Console.WriteLine(item);
    }
}

这个的输出如下:

Serial: 67455-1, Amount: 100, Status: New
Serial: 44187-10, Amount: 50, Status: New
Serial: 08657-5, Amount: 100, Status: Deleted
Serial: 29995-0, Amount: 500, Status: Deleted
Serial: 63245-8, Amount: 10, New amount: 100, Status: Changed
Serial: 12345-0, Amount: 42, Status: Unchanged

DotNet fiddle is here

【讨论】:

  • 我认为这以最优雅的方式完全解决了我的问题。在我的代码中测试后将此答案标记为解决方案。
【解决方案2】:

我建议您创建几个自定义类来简化代码理解

public class Item
{
    public string serial;
    public int? amount;
    public int? newAmount;
    public string status;
}

public class L1Item : Item
{       
    public L1Item(string s, int a)
    {
        serial = s;
        amount = a;
        status = "deleted";
    }
}

public class L2Item : Item
{
    public L2Item(string s, int a)
    {
        serial = s;
        amount = a;
        status = "new";
    }
}

然后使用您提供的输入,您可以创建两个单独的列表

List<Item> l1 = new List<Item>() { new L1Item("63245-8", 10), new L1Item("08657-5", 100), new L1Item("29995-0", 500) };
List<Item> l2 = new List<Item>() { new L2Item("63245-8", 100), new L2Item("67455-1", 100), new L2Item("44187-10", 50) };

然后您可以将它们连接成一个列表并按serial 分组

var groupedList = l1.Concat(l2).GroupBy(x => x.serial);

最后,对每个系列的所有项目进行分组,进行相应的更改并检索它们。

var output = groupedList.Select(g => new Item()
{
    serial = g.Key,
    amount = g.First().amount,
    newAmount = g.Count() > 1 ? g.Last().amount : null,
    status = g.Count() > 1 ? "changed" : g.First().status
});

【讨论】:

  • 哇,我不知道 GroupBy 会以这种形式给我一个列表。在查找 Enumerable.GroupBy 的文档后,我很确定这解决了我的一个问题
【解决方案3】:

这是一个可能的实现示例

public class Obj
{
    public string serial { get; set; }
    public int amount { get; set; }
    public int? newAmount { get; set; }
    public Status status { get; set; }
}

public enum Status
{
    undefined,
    changed,
    deleted,
    @new
}
static void Main(string[] args)
    {
        string listOneJson = @"[
                                { 
                                    serial: '63245-8',
                                    amount:  10
                                },
                                { 
                                    serial: '08657-5',
                                    amount:  100
                                }
                                ,
                                { 
                                    serial: '29995-0',
                                    amount:  500
                                }
                            ]";
        string listTwoJson = @"[
                                {
                                    serial: '63245-8',
                                    amount: 100
                                },
                                {
                                    serial: '67455-1',
                                    amount: 100
                                }
                                ,
                                {
                                    serial: '44187-10',
                                    amount: 50
                                }
                               ]";
        IList<Obj> listOne = JsonConvert.DeserializeObject<IList<Obj>>(listOneJson);
        IList<Obj> listTwo = JsonConvert.DeserializeObject<IList<Obj>>(listTwoJson);

        var result = merge(listOne, listTwo);
    }

 public static IEnumerable<Obj> merge(IList<Obj> listOne, IList<Obj> listTwo)
 {

        List<Obj> allElements = new List<Obj>();
        allElements.AddRange(listOne);
        allElements.AddRange(listTwo);

        IDictionary<string, int> dict1 = listOne.ToDictionary(x => x.serial, x => x.amount);
        IDictionary<string, int> dict2 = listTwo.ToDictionary(x => x.serial, x => x.amount);
        IDictionary<string, Obj> dictResults = new Dictionary<string, Obj>();

        foreach (var obj in allElements)
        {
            string serial = obj.serial;

            if (!dictResults.ContainsKey(serial))
            {
                bool inListOne = dict1.ContainsKey(serial);
                bool inListTwo = dict2.ContainsKey(obj.serial);

                Obj result = new Obj { serial = serial };

                if (inListOne && inListTwo) {
                    result.status = Status.changed;
                    result.amount = dict1[serial];
                    result.newAmount = dict2[serial];
                }
                else if (!inListOne && inListTwo)
                {
                    result.status = Status.@new;
                    result.amount = dict2[serial];
                }
                else if (inListOne && !inListTwo)
                {
                    result.status = Status.deleted;
                    result.amount = dict1[serial];
                }

                dictResults.Add(serial, result);
            }
        }
        return dictResults.Values;
   }

【讨论】:

    【解决方案4】:

    已经有一些很好的答案。我只是想添加一种方法来做到这一点,当数据集增加时不会增加时间复杂度。提醒一下,Linq 在幕后所做的只是 for 循环。

    由于您必须为两个列表中的每个对象比较 2 个不同的条件,不幸的是,您必须遍历它们中的每一个。但是有一种方法可以加快这个过程。

    假设您在 listOne 中有 n 个对象,在 listTwo 中有 m 个对象。

    您可以先分别循环遍历 listOne 和 listTwo 中的所有对象,并为每个列表创建一个 Dictionary。即 dictOne 和 dictTwo。这将分别需要 O(n) 和 O(m) 时间复杂度。

    然后,遍历 listOne 并检查项目是否存在于 dictTwo 中。接下来,遍历 listTwo 并检查项目是否存在于 dictOne 中。

    这样整体时间复杂度大约为 O(n+m)。

    数据模型:

    public class InputData{
        public InputData(string serial, int amount){
            this.Serial = serial;
            this.Amount = amount;
        }
        public string Serial {get; set;}
        public int Amount{get;set;}
    }
    
    public class ResultData{
        public ResultData(string serial, int amount, int newAmount, string status){
            this.Serial = serial;
            this.Amount = amount;
            this.NewAmount = newAmount;
            this.Status = status;
        }
    
        public ResultData(string serial, int newAmount, string status){
            this.Serial = serial;
            this.NewAmount = newAmount;
            this.Status = status;
        }
        public string Serial {get; set;}
        public int Amount{get;set;}
        public int NewAmount{get;set;}
        public string Status {get;set;}
    }
    

    主要方法:

    public static void Main()
    {
        List<InputData> listOne = new List<InputData>
        {
            new InputData("63245-8", 10),
            new InputData("08657-5", 100),
            new InputData("29995-0", 500)
        };
    
        List<InputData> listTwo = new List<InputData>
        {
            new InputData("63245-8", 100),
            new InputData("67455-1", 100),
            new InputData("44187-10", 50)
        };
    
        Dictionary<string, InputData> dictOne = CreateDictionary(listOne);      
        Dictionary<string, InputData> dictTwo = CreateDictionary(listTwo);
    
        List<ResultData> result = new List<ResultData>();
    
        result.AddRange(ProcessData(listOne, dictTwo, "deleted"));
        result.AddRange(ProcessData(listTwo, dictOne, "new"));
    
        foreach(var item in result){
            Console.WriteLine($"Serial: {item.Serial}, Amount: {item.Amount}, Status: {item.Status}");
        }
    }
    

    结果:

    Serial: 63245-8, Amount: 100, Status: changed
    Serial: 08657-5, Amount: 100, Status: deleted
    Serial: 29995-0, Amount: 500, Status: deleted
    Serial: 63245-8, Amount: 10, Status: changed
    Serial: 67455-1, Amount: 100, Status: new
    Serial: 44187-10, Amount: 50, Status: new
    

    如果您想查看实际代码,这是我的Fiddle

    【讨论】:

      猜你喜欢
      • 2020-05-16
      • 2018-04-21
      • 1970-01-01
      • 2017-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多