如何查找连续相同的值项目作为Linq组

时间:2018-05-11 20:54:40

标签: c# linq

var schedules = new List<Item>{
    new Item { Id=1, Name = "S" },
    new Item { Id=2, Name = "P" },
    new Item { Id=3, Name = "X" },
    new Item { Id=4, Name = "X" },
    new Item { Id=5, Name = "P" },
    new Item { Id=6, Name = "P" },
    new Item { Id=7, Name = "P" },
    new Item { Id=8, Name = "S" }
};

我想在新列表中选择相同的值和相同的订单,如下所示:

var groupedAndSelectedList = new List<List<Item>>{
    new List<Item> {       
        new Item { Id=3, Name = "X" },
        new Item { Id=4, Name = "X" },
    },
    new List<Item> {       
        new Item { Id=5, Name = "P" },
        new Item { Id=6, Name = "P" },
        new Item { Id=7, Name = "P" },
    }
}

如果项目像new Item { Id=3, Name = "A" }那样是单身,我不需要它。

分组选择列表中的所有X或P元素。但我希望如果物品位于另一个物品之后或之前。

使用linq可以吗?

4 个答案:

答案 0 :(得分:6)

您在这里寻找的是GroupWhile<T>方法。

向用户L.B表示解决方案。去给他的原始答案UpDoot https://stackoverflow.com/a/20469961/30155

    var schedules = new List<Item>{
        new Item { Id=1, Name = "S" },
        new Item { Id=2, Name = "P" },
        new Item { Id=3, Name = "X" },
        new Item { Id=4, Name = "X" },
        new Item { Id=5, Name = "P" },
        new Item { Id=6, Name = "P" },
        new Item { Id=7, Name = "P" },
        new Item { Id=8, Name = "S" }
    };

    var results = schedules
        .GroupWhile((preceding, next) => preceding.Name == next.Name) 
        //Group items, while the next is equal to the preceding one
        .Where(s => s.Count() > 1)
        //Only include results where the generated sublist have more than 1 element.
        .ToList();

    foreach (var sublist in results)
    {
        foreach (Item i in sublist)
        {
            Console.WriteLine($"{i.Name} - {i.Id}");
        }
        Console.WriteLine("");
    }

    Console.ReadLine();

您可以将实现作为扩展方法添加到所有IEnumerable<T>,如此。

public static class Extensions
{
    public static IEnumerable<IEnumerable<T>> GroupWhile<T>(this IEnumerable<T> seq, Func<T, T, bool> condition)
    {
        T prev = seq.First();
        List<T> list = new List<T>() { prev };

        foreach (T item in seq.Skip(1))
        {
            if (condition(prev, item) == false)
            {
                yield return list;
                list = new List<T>();
            }
            list.Add(item);
            prev = item;
        }

        yield return list;
    }
}

答案 1 :(得分:1)

您可以通过维护到目前为止找到的项目数来实现。这有助于您找到连续的项目,因为count(name) - index的值对它们来说是不变的:

IDictionary<string,int> count = new Dictionary<string,int>();
var groups = schedules
    .Select((s, i) => new {
        Item = s
    ,   Index = i
    })
    .GroupBy(p => {
        var name = p.Item.Name;
        int current;
        if (!count.TryGetValue(name, out current)) {
            current = 0;
            count.Add(name, current);
        }
        count[name] = current + 1;
        return new { Name = name, Order = current - p.Index };
    })
    .Select(g => g.ToList())
    .Where(g => g.Count > 1)
    .ToList();

这会为您的示例生成所需的输出:

{ Item = Id=3 Name=X, Index = 2 }
{ Item = Id=4 Name=X, Index = 3 }
-----
{ Item = Id=5 Name=P, Index = 4 }
{ Item = Id=6 Name=P, Index = 5 }
{ Item = Id=7 Name=P, Index = 6 }

Demo.

注意:如果Order = current - p.Index表达式看起来有点像#34; magic&#34;,请考虑删除最终的SelectWhere子句,以及枚举组密钥。

答案 2 :(得分:0)

@dasblinkenlight提供了一个只使用LINQ的答案。使用纯粹存在的LINQ方法的任何答案可能都很丑,可能表现不佳,并且可能不是高度可重用的。 (这不是对这个答案的批评。这是对LINQ的批评。)

@ eoin-campbell提供了一个使用自定义LINQ方法的答案。但是,我认为可以改进它以更接近地匹配现有LINQ GroupBy函数的功能,例如自定义比较器(当您需要执行诸如对键的不区分大小写的比较之类的事情时)。下面的Partition方法看起来和感觉类似于GroupBy函数,但符合连续项的要求。

您可以通过执行以下操作来使用此方法来实现目标。请注意,如果您没有提交要求,它看起来与您编写此文件的方式完全相同,但它使用的是Partition而不是GroupBy

var partitionsWithMoreThan1 = schedules.Partition(o => o.Name)
                                       .Where(p => p.Count() > 1)
                                       .Select(p => p.ToList())
                                       .ToList();

以下是方法:

static class EnumerableExtensions
{
    /// <summary>
    /// Partitions the elements of a sequence into smaller collections according to a specified
    /// key selector function, optionally comparing the keys by using a specified comparer.
    /// Unlike GroupBy, this method does not produce a single collection for each key value.
    /// Instead, this method produces a collection for each consecutive set of matching keys.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
    /// <param name="source">An <see cref="IEnumerable{T}"/> whose elements to partition.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <param name="comparer">An <see cref="IEqualityComparer{T}"/> to compare keys.</param>
    /// <returns>
    /// An <b>IEnumerable{IGrouping{TKey, TSource}}</b> in C#
    /// or <b>IEnumerable(Of IGrouping(Of TKey, TSource))</b> in Visual Basic
    /// where each <see cref="IGrouping{TKey,TElement}"/> object contains a collection of objects and a key.
    /// </returns>
    public static IEnumerable<IGrouping<TKey, TSource>> Partition<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null)
    {
        if (comparer == null)
            comparer = EqualityComparer<TKey>.Default;
        using (var enumerator = source.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                var item = enumerator.Current;
                var partitionKey = keySelector(item);
                var itemsInPartition = new List<TSource> {item};
                var lastPartitionKey = partitionKey;
                while (enumerator.MoveNext())
                {
                    item = enumerator.Current;
                    partitionKey = keySelector(item);
                    if (comparer.Equals(partitionKey, lastPartitionKey))
                    {
                        itemsInPartition.Add(item);
                    }
                    else
                    {
                        yield return new Grouping<TKey, TSource>(lastPartitionKey, itemsInPartition);
                        itemsInPartition = new List<TSource> {item};
                        lastPartitionKey = partitionKey;
                    }
                }
                yield return new Grouping<TKey, TSource>(lastPartitionKey, itemsInPartition);
            }
        }
    }

    // it's a shame there's no ready-made public implementation that will do this
    private class Grouping<TKey, TSource> : IGrouping<TKey, TSource>
    {
        public Grouping(TKey key, List<TSource> items)
        {
            _items = items;
            Key = key;
        }

        public TKey Key { get; }

        public IEnumerator<TSource> GetEnumerator()
        {
            return _items.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _items.GetEnumerator();
        }

        private readonly List<TSource> _items;
    }
}

答案 3 :(得分:-1)

根据评论澄清(问题真的现在还不清楚),我认为这就是所需要的。

它使用一种扩展方法,通过测试基于GroupByRuns的连续项,将基于GroupByWhile组的基于ScanPair组合在一起,Scan我的APL启发Aggregate运算符的变体,类似ValueTuple,但返回中间结果,并使用(Key, Value) public static IEnumerable<IGrouping<int, TRes>> GroupByRuns<T, TKey, TRes>(this IEnumerable<T> src, Func<T,TKey> keySelector, Func<T,TRes> resultSelector, IEqualityComparer<TKey> cmp = null) { cmp = cmp ?? EqualityComparer<TKey>.Default; return src.GroupByWhile((prev, cur) => cmp.Equals(keySelector(prev), keySelector(cur)), resultSelector); } public static IEnumerable<IGrouping<int, T>> GroupByRuns<T, TKey>(this IEnumerable<T> src, Func<T,TKey> keySelector) => src.GroupByRuns(keySelector, e => e); public static IEnumerable<IGrouping<int, T>> GroupByRuns<T>(this IEnumerable<T> src) => src.GroupByRuns(e => e, e => e); public static IEnumerable<IGrouping<int, TRes>> GroupByWhile<T, TRes>(this IEnumerable<T> src, Func<T,T,bool> testFn, Func<T,TRes> resultFn) => src.ScanPair(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1) .GroupBy(kvp => kvp.Key, kvp => resultFn(kvp.Value)); public static IEnumerable<(TKey Key, T Value)> ScanPair<T, TKey>(this IEnumerable<T> src, TKey seedKey, Func<(TKey Key, T Value),T,TKey> combineFn) { using (var srce = src.GetEnumerator()) { if (srce.MoveNext()) { var prevkv = (seedKey, srce.Current); while (srce.MoveNext()) { yield return prevkv; prevkv = (combineFn(prevkv, srce.Current), srce.Current); } yield return prevkv; } } } 将键与值配对。

ScanPair

我意识到这是很多扩展代码,但是通过使用通用GroupBySequential库,您可以构建其他专门的分组方法,例如GroupByRuns

现在只需Name List并选择包含多个成员的游戏,然后将每次投放转换为List,将整个内容转换为var ans = schedules.GroupByRuns(s => s.Name) .Where(sg => sg.Count() > 1) .Select(sg => sg.ToList()) .ToList();

Count() > 1

注意:对于@Aominè,在Take(2).Count()个子组(内部类型)之后,使用Skip(1).Any()或@MichaelGunter使用GroupBy优化Grouping的人很有意思IList)每个工具Count()Grouping.count方法直接从treemapify字段获取计数。

相关问题