如何基于属性合并两个对象列表并将重复项合并到新对象中

时间:2019-10-07 14:08:55

标签: c# list .net-core

我正在努力找到解决问题的简单方法: 我有两个对象列表,并且想要基于一个属性(序列)比较它们,并创建一个包含两个列表中的对象的新列表。如果对象仅在列表一中,我想将其标记为已删除(状态),如果对象仅在列表二中,则应将其标记为新(状态)。如果在两者中,我都希望将其标记为已更改(状态)并存储新旧值(金额/新金额)。

所以看起来像这样:

列出一:

[
    { 
        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吗? 谁能帮我吗?

谢谢!

编辑:因为问题出现在注释中。 如果项目都在两个列表中,则状态可以是“已更改”或“未更改”。对于实现而言,这并不重要,因为我以旧的和新的数量显示对象,并且只希望特别标记已删除和新的对象。状态为“不变”将是一个很好的参考。

4 个答案:

答案 0 :(得分: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。阅读评论以了解其工作原理:

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

答案 1 :(得分:0)

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

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
});

答案 2 :(得分:0)

这是可能的实现方式的示例

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;
   }

答案 3 :(得分:0)

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

由于您必须比较两个列表中每个对象的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