4

I have a list of strings, e.g. days of the week. And I'd like to join them on comma, with "and" before the last element. E.g: "I'm available on Tue, Wed, Thu and Fri".

Something elegant like

Joiner.on(", ").join(days)

Does not work. Is there an elegant way to do it with Guava or similar library?

Thanks

Scott Mayers
  • 437
  • 6
  • 18
  • 2
    Get the results from above statement, do split on last delimiter and append with whatever you want. I don't think there is any predefined method available for what you are looking for. – kosa Jul 26 '16 at 19:01
  • That's a great solution, but potentially incorrect if I don't use just dates. If I make it a generic function and the joined elements have the delimiter, I'm in trouble... – Scott Mayers Jul 26 '16 at 19:27
  • If you want generic function, then just don't append last element in your join condition (take that element out of the list you are passing to join) & write your own method to append the last one, which would be generic enough to handle any case. You can make this more generic by, creating a single method with index which separates first group of elements & second group of elements, and user defined join "text". Use two joins and append the results of two joins. – kosa Jul 26 '16 at 19:37

4 Answers4

4

There is no straightforward solution, but you may consider mine:

final String COMMA = ", ", AND = " and ";
List<String> list = Arrays.asList("Tue", "Wed", "Thu", "Fri");
int last = list.size() - 1;

Joiner.on(AND).join(
    list.stream().limit(last).collect(joining(COMMA)),
    list.get(last)); // Tue, Wed, Thu and Fri

The another shorter way is:

list.stream().limit(last).collect(joining(COMMA, "", AND))
    .concat(list.get(last));

These methods perfectly work for 2+ days.

Edge cases (list == null || list.isEmpty() and list.size() < 2) may be handled by the if statements.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • Thanks, it's ok, list or array, this is a direction I was thinking about... – Scott Mayers Jul 26 '16 at 19:16
  • P.S. The annoying thing is that I have to take care of edge cases. E.g. your code will throw exception on an empty list. It's not a lot of work, but I wish Guava had a one-line solution.. – Scott Mayers Jul 26 '16 at 19:26
3

Considering you're "not Java 8 friendly at this moment" (you probably mean lambdas and streams), how about using StringJoiner:

public static String join(List<String> parts, String delimiter, String lastDelimiter) {
    StringJoiner joiner = new StringJoiner(delimiter, "", lastDelimiter);

    for (int i = 0; i < parts.size() - 1; i++) {
        joiner.add(parts.get(i));
    }

    return joiner.toString() + parts.get(parts.size() - 1);
}

However, doing the same with streams:

public static String join(List<String> parts, String delimiter, String lastDelimiter) {
    return parts.stream()
            .limit(parts.size() - 1)
            .collect(Collectors.joining(delimiter, "", lastDelimiter))
            .concat(parts.get(parts.size() - 1));
}

EDIT: Just found String#join(CharSequence, Iterable<? extends CharSequence>):

public static String join(List<String> parts, String delimiter, String lastDelimiter) {
    return String.join(delimiter, parts.subList(0, parts.size() - 1)) 
            + lastDelimiter + parts.get(parts.size() - 1);
}

In order to handle corner cases I'd go for Xaerxess switch solution.

Community
  • 1
  • 1
beatngu13
  • 7,201
  • 6
  • 37
  • 66
  • Unfortunately, `StringJoiner` is also available only in Java 8+. – Grzegorz Rożniecki Jul 27 '16 at 07:23
  • @Xaerxess: I know, that's why I said "you probably mean lambdas and streams". I thought (and still think) the OP was talking about the different syntax/coding style that comes with them. Of course, if he doesn't wants to (or can't) use JDK 8 at all, that's not a valid solution. – beatngu13 Jul 27 '16 at 07:32
2

Java 6/7 compatible solution using Guava:

static String join(List<String> strings, String separator, String lastSeparator)
{
    checkNotNull(strings);
    switch (strings.size()) {
        case 0:
            return "";
        case 1:
            return strings.get(0);
        default:
            return Joiner.on(separator).join(strings.subList(0, strings.size() - 1))
                    + lastSeparator + strings.get(strings.size() - 1);
    }
}

or if you want to accept Iterable (and / or use more Guava stuff):

static String join(Iterable<String> strings, String separator, String lastSeparator)
{
    checkNotNull(strings);
    int size = Iterables.size(strings);
    switch (size) {
        case 0:
            return "";
        case 1:
            return strings.iterator().next();
        default:
            return Joiner.on(separator).join(Iterables.limit(strings, size - 1))
                    + lastSeparator + Iterables.getLast(strings);
    }
}

It handles all edge cases except null elements in strings - if you want to handle them and not throw NPE, add .useForNull(stringForNull) to your joiner.

Grzegorz Rożniecki
  • 27,415
  • 11
  • 90
  • 112
1

Courtesy to Java having replaceFirst but no replaceLast, and assuming "_,_" will not appear in any of the elements/parts, I hacked the following function:

public static String join(Iterable<?> parts) {
    String reverse = new StringBuilder(Joiner.on("_,_").join(parts)).reverse().toString();
    reverse = reverse.replaceFirst("_,_", " dna ").replaceAll("_,_", " ,");
    return new StringBuilder(reverse).reverse().toString();
}

And then:

System.out.println(join(Arrays.asList("One", "Two", "Three", "Four")));

Gives: One, Two, Three and Four

Scott Mayers
  • 437
  • 6
  • 18