基于条件合并List<T>中的两个或多个T

4

我有以下的类:

public class FactoryOrder
    {
        public string Text { get; set; }
        public int OrderNo { get; set; }        
    }

并收集持有FactoryOrders列表的集合

List<FactoryOrder>()

这里是示例数据

FactoryOrder("Apple",20)
FactoryOrder("Orange",21)
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes",71)
FactoryOrder("mango",72)
FactoryOrder("Cherry",73)

我的要求是合并工厂订单的文本,其中订单号按顺序排列,并保留合并后的较低订单号 - 因此生成的输出将是:

   FactoryOrder("Apple Orange",20) //Merged Apple and Orange and retained Lower OrderNo 20
    FactoryOrder("WaterMelon",42)
    FactoryOrder("JackFruit",51)
    FactoryOrder("Grapes mango Cherry",71)//Merged Grapes,Mango,cherry and retained Lower OrderNo 71

我对Linq不熟悉,所以不知道该怎么做。如果有帮助或指点,将不胜感激。


4
如果你的逻辑非常依赖于连续的项目,LINQ可能不是最简单的方法。使用一个简单的循环。你可以先用LINQ将它们排序:orders.OrderBy(x => x.OrderNo) - Tim Schmelter
@TimSchmelter 我甚至认为没有“理智的”“纯粹的”LINQ方法来解决这个问题... - xanatos
1
@xanatos:这要看你所谓的“纯”的定义。在SO上有一些LINQ扩展方法可以按相邻项进行分组。但它们只是隐藏了许多复杂的代码,比如这个 - Tim Schmelter
@xanatos:但是即使这种实现也不关心属性的连续值,而是关心序列中具有相同属性值的连续项。所以你是对的,我不知道任何实现。 - Tim Schmelter
@TimSchmelter 显然,构建一个完整的程序并将其隐藏在LINQ后面并不是“纯粹”的 :-) 但这仍然是一个很好的解决方案。 - xanatos
5个回答

4

正如评论所述,如果您的逻辑如此严重地依赖于连续的项目,则LINQ不是最简单的方法。使用简单的循环。

您可以首先使用LINQ对其进行排序:orders.OrderBy(x => x.OrderNo )

var consecutiveOrdernoGroups = new List<List<FactoryOrder>> { new List<FactoryOrder>() };
FactoryOrder lastOrder = null;
foreach (FactoryOrder order in orders.OrderBy(o => o.OrderNo))
{
    if (lastOrder == null || lastOrder.OrderNo == order.OrderNo - 1)
        consecutiveOrdernoGroups.Last().Add(order);
    else
        consecutiveOrdernoGroups.Add(new List<FactoryOrder> { order });

    lastOrder = order;
}

现在,您只需使用连接的名称为每个组建立FactoryOrder列表。这就是LINQ和String.Join派上用场的地方:
orders = consecutiveOrdernoGroups
    .Select(list => new FactoryOrder 
    { 
        Text    = String.Join(" ", list.Select(o => o.Text)),
        OrderNo = list.First().OrderNo // is the minimum number
    })
    .ToList();

使用您的样本获得的结果:

输入图像描述


1

我不确定是否可以使用单个易懂的LINQ表达式完成此操作。可行的方法是进行简单的枚举:

    private static IEnumerable<FactoryOrder> Merge(IEnumerable<FactoryOrder> orders)
    {
        var enumerator = orders.OrderBy(x => x.OrderNo).GetEnumerator();

        FactoryOrder previousOrder = null;
        FactoryOrder mergedOrder = null;

        while (enumerator.MoveNext())
        {
            var current = enumerator.Current;

            if (mergedOrder == null)
            {
                mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
            }
            else
            {
                if (current.OrderNo == previousOrder.OrderNo + 1)
                {
                    mergedOrder.Text += current.Text;
                }
                else
                {
                    yield return mergedOrder;
                    mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
                }
            }

            previousOrder = current;
        }

        if (mergedOrder != null)
            yield return mergedOrder;
    }

这里假设FactoryOrder有一个接受文本和订单号的构造函数。

0

使用副作用实现的Linq:

var groupId = 0;
var previous = Int32.MinValue;
var grouped = GetItems()
    .OrderBy(x => x.OrderNo)
    .Select(x =>
    {
        var @group = x.OrderNo != previous + 1 ? (groupId = x.OrderNo) : groupId;
        previous = x.OrderNo;
        return new
                {
                    GroupId = group,
                    Item = x
                };
    })
    .GroupBy(x => x.GroupId)
    .Select(x => new FactoryOrder(
       String.Join(" ", x.Select(y => y.Item.Text).ToArray()), 
       x.Key))
    .ToArray();

foreach (var item in grouped)
{
    Console.WriteLine(item.Text + "\t" + item.OrderNo);
}

输出:

Apple Orange    20
WaterMelon  42
JackFruit   51
Grapes mango Cherry 71

或者,通过使用生成器扩展方法来消除副作用

public static class IEnumerableExtensions
{
    public static IEnumerable<IList<T>> MakeSets<T>(this IEnumerable<T> items, Func<T, T, bool> areInSameGroup)
    {
        var result = new List<T>();
        foreach (var item in items)
        {
            if (!result.Any() || areInSameGroup(result[result.Count - 1], item))
            {
                result.Add(item);
                continue;
            }
            yield return result;
            result = new List<T> { item };
        }
        if (result.Any())
        {
            yield return result;
        }
    }
}

而你的实现变成了

var grouped = GetItems()
    .OrderBy(x => x.OrderNo)
    .MakeSets((prev, next) => next.OrderNo == prev.OrderNo + 1)
    .Select(x => new FactoryOrder(
        String.Join(" ", x.Select(y => y.Text).ToArray()),
        x.First().OrderNo))
    .ToList();

foreach (var item in grouped)
{
    Console.WriteLine(item.Text + "\t" + item.OrderNo);
}

输出相同,但代码更易于理解和维护。


0

LINQ + 顺序处理 = Aggregate

虽然使用 Aggregate 不一定总是最佳选择。在 for(each) 循环中进行顺序处理通常会产生更易读的代码(请参见 Tim 的答案)。无论如何,这里提供了一个纯 LINQ 解决方案。

它遍历订单并首先将它们收集到一个字典中,该字典以连续订单的第一个 Id 作为 Key,以订单集合作为 Value。然后使用 string.Join 生成结果:

类:

class FactoryOrder
{
    public FactoryOrder(int id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
}

程序:

IEnumerable<FactoryOrder> orders =
    new[]
    {
        new FactoryOrder(20, "Apple"),
        new FactoryOrder(21, "Orange"),
        new FactoryOrder(22, "Pear"),
        new FactoryOrder(42, "WaterMelon"),
        new FactoryOrder(51, "JackFruit"),
        new FactoryOrder(71, "Grapes"),
        new FactoryOrder(72, "Mango"),
        new FactoryOrder(73, "Cherry"),
    };


var result = orders.OrderBy(t => t.Id).Aggregate(new Dictionary<int, List<FactoryOrder>>(),
    (dir, curr) =>
    {
        var prevId = dir.SelectMany(d => d.Value.Select(v => v.Id))
            .OrderBy(i => i).DefaultIfEmpty(-1)
            .LastOrDefault();
        var newKey = dir.Select(d => d.Key).OrderBy(i => i).LastOrDefault();
        if (prevId == -1 || curr.Id - prevId > 1)
        {
            newKey = curr.Id;
        }
        if (!dir.ContainsKey(newKey))
        {
            dir[newKey] = new List<FactoryOrder>();
        }
        dir[newKey].Add(curr);

        return dir;
    }, c => c)
    .Select(t => new
                 {
                     t.Key, 
                     Items = string.Join(" ", t.Value.Select(v => v.Name))
                 }).ToList();

正如您所看到的,这里发生的事情并不是非常直观,而且当有“许多”项时,很有可能表现不佳,因为正在增长的字典一遍又一遍地被访问。

也就是说:不要使用Aggregate。


0
刚写了一个方法,它很简洁并且在性能方面表现非常不错:
    static List<FactoryOrder> MergeValues(List<FactoryOrder> dirtyList)
    {            
        FactoryOrder[] temp1 = dirtyList.ToArray();
        int index = -1;
        for (int i = 1; i < temp1.Length; i++)
        {
            if (temp1[i].OrderNo - temp1[i - 1].OrderNo != 1) { index = -1; continue; }
            if(index == -1 ) index = dirtyList.IndexOf(temp1[i - 1]); 
            dirtyList[index].Text += " " + temp1[i].Text;                
            dirtyList.Remove(temp1[i]);
        }
        return dirtyList;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接