33

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.

lorcan
  • 359
  • 1
  • 3
  • 4
  • 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. – Servy Feb 15 '13 at 15:05

5 Answers5

50

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()
Tom Pažourek
  • 9,582
  • 8
  • 66
  • 107
  • 6
    Please note that if you use the seed value, it will appear at the begining of the result string: `;item1;item2` – Cosmin Oct 10 '16 at 13:28
11

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.

Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
8

You can use

.Aggregate(string.Empty, (i, j) => i + LISTSEPARATOR + j);

with the initial value it works for empty collections

Karpik
  • 322
  • 1
  • 5
  • 14
  • 1
    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 question – George Mauer Apr 08 '19 at 19:05
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());
Cristian Lupascu
  • 39,078
  • 16
  • 100
  • 137
0

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);