2

I have made a Linq query where I am grouping by calls and summing duration and everything is working according to expectations. However problem is, that I am getting too many results as an output. After grouping I would like to get lets say maximum 10 top results and combine rest to "rest" group. Is there some way of doing that using linq?

Here is an example of my current query:

  IEnumerable<DataModel> data = this.Data
    .GroupBy(g => #here is regex match#)
    .Select(s => new DataModel
    {
      CallerNumber = s.Key,
      Duration = TimeSpan.FromTicks(s.Sum(c => c.Duration.Ticks)),
    })
    .Where(w => !string.IsNullOrEmpty(w.CallerNumber))
    .OrderByDescending(o => o.Duration);

I can use .Take(10); in the end of query, but how to combine all the leftovers to "rest" part? I mean I will get top 10 records, but then there are more at position 11, 12, 13, 14 etc. that are now not displayed. How to get them combined and displayed as 11th item in the output list.

  IEnumerable<DataModel> data = this.Data
    .GroupBy(g => #here is regex match#)
    .Select(s => new DataModel
    {
      CallerNumber = s.Key,
      Duration = TimeSpan.FromTicks(s.Sum(c => c.Duration.Ticks)),
    })
    .Where(w => !string.IsNullOrEmpty(w.CallerNumber))
    .OrderByDescending(o => o.Duration)
    .Take(10);

EDIT:

The idea is that I have, lets say phone numbers:

050321        4:30
045345        2:00
050321        4:00
045345        6:00
076843        1:00
050321        1:00
032345        3:00
043453        2:00
032345        3:00

This is what I would like to achieve (lets say I want to get top 3 and the rest combined):

050321        9:30
045345        8:00
032345        6:00
Rest          3:00
10101
  • 2,232
  • 3
  • 26
  • 66
  • Didn't get the idea, could you please clarify what is the point of truncating the result and then combine it back? – E. Shcherbo Mar 13 '23 at 20:38
  • Oh, gotcha, you want them to appear as a single element – E. Shcherbo Mar 13 '23 at 20:39
  • 1. Perform a grouping into a variable; 2. Take 10 from that variable into `part1`; 3. Skip 10 from thay variable and group again into `rest`; 4. Combine `part1` with `rest` using `Concat` – E. Shcherbo Mar 13 '23 at 20:42
  • Instead of second grouping you can also use `Aggregate` because in fact you creating a single object from a collection – E. Shcherbo Mar 13 '23 at 20:47

1 Answers1

3

I suggest grouping twice, i.e.

int top = 10;

var data = this
  .Data
  .GroupBy(item => # here is regex match #)
  .Select(group => (key : group.Key, 
                    total : group.Sum(item => item.Duration.Ticks)))
  .OrderByDescending(pair => pair.total)
  .Select((pair, index) => (pair.key, pair.total, index))
  .GroupBy(rec => rec.index < top ? rec.key : "Rest",
           rec => rec.total)
  .Select(group => new DataModel() {
     CallerNumber = group.Key,
     Duration = TimeSpan.FromTicks(group.Sum(item => item))
   });

the idea is to order by descending and then group by all items with indexes 11, 12, .. under "Rest" key:

int top = 10;

...

.GroupBy(rec => rec.index < top ? rec.key : "Rest",
         rec => rec.total)

if rec has index small enough (0..9) its key is preserved, otherwise it's grouped under "Rest" key.

Fiddle

If you want to eliminate group with an empty key you can either filter it out:

var data = 
  ...
  .GroupBy(item => # here is regex match #)
  .Where(group => !string.IsNullOrEmpty(group)) 
  ...

Or put it into "Rest":

var data = 
  ...
  .Select(group => (key : group.Key, 
                    total : group.Sum(item => item.Duration.Ticks)))
  .OrderBy(pair => string.IsNullOrEmpty(pair.key) ? 1 : 0)
  .ThenByDescending(pair => pair.total)
  ...
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • Thank you for well described answer! It is almost what I wanted to achieve, however setting for example to `<3` is giving me top 2, one empty (that is a combined result of some values) and "Rest". Might be a problem with my code, I am trying to investigate now what might be the issue. – 10101 Mar 13 '23 at 21:20
  • 1
    @10101: I can't reproduce your problem with low top; please, fiddle yourself: https://dotnetfiddle.net/QqYzpq – Dmitry Bychenko Mar 13 '23 at 22:05
  • 1
    You may want to either remove group with empty `key` - `.Where(w => !string.IsNullOrEmpty(w.CallerNumber))` or put into `Rest` unconditionally - `.OrderBy(pair => string.IsNullOrEmpty(pair.key) ? 1 : 0).ThenByDescending(pair => pair.total)` – Dmitry Bychenko Mar 13 '23 at 22:15
  • I apologize for late comment. I got this working as expecting in the end. However I have one more additional question. I can also ask a new one as this is not part of my original scope. Just as a final adjustment I would like to order my items in way that `Rest` would be always the last one. Do you some hint how to achieve that? Currently I can order by descending or ascending, but this is placing `Rest` according to Duration value. Is there some trick to send it always to the end of my list of items? – 10101 Mar 28 '23 at 22:40
  • 1
    @10101: Add `.OrderBy(group => group.Key == "Rest" ? 2 : 1)` before the final `Select` or even `.OrderBy(group => group.Key == "Rest")` – Dmitry Bychenko Mar 28 '23 at 22:44
  • That was fast! Thank you a million! I will check your approach. I have been currently experimenting with `.OrderByDescending(o => o.CallerNumber == "Rest").ThenBy(t => t.Duration);` in the end of linq query – 10101 Mar 28 '23 at 22:46
  • 1
    @10101: note, that `false < true` (rule of thumb: `false` is `0`, when `true` is `1`), so `.OrderByDescending(o => o.CallerNumber == "Rest")` will move `Rest` on the *top*, not to the *bottom* as you want – Dmitry Bychenko Mar 28 '23 at 22:48