13

Using stream.collect(Collectors.joining(", ")) I can easily join all the strings of my stream delimited by a comma. A possible result would be "a, b, c". But what if I want the last delimiter to be different. For example to be " and " such that I get "a, b and c" as result. Is there an easy solution?

principal-ideal-domain
  • 3,998
  • 8
  • 36
  • 73
  • 3
    To be honest -- no, there isn't. `Collectors.joining` uses some pretty awkward tricks that break down completely when you try doing something like this. – Louis Wasserman Jan 22 '16 at 00:09
  • 1
    Implement your own [`Collector`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html). As for "easy", that's a matter of skill and opinion. – Andreas Jan 22 '16 at 00:19
  • @Andreas Is it possible to implement such a collector? How would the collector know when to use the special delimiter? – principal-ideal-domain Jan 22 '16 at 00:32
  • http://stackoverflow.com/questions/31044041/how-do-i-iterate-over-a-stream-in-java-using-for this question has a way of converting it into and array (one liner.) Then you can just iterate over it and add your custom delimiters. Should be pretty simple to do with an array :) – RisingSun Jan 22 '16 at 00:33
  • 4
    @erickson If you want the Oxford comma, specify the delimiters as `", "` and `", and "`. (I also prefer it.) – David Conrad Jan 22 '16 at 01:04
  • 1
    Actually, that won't quite work, but I've added an Oxford comma variant to my answer. – David Conrad Jan 22 '16 at 18:44

7 Answers7

17

If they are already in a list, no stream is needed; simply join a sublist of all but the last element and concat the other delimiter and the final element:

int last = list.size() - 1;
String joined = String.join(" and ",
                    String.join(", ", list.subList(0, last)),
                    list.get(last));

Here's a version that does the above using Collectors.collectingAndThen:

stream.collect(Collectors.collectingAndThen(Collectors.toList(),
    joiningLastDelimiter(", ", " and ")));

public static Function<List<String>, String> joiningLastDelimiter(
        String delimiter, String lastDelimiter) {
    return list -> {
                int last = list.size() - 1;
                if (last < 1) return String.join(delimiter, list);
                return String.join(lastDelimiter,
                    String.join(delimiter, list.subList(0, last)),
                    list.get(last));
            };
}

This version can also handle the case where the stream is empty or only has one value. Thanks to Holger and Andreas for their suggestions which greatly improved this solution.

I had suggested in a comment that the Oxford comma could be accomplished with this using ", " and ", and" as the delimiters, but that yields incorrect results of "a, and b" for two elements, so just for fun here's one that does Oxford commas correctly:

stream.collect(Collectors.collectingAndThen(Collectors.toList(),
    joiningOxfordComma()));

public static Function<List<String>, String> joiningOxfordComma() {
    return list -> {
                int last = list.size() - 1;
                if (last < 1) return String.join("", list);
                if (last == 1) return String.join(" and ", list);
                return String.join(", and ",
                    String.join(", ", list.subList(0, last)),
                    list.get(last));
            };
}
David Conrad
  • 15,432
  • 2
  • 42
  • 54
  • Also, I pasted that into Eclipse, and get a compile error, that can be fixed by explicitly giving the types: `Collector., String>of(` – Andreas Jan 22 '16 at 00:57
  • 1
    Hmm, `javac` doesn't give me a compile error, but I can add the explicit types. What version of Eclipse? – David Conrad Jan 22 '16 at 00:58
  • 2
    Since here the only relevant function is the *finisher*, you could simply use `Collectors.collectingAndThen(Collectors.toList(), list -> { … })`. By the way, you could also use `String.join(lastDelimiter, String.join(delimiter, list.subList(0, last)), list.get(last))` for the return statement. – Holger Jan 22 '16 at 10:18
  • The proposed solutions fail for the empty list. – Ivin Oct 01 '21 at 08:27
  • ...and fails also for the 1-element list – Ivin Oct 01 '21 at 08:41
  • 1
    @Ivin I just tried both the `joiningLastDelimiter()` and `joiningOxfordComma()` solutions, and they both work fine for the empty list and the 1-element list. Only the first solution would have to be modified with guards for those cases. – David Conrad Jan 04 '22 at 19:43
  • 1
    This can be a bit simpler without returning a Function, i.e., just a static method taking a List and returning a String (and then refer to it for the `finisher` argument with a function reference, `MyClass::joining...`) – Joshua Goldberg Apr 14 '23 at 20:45
7

If you're fine with "a, b, and c", then it's possible to use mapLast method of my StreamEx library which extends standard Stream API with additional operations:

String result = StreamEx.of("a", "b", "c")
                        .mapLast("and "::concat)
                        .joining(", "); // "a, b, and c"

The mapLast method applies given mapping to the last stream element keeping others unchanged. I even have similar unit-test.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • 1
    This is not the answer the OP wants, but if you're a fan of the Oxford comma, this answer is grammatically "more" correct. Thanks Tagir. – Sean Connolly Aug 12 '16 at 18:38
1

Try joining the last 2 strings first with stream.collect(Collectors.joining(" and "))

Then join all remaining strings and this new string with the code you used in your question: stream.collect(Collectors.joining(", ")).

The Coding Wombat
  • 805
  • 1
  • 10
  • 29
1

If you are looking for old Java solution, using Guava libraries would be easy.

    List<String> values = Arrays.asList("a", "b", "c");
    String output = Joiner.on(",").join(values);
    output = output.substring(0, output.lastIndexOf(","))+" and "+values.get(values.size()-1);
    System.out.println(output);//a,b and c
Jude Niroshan
  • 4,280
  • 8
  • 40
  • 62
0
String str = "a , b , c , d";
String what_you_want = str.substring(0, str.lastIndexOf(","))
        + str.substring(str.lastIndexOf(",")).replace(",", "and");

// what_you_want is : a , b , c and d
Manos Nikolaidis
  • 21,608
  • 12
  • 74
  • 82
Rahmat Waisi
  • 1,293
  • 1
  • 15
  • 36
0
    List<String> names = Arrays.asList("Thomas", "Pierre", "Yussef", "Rick");
    int length = names.size();
    String result = IntStream.range(0, length - 1).mapToObj(i -> {
        if (i == length - 2) {
            return names.get(i) + " and " + names.get(length - 1);
        } else {
            return names.get(i);
        }
    }).collect(Collectors.joining(", "));
jbwt
  • 384
  • 5
  • 14
0

This is not a Streams-API solution but is pretty fast. Enjoy!

public static final <E> String join(
    Iterable<E> objects, String separator, String lastSeparator) 
{
    Objects.requireNonNull(objects);

    final String sep = separator == null ? "" : separator;
    final String lastSep = lastSeparator == null ? sep : lastSeparator;

    final StringBuilder builder = new StringBuilder();

    final Iterator<E> iterator = objects.iterator();
    while (iterator.hasNext()) {
        final E next = iterator.next();
        if (builder.length() > 0) {
            if (iterator.hasNext()) {
                builder.append(sep);
            }
            else {
                builder.append(lastSep);
            }
        }
        builder.append(next);
    }

    return builder.toString();
}
Erk
  • 1,159
  • 15
  • 9