19

I know how to do this in an ugly way, but am wondering if there is a more elegant and succinct method.

I have a string array of e-mail addresses. Assume the string array is of arbitrary length -- it could have a few items or it could have a great many items. I want to build another string consisting of say, 50 email addresses from the string array, until the end of the array, and invoke a send operation after each 50, using the string of 50 addresses in the Send() method.

The question more generally is what's the cleanest/clearest way to do this kind of thing. I have a solution that's a legacy of my VBScript learnings, but I'm betting there's a better way in C#.

  • Thanks everyone for the amazing answers -- indeed your methods were vastly better than the simple looping solution I had implemented in days of yore. It was exactly the kind of guidance I was hoping to get! –  Jun 18 '09 at 00:18
  • possible duplicate of [Split List into Sublists with LINQ](http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq) – nawfal Feb 18 '13 at 11:03

8 Answers8

47

You want elegant and succinct, I'll give you elegant and succinct:

var fifties = from index in Enumerable.Range(0, addresses.Length) 
              group addresses[index] by index/50;
foreach(var fifty in fifties)
    Send(string.Join(";", fifty.ToArray());

Why mess around with all that awful looping code when you don't have to? You want to group things by fifties, then group them by fifties. That's what the group operator is for!

UPDATE: commenter MoreCoffee asks how this works. Let's suppose we wanted to group by threes, because that's easier to type.

var threes = from index in Enumerable.Range(0, addresses.Length) 
              group addresses[index] by index/3;

Let's suppose that there are nine addresses, indexed zero through eight

What does this query mean?

The Enumerable.Range is a range of nine numbers starting at zero, so 0, 1, 2, 3, 4, 5, 6, 7, 8.

Range variable index takes on each of these values in turn.

We then go over each corresponding addresses[index] and assign it to a group.

What group do we assign it to? To group index/3. Integer arithmetic rounds towards zero in C#, so indexes 0, 1 and 2 become 0 when divided by 3. Indexes 3, 4, 5 become 1 when divided by 3. Indexes 6, 7, 8 become 2.

So we assign addresses[0], addresses[1] and addresses[2] to group 0, addresses[3], addresses[4] and addresses[5] to group 1, and so on.

The result of the query is a sequence of three groups, and each group is a sequence of three items.

Does that make sense?

Remember also that the result of the query expression is a query which represents this operation. It does not perform the operation until the foreach loop executes.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
14

Seems similar to this question: Split a collection into n parts with LINQ?

A modified version of Hasan Khan's answer there should do the trick:

public static IEnumerable<IEnumerable<T>> Chunk<T>(
    this IEnumerable<T> list, int chunkSize)
{
    int i = 0;
    var chunks = from name in list
                 group name by i++ / chunkSize into part
                 select part.AsEnumerable();
    return chunks;
}

Usage example:

var addresses = new[] { "a@example.com", "b@example.org", ...... };

foreach (var chunk in Chunk(addresses, 50))
{
    SendEmail(chunk.ToArray(), "Buy V14gr4");
}
Community
  • 1
  • 1
dtb
  • 213,145
  • 36
  • 401
  • 431
2

It sounds like the input consists of separate email address strings in a large array, not several email address in one string, right? And in the output, each batch is a single combined string.

string[] allAddresses = GetLongArrayOfAddresses();

const int batchSize = 50;

for (int n = 0; n < allAddresses.Length; n += batchSize)
{
    string batch = string.Join(";", allAddresses, n, 
                      Math.Min(batchSize, allAddresses.Length - n));

    // use batch somehow
}
Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
2

Assuming you are using .NET 3.5 and C# 3, something like this should work nicely:

string[] s = new string[] {"1", "2", "3", "4"....};

for (int i = 0; i < s.Count(); i = i + 50)
{
    string s = string.Join(";", s.Skip(i).Take(50).ToArray());
    DoSomething(s);
}
Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • That skip/take stuff is unnecessary copying - there's an overload of string.Join that lets you specify a range within the array. – Daniel Earwicker Jun 17 '09 at 19:06
  • However, Take will not throw an exception if you specify more items than are in the array. Your Math.Min addresses that, but I find the linq solution more readable and easier to understand. – Erik Funkenbusch Jun 17 '09 at 19:10
  • There is a bug here. the first iteration of the loop you'll skip 0, and take 50, on the second iteration you'll skip 2500 (i became 50, so i*50 = 2500), instead of 50 – Oleg D. Dec 21 '11 at 19:33
1

I would just loop through the array and using StringBuilder to create the list (I'm assuming it's separated by ; like you would for email). Just send when you hit mod 50 or the end.

void Foo(string[] addresses)
{
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < addresses.Length; i++)
    {
        sb.Append(addresses[i]);
        if ((i + 1) % 50 == 0 || i == addresses.Length - 1)
        {
            Send(sb.ToString());
            sb = new StringBuilder();
        }
        else
        {
            sb.Append("; ");
        }
    }
}

void Send(string addresses)
{
}
Jon B
  • 51,025
  • 31
  • 133
  • 161
0

I think we need to have a little bit more context on what exactly this list looks like to give a definitive answer. For now I'm assuming that it's a semicolon delimeted list of email addresses. If so you can do the following to get a chunked up list.

public IEnumerable<string> DivideEmailList(string list) {
  var last = 0;
  var cur = list.IndexOf(';');
  while ( cur >= 0 ) {
    yield return list.SubString(last, cur-last);
    last = cur + 1;
    cur = list.IndexOf(';', last);
  }
}

public IEnumerable<List<string>> ChunkEmails(string list) {
  using ( var e = DivideEmailList(list).GetEnumerator() ) {
     var list = new List<string>();
     while ( e.MoveNext() ) {
       list.Add(e.Current);
       if ( list.Count == 50 ) {
         yield return list;
         list = new List<string>();
       }
     }
     if ( list.Count != 0 ) {
       yield return list;
     }
  }
}
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
0

I think this is simple and fast enough.The example below divides the long sentence into 15 parts,but you can pass batch size as parameter to make it dynamic.Here I simply divide using "/n".

 private static string Concatenated(string longsentence)
 {
     const int batchSize = 15;
     string concatanated = "";
     int chanks = longsentence.Length / batchSize;
     int currentIndex = 0;
     while (chanks > 0)
     {
         var sub = longsentence.Substring(currentIndex, batchSize);
         concatanated += sub + "/n";
         chanks -= 1;
         currentIndex += batchSize;
     }
     if (currentIndex < longsentence.Length)
     {
         int start = currentIndex;
         var finalsub = longsentence.Substring(start);
         concatanated += finalsub;
     }
     return concatanated;
 }

This show result of split operation.

 var parts = Concatenated(longsentence).Split(new string[] { "/n" }, StringSplitOptions.None);
Nic
  • 12,220
  • 20
  • 77
  • 105
Olseno
  • 1
  • 2
0

Extensions methods based on Eric's answer:

public static IEnumerable<IEnumerable<T>> SplitIntoChunks<T>(this T[] source, int chunkSize)
{
    var chunks = from index in Enumerable.Range(0, source.Length)
                 group source[index] by index / chunkSize;

    return chunks;
}

public static T[][] SplitIntoArrayChunks<T>(this T[] source, int chunkSize)
{
    var chunks = from index in Enumerable.Range(0, source.Length)
                 group source[index] by index / chunkSize;

    return chunks.Select(e => e.ToArray()).ToArray();
}
turdus-merula
  • 8,546
  • 8
  • 38
  • 50