views:

271

answers:

10

is there a way in .NET (or some sort of standard extension methods) to ask questions of an enumeration?

For example is the current item the first or last item in the enumeration:

string s = "";

foreach (var person in PeopleListEnumerator) {

  if (PeopleListEnumerator.IsFirstItem) s += "[";

  s += person.ToString();

  if (!PeopleListEnumerator.IsLastItem) s += ",";
  else s += "]";
}
+2  A: 

Not that I know of. You can try writing such extension methods yourself, though. Or keep track of the first/last item elsewhere.

Eric Lippert had a blog post about that kind of problem recently along with some thoughts about how to modify your problem description to more accurately reflect what you actually want.

You're probably better off using a String.Join with some LINQ in this case:

String.Join(
  ",",
  (from p in PeopleListEnumerator
  select p.ToString()).ToArray()
)
Joey
A: 

I find myself defining and using a "bool first" local variable, for example:

string s = "[";
bool first = true;

foreach (var person in PeopleListEnumerator) {

  if (first)
    first = false;
  else
    s += ",";
  s += person.ToString();
}
s += "]";
ChrisW
+3  A: 

If your collection is a List, you can do:

string s = "[" + String.Join(",", PeopleList.ToArray()) + "]";
RedFilter
You can do it even if it's not a list. Your code works on any IEnumerable<string> assuming you have `using System.Linq` stuff.
Mehrdad Afshari
Thanks for the clarification, I'd only ever done this with a list...
RedFilter
+8  A: 

Just for the sake of fun, a solution to the general problem that doesn't require eager evaluation and has a single local variable (except the enumerator):

static class TaggedEnumerableExtensions
{
    public class TaggedItem<T>
    {
        public TaggedItem(T value, bool isFirst, bool isLast)
        {
            IsFirst = isFirst;
            IsLast = isLast;
            Value = value;
        }
        public T Value { get; private set; }
        public bool IsFirst { get; private set; }
        public bool IsLast { get; private set; }
    }
    public static IEnumerable<TaggedItem<T>> ToTaggedEnumerable<T>(this IEnumerable<T> e)
    {
        using (var enumerator = e.GetEnumerator()) {
            if (!enumerator.MoveNext())
                yield break;
            var current = enumerator.Current;
            if (!enumerator.MoveNext()) {
                yield return new TaggedItem<T>(current, true, true);
                yield break;
            } else {
                yield return new TaggedItem<T>(current, true, false);
            }

            for (;;) {
                current = enumerator.Current;
                if (!enumerator.MoveNext()) {
                    yield return new TaggedItem<T>(current, false, true);
                    yield break;
                }
                yield return new TaggedItem<T>(current, false, false);
            }
        }
    }
}

Test:

class Program
{
    static void Main(string[] args)
    {
        foreach (var item in Enumerable.Range(0, 10).ToTaggedEnumerable()) {
            Console.WriteLine("{0} {1} {2}", item.IsFirst, item.IsLast, item.Value);
        }
    }
}
Mehrdad Afshari
A: 

In .NET enumerations have no concept of order, except by value. If you want a first and last enumeration I would suggest explicitly defining what these first and last values are. The following enumeration is an example of this:

public enum PlayerType : byte
{
    Starter = byte.MinValue,
    RegularPlayer = 1,
    BenchWarmer = 2,
    Closer = byte.MaxValue
}

You can then express something similar to the following:

List<byte> values = new List<byte>((byte[])Enum.GetValues(typeof(PlayerType)));

PlayerType starter = (PlayerType)values.Min();
PlayerType closer = (PlayerType)values.Max();
karbon
This has nothing to do with the question
SLaks
I though originally this was what the OP was about before reading a bit more...
ShuggyCoUk
A: 

This code closely matches what you really want to accomplish:

StringBuilder s = new StringBuilder("[");
string delimiter = "";

foreach (var person in PeopleListEnumerator) {  
  s.Append(delimiter).Append(person.ToString());
  delimiter = ",";
}

return s.Append("]").ToString();
Joel Coehoorn
Elegant! (But then I have to add extra characters to bring the total up to 15, otherwise StackOverflow won't let me submit my comment :)
Darren Oster
+1  A: 

The IEnumerable interface does not define or expect the items that it returns to be in any given order. So the concept of "First" doesn't always apply.

However, for this particular pattern this is what I do

StringBuilder result = new StringBuilder();
result.Append( '[' );

foreach( var person in PeopleListEnumerator )
{
    if( result.Length > 1 )
        result.Append( ',' );
    result.Append( person.ToString() );
}

result.Append( ']' );
Paul Alexander
Fixed a typo: plus, this is especially true because an IEnumerable doesn't need a last item (it can go on forever), and it may not have a first item (they can be empty).
Joel Coehoorn
Wouldn't this always (if result had values) output [,item, item...]? The first comma should be suppressed.
Darren Oster
the if(result.Length >1) suppresses the first ','.
Paul Alexander
+1  A: 

Using LINQ, you can do:

string s = 
    string.Format("[{0}]", string.Join(",",PeopleListEnumerator.Select(p => p.ToString()).ToArray()));
Jason Jackson
Why on earth you might need to use string.Format("{0}", str)?
Mehrdad Afshari
Sorry, forgot the brackets, which was part of the requested solution. The string.Format function is cheaper that string concatenation, which is why I use it.
Jason Jackson
@Jason: Nope, that's a myth. In fact, the compiler optimizes "[" + x + "]" to a *single* call to `string.Concat` which will allocate all the memory beforehand. It's the fastest way you could concatenate a *known number of strings*; faster than `StringBuilder` and other methods. `string.Format` will have to parse the format specifier and do other stuff to achieve the same thing. Read: http://blogs.msdn.com/ricom/archive/2004/03/12/88715.aspx
Mehrdad Afshari
Do you have a better, more authoritative link? I could buy that the compiler would make that optimization for the case at hand, but that article is terribly formatted, discusses a related topic and doesn't spend much time focusing on this exact issue, and I actually read the string.Format() "trick" in a book (which I am willing to concede was wrong, as I have read plenty of things in books that were wrong).
Jason Jackson
string.Format is inherently doing more work since it must first parse the string... a _really_ fancy compiler could preparse it if it was sure the string never changes but none currently do (or are likely to).
ShuggyCoUk
As for a citation for what ""+ x+"" does compile it and take a look with reflector. below a certain number of arguments it generates a call to a particular override of string.Concat. above it it creates a local temporary array of the values and uses the params version of string.Concat. This behaviour is not mandated by the c# spec so they are free to change their minds. As to Concat being faster when done all at once it does indeed allocate the full required string all in one go so should be faster in all cases than a StringBuilder, again take a look at the source (string.ConcatArray)
ShuggyCoUk
unless of course the compiler knows that all arguments are constant at compile time at which point it guarantees (via the spec) that it will convert to a single string in advance
ShuggyCoUk
+1  A: 

Jon Skeet wrote Smart Enumerations to provide this sort of functionality and they are part of the MiscUtils library.

That said your specific example is best solved by the String.Join() approach as many others have pointed out. Writing a general string Join(this IEnumerable<T>,string) extension is not hard and is then usable in any more situations without having to resort to annoying temporary arrays.

ShuggyCoUk
A: 

The way I'd skin this cat (yes, there are that many ways) would be:

StringBuilder sb = new StringBuilder();

foreach (var person in PeopleListEnumerator )
{
    sb.AppendFormat(",{0}", person.ToString());
}

string s = "[" + sb.ToString().Substring(1) + "]";

Hope this helps...

Darren Oster
If I were going with your idea, that last line would use `sb.ToString(1, sb.Length-1)`. But I use the post append delimiter and then just adjust the length of the StringBuilder back the length of the delimiter.
Mark Hurd