Using Enumerable.Aggregate(...) Method over an empty sequence
Asked Answered
A

5

34

I would like to use the Enumerable.Aggregate(...) method to concatenate a list of strings separated by a semicolon. Rather easy, isn't it?

Considering the following:

  • private const string LISTSEPARATOR = "; ";
  • album.OrderedTracks is List<TrackDetails>
  • TrackDetails has DiscNumber Int16? property

The following statement will trow an exception if the sequence returned by Distinct() is empty (as the Aggregate() method doesn't apply on empty sequence):

    txtDiscNumber.Text = album.OrderedTracks
        .Where(a => a.DiscNumber.HasValue)
        .Select(a => a.DiscNumber.Value.ToString())
        .Distinct()
        .Aggregate((i, j) => i + LISTSEPARATOR + j);

The workaround I am using:

    List<string> DiscNumbers = 
        album.OrderedTracks
            .Where(a => a.DiscNumber.HasValue)
            .Select(a => a.DiscNumber.Value.ToString())
            .Distinct()
            .ToList();

    if (!DiscNumbers.Any())
        txtDiscNumber.Text = null;
    else
        txtDiscNumber.Text = 
            DiscNumbers.Aggregate((i, j) => i + LISTSEPARATOR + j);

Is there any better solution? Is it possible to do this in a single LINQ statement?

Thanks in advance.

Aphotic answered 15/2, 2013 at 14:43 Comment(1)
Aggregate, in general, is not a good idea for dealing with aggregating strings, because concating strings is not a cheap operation, and it scales very poorly. If you're going to do this yourself you should be using something like a StringBuilder, although in your specific case you can use String.Join, which will internally avoid excessive string concatenation, so not only does it handle this edge case better, but it will perform much better for non-trivial data sets.Trochaic
C
49

To concatenate a list of strings, use the string.Join method.

The Aggregate function doesn't work with empty collections. It requires a binary accumulate function and it needs an item in the collection to pass to the binary function as a seed value.

However, there is an overload of Aggregate:

public static TResult Aggregate<TSource, TAccumulate, TResult>(
    this IEnumerable<TSource> source,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> func,
    Func<TAccumulate, TResult> resultSelector
)

This overload allows you to specify a seed value. If a seed value is specified, it will also be used as the result if the collection is empty.

EDIT: If you'd really want to use Aggregate, you can do it this way:

sequence.Aggregate(string.Empty, (x, y) => x == string.Empty ? y : x + Separator + y)

Or this way by using StringBuilder:

sequence.Aggregate(new StringBuilder(), (sb, x) => (sb.Length == 0 ? sb : sb.Append(Separator)).Append(x)).ToString()
Coagulase answered 15/2, 2013 at 14:46 Comment(1)
Please note that if you use the seed value, it will appear at the begining of the result string: ;item1;item2Denunciate
C
12

I think you might find the following helper extension method useful.

public static TOut Pipe<TIn, TOut>(this TIn _this, Func<TIn, TOut> func)
{
    return func(_this);
}

It allows you to express your query in the following way.

txtDiscNumber.Text = album.OrderedTracks
    .Where(a => a.DiscNumber.HasValue)
    .Select(a => a.DiscNumber.Value.ToString())
    .Distinct()
    .Pipe(items => string.Join(LISTSEPARATOR, items));

This still reads "top to bottom," which greatly aids readability.

Chemarin answered 15/2, 2013 at 21:17 Comment(0)
V
8

You can use

.Aggregate(string.Empty, (acc, name) => acc + (acc.Length > 0 ? separator : string.Empty) + name)

with the initial value it works for empty collections but it's not pretty with the length check

Vitebsk answered 20/2, 2019 at 15:45 Comment(3)
This is the actual answer for how to use Aggregate for this. Yes Join is better in this very specific situation, but not really the questionDextrorotation
This will prepend a LISTSEPARATOR before any other value.Boschbok
you're right, I've updated it with the length checkVitebsk
P
7

Use String.Join like this:

 txtDiscNumber.Text = String.Join(LISTSEPARATOR,
      album.OrderedTracks
                  .Where(a => a.DiscNumber.HasValue)
                  .Select(a => a.DiscNumber.Value.ToString())
                  .Distinct());
Pich answered 15/2, 2013 at 14:46 Comment(0)
T
-1

Used methods like that a lot for debugging purposes, came up with two extension-methods:

public static string Concatenate<T, U>(this IEnumerable<T> source, Func<T, U> selector, string separator = ", ")
{
    if (source == null)
    {
        return string.Empty;
    }

    return source
        .Select(selector)
        .Concatenate(separator);
}

public static string Concatenate<T>(this IEnumerable<T> source, string separator = ", ")
{
    if (source == null)
    {
        return string.Empty;
    }

    StringBuilder sb = new StringBuilder();
    bool firstPass = true;
    foreach (string item in source.Distinct().Select(x => x.ToString()))
    {
        if (firstPass)
        {
            firstPass = false;
        }
        else
        {
            sb.Append(separator);
        }

        sb.Append(item);
    }

    return sb.ToString();
}

Use like this:

string myLine = myCol.Concatenate(x => x.TheProperty);
Tortuous answered 15/2, 2013 at 15:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.