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?
2 Answers 2
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.
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