1

I'm trying to create a LINQ query which is a derivative of SelectMany. I have N items:

new {
   { Text = "Hello", Width = 2 },
   { Text = "Something else", Width = 1 },
   { Text = "Another", Width = 1 },
   { Text = "Extra-wide", Width = 3 },
   { Text = "Random", Width = 1 }
}

I would like the result to be a List<List<object>>(), where:

List<List<object>> = new {
   // first "row"
   {
      { Text = "Hello", Width = 2 },
      { Text = "Something else", Width = 1 },
      { Text = "Another", Width = 1 }
   },
   // second "row"
   {
      { Text = "Extra-wide", Width = 3 },
      { Text = "Random", Width = 1 }
   }
}

So the items are grouped into "rows" where Sum(width) in the internal List is less than or equal to a number (maxWidth - in my instance, 4). It's kinda a derivative of GroupBy, but the GroupBy is dependent on earlier values in the array - which is where I get stumped.

Any ideas would be appreciated.

Jamie Howarth
  • 3,273
  • 3
  • 20
  • 26

3 Answers3

3

We can combine the ideas of LINQ's Aggregate method with a GroupWhile method to group consecutive items while a condition is met to build an aggregate value for the current group to be used in the predicate:

public static IEnumerable<IEnumerable<T>> GroupWhileAggregating<T, TAccume>(
    this IEnumerable<T> source,
    TAccume seed,
    Func<TAccume, T, TAccume> accumulator,
    Func<TAccume, T, bool> predicate)
{
    using (var iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
            yield break;

        List<T> list = new List<T>() { iterator.Current };
        TAccume accume = accumulator(seed, iterator.Current);
        while (iterator.MoveNext())
        {
            accume = accumulator(accume, iterator.Current);
            if (predicate(accume, iterator.Current))
            {
                list.Add(iterator.Current);
            }
            else
            {
                yield return list;
                list = new List<T>() { iterator.Current };
                accume = accumulator(seed, iterator.Current);
            }
        }
        yield return list;
    }
}

Using this grouping method we can now write:

var query = data.GroupWhileAggregating(0,
    (sum, item) => sum + item.Width,
    (sum, item) => sum <= 4);
Servy
  • 202,030
  • 26
  • 332
  • 449
  • Nice but it's more or less a (I must agree far more elegant and flexible!) implementation of my answer below. – derpirscher Jul 08 '14 at 14:51
  • @derpirscher sadly elegance wins this round - his solution is spot on. – Jamie Howarth Jul 09 '14 at 10:57
  • should it be (sum, item) => sum + item.Width <= 4) ? – Chris DaMour Jan 24 '20 at 17:42
  • @ChrisDaMour No, because in this case the accumulator runs before the predicate, so the predicate is running with the "next" item having already change the accumulator. If you changed the grouping method to call the predicate before accumulating the next item, *then* you'd need to do that. – Servy Jan 25 '20 at 01:30
  • hm in my testing i had to do it that way, but maybe my case was different...i didn't want to go over the limit – Chris DaMour Jan 25 '20 at 02:23
1

You can sort of do that with the Batch method from MoreLinq library which is available as a NuGet package. The result is a List<IEnumerable<object>>. Here is the code:

class Obj
{
    public string Text {get;set;}
    public int Width {get;set;}
}

void Main()
{

    var data = new [] {
        new Obj { Text = "Hello", Width = 2 },
        new Obj { Text = "Something else", Width = 1 },
        new Obj { Text = "Another", Width = 1 },
        new Obj { Text = "Extra-wide", Width = 3 },
        new Obj { Text = "Random", Width = 1 }
    };

    var maxWidth = data.Max (d => d.Width );
    var result = data.Batch(maxWidth).ToList();
    result.Dump(); // Dump is a linqpad method

Output

enter image description here

NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61
  • 1
    Found a working batch function from [this answer](http://stackoverflow.com/a/13731854/1277156) as well. – Measurity Jul 08 '14 at 11:50
  • 2
    that's quite a clever implementation – NeddySpaghetti Jul 08 '14 at 11:54
  • 2
    Batch(maxSize) will create Batches where each batch has at most maxSize elements. It does not take into account any of the properties of the elements. I.e. if you change the Width of "Hello" to 4, the first batch will also contain "Something else" and "another" and even "Extra-wide", although as of the original question it should only consist of "Hello". – derpirscher Jul 08 '14 at 13:11
0

I don't think you can do that with LINQ. One alternative approach would be the following:

var data = ... // original data
var newdata = new List<List<object>>();
int csum = 0;
var crow = new List<object>();
foreach (var o in data) {
    if (csum + o.Width > 4) {  //check if the current element fits into current row
        newdata.Add(crow);  //if not add current row to list
        csum = 0;
        crow = new List<object>();  //and create new row
    }
    crow.Add(o); //add current object to current row
    csum += o.Width;
}

if (crow.Count() > 0)  //last row
    newData.Add(c);

EDIT: The other answer suggests to use Batch from the MoreLinq Library. In fact, the above source code, is more or less the same, what Batch does, but not only counting the elements in each batch but summing up the desired property. One could possibly generalize my code with a custom selector to be more flexible in terms of "batch size".

derpirscher
  • 14,418
  • 3
  • 18
  • 35