How to find consecutive same values items as a Linq group

前端 未结 4 1111
無奈伤痛
無奈伤痛 2021-01-29 09:23
var schedules = new List{
    new Item { Id=1, Name = \"S\" },
    new Item { Id=2, Name = \"P\" },
    new Item { Id=3, Name = \"X\" },
    new Item { Id=4,         


        
4条回答
  •  爱一瞬间的悲伤
    2021-01-29 10:08

    Based on the comment clarifications (the question is really unclear now), I think this is what is needed.

    It uses an extension method that groups runs of keys together, GroupByRuns, that is based on a GroupByWhile the groups by testing consecutive items, which is based on ScanPair, which is a variation of my APL inspired Scan operator that is like Aggregate, but returns intermediate results, and uses a ValueTuple (Key, Value) to pair keys with values along the way.

    public static IEnumerable> GroupByRuns(this IEnumerable src, Func keySelector, Func resultSelector, IEqualityComparer cmp = null) {
        cmp = cmp ?? EqualityComparer.Default;
        return src.GroupByWhile((prev, cur) => cmp.Equals(keySelector(prev), keySelector(cur)), resultSelector);
    }
    public static IEnumerable> GroupByRuns(this IEnumerable src, Func keySelector) => src.GroupByRuns(keySelector, e => e);
    public static IEnumerable> GroupByRuns(this IEnumerable src) => src.GroupByRuns(e => e, e => e);
    
    public static IEnumerable> GroupByWhile(this IEnumerable src, Func testFn, Func 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(this IEnumerable 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;
            }
        }
    }
    

    I realize this is a lot of extension code, but by using the general ScanPair base, you can build other specialized grouping methods, such as GroupBySequential.

    Now you just GroupByRuns of Name and select the runs with more than one member, then convert each run to a List and the whole thing to a List:

    var ans = schedules.GroupByRuns(s => s.Name)
                       .Where(sg => sg.Count() > 1)
                       .Select(sg => sg.ToList())
                       .ToList();
    

    NOTE: For @Aominè, who had an interesting take on optimizing Count() > 1 using Take(2).Count() or @MichaelGunter using Skip(1).Any(), after GroupBy the sub-groups (internal type Grouping) each implement IList and the Count() method just gets the count directly from the Grouping.count field.

提交回复
热议问题