views:

1976

answers:

19

Using a comma is just to make it specific - it could be any delimiter, so using List's default toString() doesn't help. (The question could be rephrased as: "best way to implement List's toString?")

This is a well-known problem, and I know several ways of doing it, but I'm wondering what the best way is (where "best" means clearest and/or shortest, not most efficient).

The problem as a story: I have a list. I want to loop over it, printing each value. I want to print a comma between each item - but not after the last one (nor before the first one).

The problem as a grammar (edit: added second alternative):

List --> Item ( , Item ) *
List --> ( Item , ) * Item

Sample solution 1:

boolean isFirst = true;
for (Item i : list) {
  if (isFirst) {
    System.out.print(i);        // no comma
    isFirst = false;
  } else {
    System.out.print(", "+i);   // comma
  }
}

Sample solution 2 - create a sublist:

if (list.size()>0) {
  System.out.print(list.get(0));   // no comma
  List theRest = list.subList(1, list.size());
  for (Item i : theRest) {
    System.out.print(", "+i);   // comma
  }
}

Sample solution 3:

  Iterator<Item> i = list.iterator();
  if (i.hasNext()) {
    System.out.print(i.next());
    while (i.hasNext())
      System.out.print(", "+i.next());
  }

These treat the first item specially; one could instead treat the last one specially.

Incidentally, here is how List toString is implemented (it's inherited from AbstractCollection), in Java 1.6:

public String toString() {
    Iterator<E> i = iterator();
    if (! i.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = i.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! i.hasNext())
            return sb.append(']').toString();
        sb.append(", ");
    }
}

It exits the loop early to avoid the comma after the last item. BTW: this is the first time I recall seeing "(this Collection)"; here's code to provoke it:

List l = new LinkedList();
l.add(l);
System.out.println(l);

I welcome any solution, even if they use unexpected libraries (regexp?); and also solutions in languages other than Java (e.g. I think Python/Ruby have an intersperse function - how is that implemented?).

Clarification: by libraries, I mean the standard Java libraries. For other libraries, I consider them with other languages, and interested to know how they're implemented.

EDIT toolkit mentioned a similar question: http://stackoverflow.com/questions/285523/last-iteration-of-for-loop-in-java

And another: http://stackoverflow.com/questions/156650/does-the-last-element-in-a-loop-deserve-a-separate-treatment

+3  A: 

In Python its easy

",".join( yourlist )

In C# there is a static method on the String class

String.Join(",", yourlistofstrings)

Sorry, not sure about Java but thought I'd pipe up as you asked about other languages. I'm sure there would be something similar in Java.

tarn
IIRC, you get a trailing , in the .Net version. :-(
Tommy
Just tested, no trailing delimiter in .NET or Python
tarn
thanks! I found the source for it: http://svn.python.org/view/python/branches/release22-branch/Objects/stringobject.c?view=markup search for "Catenate everything". It omits the separator for the last item.
13ren
@13ren Nicely done! Does it help? I had a lock and quickly remembered why I choose not to program in C these days :P
tarn
I mean look, not lock.
tarn
@tarn, yeah, it's an interesting example because it only adds the comman if it is not the last item: if (i < seqlen - 1) { <add separator> }
13ren
+3  A: 
StringBuffer result = new StringBuffer();
for(Iterator it=list.iterator; it.hasNext(); ) {
  if (result.length()>0)
    result.append(", ");
  result.append(it.next());
}

Update: As Dave Webb mentioned in the comments this may not produce correct results if the first items in the list are empty strings.

cherouvim
Cute, unexpected, I like it! thanks.
13ren
meh.. still has the if in the loop.
sfossen
You can use a foreach loop to make it shorter.
Peter Lawrey
That won't work if the first item in the list is an empty string.
Dave Webb
@Dave Webb: Yes, thanks. The question does not have such a requirement though.
cherouvim
@cherovium: The question doesn't say the none of the items will be the empty string so it's an implicit requirement. If you're creating a CSV file empty fields can be pretty common.
Dave Webb
@Dave Webb: Now I get it. You are right.
cherouvim
Turns out Jon Skeet gave this answer, too (meaning you're in good company): http://stackoverflow.com/questions/285523/last-iteration-of-for-loop-in-java/285544#285544
13ren
+3  A: 

I usually use something similar to version 3. It works well c/c++/bash/... :P

sfossen
+1  A: 
for(int i=0, length=list.size(); i<length; i++)
  result+=(i==0?"":", ") + list.get(i);
cherouvim
Not exactly "pretty", but it works...
David Zaslavsky
Yes it's a bit cryptic, but an alternative to what's already been posted.
cherouvim
I think the code looks fine, IMHO its more pretty than your other answer and its hardly 'cryptic'. Wrap it up in a method called Join and your laughing, still I would expect the language to have a better way of solving what is really a common problem.
tarn
@tarn: you think it looks good because you have a Python/perl background ;) I like it as well
cherouvim
+11  A: 
org.apache.commons.lang.StringUtils.join(list,",");
bwalliser
Found the source: http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/java/org/apache/commons/lang/StringUtils.java?view=markupThe join method has 9 overloadings. The iteration-based ones are like "Sample solution 3"; the index-based ones use: if (i > startIndex) { <add separator> }
13ren
I'm really after implementations though. It's a good idea to wrap it up in a function, but in practice, my delimiter is sometimes a newline, and each line is also indented to some specified depth. Unless... join(list, "\n"+indent) ...will that always work? Sorry, just thinking aloud.
13ren
+1 for using a more or less standard library
keuleJ
A: 

You can also unconditionally add the delimiter string, and after the loop remove the extra delimiter at the end. Then an "if list is empty then return this string" at the beginning will allow you to avoid the check at the end (as you cannot remove characters from an empty list)

So the question really is:

"Given a loop and an if, what do you think is the clearest way to have these together?"

Thorbjørn Ravn Andersen
+1  A: 

Based on Java's List toString implementation:

Iterator i = list.iterator();
for (;;) {
  sb.append(i.next());
  if (! i.hasNext()) break;
  ab.append(", ");
}

It uses a grammar like this:

List --> (Item , )* Item

By being last-based instead of first-based, it can check for skip-comma with the same test to check for end-of-list. I think this one is very elegant, but I'm not sure about clarity.

13ren
+18  A: 

See also #285523

    String delim = "";
    for (Item i : list) {
        sb.append(delim).append(i);
        delim = ",";
    }
toolkit
A very surprising way to store state! It's almost OO polymorphic. I don't like the unnecessary assignment every loop, but I bet it's more efficient than an if. Most solutions repeat a test we know won't be true - though inefficient, they're probably the clearest.Thanks for the link!
13ren
I like this, but because it's clever and surprising, I thought it wouldn't be appropriate to use (surprising is not good!). However, I couldn't help myself. I'm still not sure about it; however, it is so short that it is easy to work out provided there's a comment to tip you off.
13ren
+2  A: 

I usually do this :

StringBuffer sb = new StringBuffer();
Iterator it = myList.iterator();
if (it.hasNext()) { sb.append(it.next().toString()); }
while (it.hasNext()) { sb.append(",").append(it.next().toString()); }

Though I think I'll to a this check from now on as per the Java implementation ;)

Alex Marshall
That's same logic as Sample 3, I like it because it does nothing unnecessary. However, I don't know if it is the clearest. BTW: it's slightly more efficient if you put the while inside the if, by avoiding two tests when the list is empty. It also makes it slighly clearer, to me.
13ren
+1  A: 

This is very short, very clear, but gives my sensibilities the creeping horrors. It's also a bit awkward to adapt to different delimiters, especially if a String (not char).

for (Item i : list)
  sb.append(',').append(i);
if (sb.charAt(0)==',') sb.deleteCharAt(0);

Inspired by: http://stackoverflow.com/questions/285523/last-iteration-of-for-loop-in-java/669221#669221

13ren
+2  A: 

If you can use Groovy (which runs on the JVM):

def list = ['a', 'b', 'c', 'd']
println list.join(',')
kungfoo
thanks, but I'm interested in implementations. How is this one implemented?
13ren
+1  A: 

If you use the Spring Framework you can do it with StringUtils:

public static String arrayToDelimitedString(Object[] arr)
public static String arrayToDelimitedString(Object[] arr, String delim)
public static String collectionToCommaDelimitedString(Collection coll)
public static String collectionToCommaDelimitedString(Collection coll, String delim)
Henryk Konsek
Thanks, but how is it implemented?
13ren
+1  A: 

(Copy paste of my own answer from here.) Many of the solutions described here are a bit over the top, IMHO, especially those that rely on external libraries. There is a nice clean, clear idiom for achieving a comma separated list that I have always used. It relies on the conditional (?) operator:

Edit: Original solution correct, but non-optimal according to comments. Trying a second time:

int[] array = {1, 2, 3};
StringBuilder builder = new StringBuilder();
for (int i = 0 ;  i < array.length; i++)
       builder.append(i == 0 ? "" : ",").append(array[i]);

There you go, in 4 lines of code including the declaration of the array and the StringBuilder.

2nd Edit: If you are dealing with an Iterator:

    List<Integer> list = Arrays.asList(1, 2, 3);
    StringBuilder builder = new StringBuilder();
    for (Iterator it = list.iterator(); it.hasNext();)
        builder.append(it.next()).append(it.hasNext() ? "," : "");
Julien Chastang
This and http://stackoverflow.com/questions/668952/clearest-way-to-comma-delimit-a-list-java/668972#668972 above are similar. It's short and clear, but if you only have an iterator, you'd first need to convert. Inefficient, but maybe worth it for the clarity of this approach?
13ren
Ugh, string concatenation creating a temporary StringBuilder within use of a StringBuilder. Inefficient. Better (more likes of Java, but better running code) would be "if (i > 0) { builder.append(','); }" followed by builder.append(array[i]);
Eddie
Yes, if you look in the bytecode, you are right. I can't believe such a simple question can get so complicated. I wonder if the compiler can optimize here.
Julien Chastang
Provided 2nd, hopefully better solution.
Julien Chastang
Might be better to split them (so people can vote on one or other).
13ren
I'd assumed this test-every-time-style only worked with arrays, not iterators - good one! BTW: You have two "hasNext()" tests - they can be combined like this: http://stackoverflow.com/questions/668952/clearest-way-to-comma-delimit-a-list-java/669127#669127 Shorter is nice, but which is clearer?
13ren
That is, a single hasNext() can both break out of the loop, and omit the last comma.
13ren
+1  A: 

I didn't compile it... but should work (or be close to working).

public static <T> String toString(final List<T> list, 
                                  final String delim)
{
    final StringBuilder builder;

    builder = new StringBuilder();

    for(final T item : list)
    {
        builder.append(item);
        builder.append(delim);
    }

    // kill the last delim
    builder.setLength(builder.length() - delim.length());

    return (builder.toString());
}
TofuBeer
A: 
for (int i=0; i<1; i++)
  sb.append(array[i]);
for (int i=1; i<array.length; i++)
  sb.append(",").append(array[i]);

Based on http://stackoverflow.com/questions/668952/clearest-way-to-comma-delimit-a-list-java/669831#669831

Using this idea: http://stackoverflow.com/questions/156650/does-the-last-element-in-a-loop-deserve-a-separate-treatment

13ren
+1  A: 
StringBuffer sb = new StringBuffer();

for (int i = 0; i < myList.size(); i++)
{ 
    if (i > 0) 
    {
        sb.append(", ");
    }

    sb.append(myList.get(i)); 
}
Aries McRae
+1  A: 

I somewhat like this approach, which I found on a blog some time ago. Unfortunately I don't remember the blog's name/URL.

You can create a utility/helper class that looks like this:

private class Delimiter
{
    private final String delimiter;
    private boolean first = true;

    public Delimiter(String delimiter)
    {
        this.delimiter = delimiter;
    }

    @Override
    public String toString()
    {
        if (first) {
            first = false;
            return "";
        }

        return delimiter;
    }
}

Using the helper class is simple as this:

StringBuilder sb = new StringBuilder();
Delimiter delimiter = new Delimiter(", ");

for (String item : list) {
    sb.append(delimiter);
    sb.append(item);
}
Tobias Müller
Just a note: it should be "delimiter", not "delimeter".
Michael Myers
Oops. Thank you. Fixed.
Tobias Müller
A: 
I think you meant to say `listToString.substring(0, listToString.length()-separator.length());`
13ren
A: 

One option for the foreach loop is:

StringBuilder sb = new StringBuilder();
for(String s:list){
  if (sb.length()>0) sb.append(",");
  sb.append(s);
}
BAH