views:

1235

answers:

6

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#.

A: 

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
actually he said he has an array of email addresses, so there is no need to split by semi-colon.
Stan R.
And the output is supposed to be a combined string.
Daniel Earwicker
+1  A: 

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
+6  A: 

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[] { "[email protected]", "[email protected]", ...... };

foreach (var chunk in Chunk(addresses, 50))
{
    SendEmail(chunk.ToArray(), "Buy V14gr4");
}
dtb
+1 - nice answer
Scott Ivey
All seems a bit unnecessary to me! the string.Join method still needs to be called, and that can directly address a subrange within an array.
Daniel Earwicker
Nice, but we can do this without mutating "i". See my answer.
Eric Lippert
maybe i am missing something, but isnt Chunk<T> an Extension method, so shouldnt you be doing addresses.Chunk ?
Stan R.
+1  A: 

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
+2  A: 

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*50).Take(50).ToArray());
    DoSomething(s);
}
Mystere Man
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
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.
Mystere Man
+5  A: 

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!

Eric Lippert
Some index/i confusion?
Daniel Earwicker
Whoops, typo, thank you.
Eric Lippert
Beautiful :-) +1
DoctaJonez
Is there an equivalently elegant solution for batching IEnumerable (i.e. when you don't know the size) other than the awful looping code?
Erich Mirabal
Sure. Take a look at DTB's answer.
Eric Lippert