2

I would like to pass in a different function to Aggregate for the last element in the collection.

A use for this would be:

List<string> listString = new List{"1", "2", "3"};
string joined = listString.Aggregate(new StringBuilder(), 
                                    (sb,s) => sb.Append(s).Append(", "), 
                                    (sb,s) => sb.Append(s)).ToString();

//joined => "1, 2, 3"

What would be a custom implementation if no other exists?

P.S. I would like to do this w/ composable functions iterating once through the collection. In other words, I do not want to do a Select wrapped in a String.Join

Paul Nikonowicz
  • 3,883
  • 21
  • 39
  • 11
    For the **output** you can use `string.Join(", ", listString);` – Habib May 22 '14 at 18:09
  • @PaulNikonowicz: What do you mean you would like to do it with _"composable functions"_? Is there a particular requirement you need besides the simple `String.Join` call Habib suggested? – Chris Sinclair May 22 '14 at 18:12
  • 3
    I think one reason you're having trouble finding a way to do this is that it runs contrary to the functional nature of LINQ. You're trying to take a side-effect-based approach to something that should have no side-effects. – JLRishe May 22 '14 at 18:13
  • 1
    @JLRishe Hm... I don't see any side-effects here. Can you, please, explain – Sergey Krusch May 22 '14 at 18:19
  • @SergeyKrusch `sb.Append(s)` mutates the value of `sb`, and that's what we call a side-effect. – JLRishe May 22 '14 at 18:22
  • @JLRishe Ah, this. Yes. I was thinking about the custom Aggregate method :) – Sergey Krusch May 22 '14 at 18:24
  • 1
    @JLRishe - but it is not directly related to question - one can use exactly the same code with `Enumerable.Concat` instead of `StringBuilder` that does not mutate anything (but sample will look way more complicated than needs to be). – Alexei Levenkov May 22 '14 at 18:24
  • @AlexeiLevenkov If you're using `Enumerable.Concat` then you probably won't need a special case function for the last element. For example, a side-effect free equivalent to the example in the question is `string joined = listString.Aggregate((l, r) => string.Concat(l, ", ", r));`. – JLRishe May 22 '14 at 18:34
  • 1
    This is fundamentally not possible in any reasonably way because LINQ can't know when it's reached the end of the list until *after* it's called your delegate for the item. Think of an `IEnumerable` wrapper around a `NetworkStream`, for example. You don't know that you've reached the end of the stream until you try to read the next string ... potentially long after you've already processed the previous string. – Jim Mischel May 22 '14 at 18:36
  • @JimMischel - agree that it is not possible with `Aggregate` alone, but a function that handles whole sequence (like `Aggregate`/`Sum`) can easily postpone handling of the item till it looks at next one and decide if it is first/middle/last one. Definitely "last" not going to work with methods similar to `Where` or `Select` that don't look at whole sequence at once, but at least one can special case "first" with these. – Alexei Levenkov May 22 '14 at 19:00
  • @ChrisSinclair What I mean by "composable functions" is that I don't want to use string.Join. – Paul Nikonowicz May 22 '14 at 19:35
  • @JLRishe it doesn't matter that the implementation of `sb.Append` mutates the value of `sb` because `sb.Append` returns the `StringBuffer` being modified. – Paul Nikonowicz May 22 '14 at 19:36
  • @JimMischel Why not have look ahead functionality? Streams have a `Peek` on them. – Paul Nikonowicz May 22 '14 at 19:40
  • @PaulNikonowicz: Any particular reason _why_ you don't want to use `String.Join`? EDIT: Ahh, I see your edit to the question. So you want to avoid a usage like: `string joined = String.Join(", ", myCollection.Select(obj => obj.SomeProperty.ToString()));`? In that case, you can easily create your own extension method against `IEnumerable` (which would wrap `String.Join`, or _whatever_ implementation you wish) whereby your usage syntax would be `string joined = myCollection.Select(obj => obj.SomeProperty.ToString()).Join(", ");` – Chris Sinclair May 22 '14 at 20:27
  • @ChrisSinclair That's one way. I'm still hoping for a tail function however. – Paul Nikonowicz May 23 '14 at 00:29
  • @PaulNikonowicz: The only way to do it with lookahead is if you delay returning the current item until you're certain that one is following, or that you're at the end of the stream. With `NetworkStream`, that means you would block on the last item until the stream was closed. This could be a problem if you have to process lines in a timely manner. For `NetworkStream` and similar constructs a `Peek` function isn't very helpful because there are three possible states: 1) definitely bytes following; 2) stream has been closed; 3) no *known* bytes following, but stream is still open. – Jim Mischel May 23 '14 at 02:28
  • @AlexeiLevenkov: Yes, you can easily special-case the *first* item. But that's not what the OP asked for. He specifically asked to special-case the last item, which is a much harder problem. – Jim Mischel May 23 '14 at 02:42
  • 1
    @PaulNikonowicz Yes, it does "matter" whether there are side-effects, because if you are trying to perform a LINQ operation in which the functions have side-effects, it's a pretty good indicator that you are going about it the wrong way. And you still haven't told us _why_ you don't want to use `String.Join`. Starting with .NET 4, [`String.Join`](http://msdn.microsoft.com/en-us/library/dd992421(v=vs.100).aspx) can take an `IEnumerable` and will only enumerate the list once if you use it with a `Select` or even a whole sequence of LINQ operations. – JLRishe May 23 '14 at 03:15
  • ... in other words, "[using] composable functions iterating once through the collection" and "do[ing] a `Select` wrapped in a `String.Join`" are not mutually exclusive, though your post implies that they are. – JLRishe May 23 '14 at 03:22
  • @JimMischel I fail to see how last is harder than first for function that sees *whole sequence* (like `Aggregate` or `Join`) which I think the question is about. Such function does not even have to call any callbacks *before* finishing iteration (clearly not best approach, but you should not see observable difference unless you have some serious external notification going on). – Alexei Levenkov May 23 '14 at 05:15
  • @JLRishe I didn't know that `String.Join` took an IEnumerable. That's pretty cool. – Paul Nikonowicz May 23 '14 at 06:28

2 Answers2

3

Aggregate does not allow that in natural way.

You can carry previous element and do you final handling after Aggregate. Also I think your best bet would be to write custom method that does that custom handling for last (and possibly first) element.

Some approximate code to special case last item with Aggregate (does not handle most special case like empty/short list):

var firstLast = seq.Aggregate(
  Tuple.Create(new StringBuilder(), default(string)),
  (sum, cur) => 
     {
       if (sum.Item2 != null)
       {
            sum.Item1.Append(",");
            sum.Item1.Append(sum.Item2);
       }
       return Tuple.Create(sum.Item1, cur);
     });
 firstLast.Item1.Append(SpecialProcessingForLast(sum.Item2));
 return firstLast.Item1.ToString();  

Aggregate with special case for "last". Sample is ready to copy/paste to LinqPad/console app, uncomment "this" when making extension function. Main shows aggregating array with summing all but last element, last one is subtracted from result:

void Main()
{
   Console.WriteLine(AggregateWithLast(new[] {1,1,1,-3}, 0, (s,c)=>s+c, (s,c)=>s-c));
   Console.WriteLine(AggregateWithLast(new[] {1,1,1,+3}, 0, (s,c)=>s+c, (s,c)=>s-c));
}

public static TAccumulate AggregateWithLast<TSource, TAccumulate>(
    /*this */ IEnumerable<TSource> source, 
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> funcAll,
    Func<TAccumulate, TSource, TAccumulate> funcLast)
{
  using (IEnumerator<TSource> sourceIterator = source.GetEnumerator())
  {
    if (!sourceIterator.MoveNext())
    {
      return seed;
    }

    TSource last = sourceIterator.Current;
    TAccumulate total = seed;

    while (sourceIterator.MoveNext())
    {
      total = funcAll(total, last);
      last = sourceIterator.Current;
    }

    return funcLast(total, last);
  }
}

Note: if you need just String.Join than one in .Net 4.0+ takes IEnumerable<T> - so it will iterate sequence only once without need to ToList/ToArray.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • I would write custom Aggregate function as well. – Sergey Krusch May 22 '14 at 18:35
  • this is a cool overloaded example for the existing `Aggregate` implementation. How about a custom implementation that takes a second "tail" function? – Paul Nikonowicz May 22 '14 at 19:44
  • @PaulNikonowicz here you go. Not really tested... Code partially copied from Jon Skeet's answer for [Use LINQ to select by min](http://stackoverflow.com/questions/914109/how-to-use-linq-to-select-object-with-minimum-or-maximum-property-value/914279#914279) – Alexei Levenkov May 24 '14 at 03:27
1

Another approach for your particular example, is to skip the comma for the first element and prepend it to the tail elements, like this:

List<string> listString = new() { "1", "2", "3" };
string joined = listString
    .Select((value, index) => (value, index))
    .Aggregate(new StringBuilder(), (sb, s) =>
        s.index == 0
            ? sb.Append(s.value)
            : sb.Append(", ").Append(s.value))
    .ToString();

I know this does not address the question in the title, but for most "join things with some infix" problems, this works well. That is, when string.Join is not the solution.

asgerhallas
  • 16,890
  • 6
  • 50
  • 68
  • This is good. I suppose the "index" could instead be the size of the List, and then the last element could be processed instead. – Paul Nikonowicz Oct 11 '22 at 13:56
  • 1
    @PaulNikonowicz yes, you are right of course :) But I wanted to also address the P.S. part of the question: "I would like to do this w/ composable functions iterating once through the collection." and I took that to mean he wanted to work on IEnumerables, and wouldn't want to enumerate them to count the length. I should have made that more clear in my code. – asgerhallas Oct 24 '22 at 18:55