2
\$\begingroup\$

I'm creating a human-readable list from the keys of a map object. I want it to be comma-separated, but include the word "and" at the end before the last element, supplying a given locale for the word "and".

E.g., my map might have the strings:

"AK", "Alaska"
"CA", "California"
"NY", "New York"

I would want as output:

Alaska, California, and New York

for English and

Alaska, California, y New York

for Spanish.

So I have:

public static String getHumanReadableStates(Map<String, String> stateMap) {

    long count = stateMap.values().stream().map(Object::toString).count();
    return stateMap.values().stream().map(Object::toString)
            .limit(count - 1)
            .collect(Collectors.joining(", "))
            .concat(", ")
            .concat(LocaleUtil.getBundleValue("theWordAnd"))
            .concat(" ")
            .concat(Objects.requireNonNull(stateMap.values().stream().reduce((first, second) -> second)
                    .orElse(null)));
}

where LocaleUtil has a method called getBundleValue() to simply get the word "and" from messages.properties.

Is there a simpler way to achieve this? Or should streams be avoided altogether?

\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

A stream-based approach is still feasible, by materializing your state names into a List first.

By using a List, you can retrieve all the names except the last using List.subList(int, int) (this may be empty, for a single-entry Map), and then retrieving the last (or the only, for a single-entry Map) directly using a simple List.get(int). Afterwards, you just need to handle for an empty Map or String values.

public static String getHumanReadableStates(Map<String, String> stateMap) {
    List<String> names = new ArrayList<>(stateMap.values());
    if (names.isEmpty()) {
        return "";
    }
    String exceptLastPlusBlank = Stream.concat(
            names.subList(0, names.size() - 1).stream(), Stream.of(""))
            .collect(Collectors.joining(", "));
    return Stream.of(exceptLastPlusBlank, names.get(names.size() - 1))
            .filter(v -> !v.isEmpty())
            .collect(Collectors.joining(LocaleUtil.getBundleValue("theWordAnd") + " "));
}

Stream.of("") is introduced to append the final comma.

\$\endgroup\$
1
\$\begingroup\$

To get the different behaviour for the final element, you will need to be able to tell how many items are left in the list before you hit the end, so that seems to rule out streams, since you only have access to one element at a time, and the "built in" joining collector doesn't appear to allow any special treatment for the final element.

So really, you're reduced to StringBuilder and the logic (pseudocode):

  • if (have any elements at all)
    • add first element to string
    • while (have more than one element remaining)
    • add delimiter (", " I guess), then the next element from the list
    • add " and " (or translation), then the final element from the list

Or, for slightly more readable code [citation needed], still using streams a bit, take the list of values, and:

  • if (have any elements at all)
    • remove final element and keep safe somewhere
    • if (any elements remaining)
    • apply .stream().collect(Collectors.joining(", ")) to list
    • concatenate with " and " (or translation) and the saved final element, then return it
    • else, just return the saved final element
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.