Reputation: 36179
This code:
Kid[] kids = Kid.getSimpleArray();
String names = Stream.of(kids)
.filter(Kid::hasToy)
.map(Kid::getSurname)
.collect(Collectors.joining(", "));
out.println(names);
gives me this result:
Stępień, Inglot, Czubówna, Lepiej, Łagowska
And I'm thinking of a solution to make it like this:
1.Stępień, 2.Inglot, 3.Czubówna, 4.Lepiej, 5.Łagowska
P.S I couldn't think of a better question to get answer I'm looking for.
Upvotes: 3
Views: 158
Reputation: 100199
Using third-party free library StreamEx (written by me) which enhances standard Stream API you can zip the stream with numbers:
String result = StreamEx.of("abc", "def", "aaa", "foo", "a12", "a34", "bar", "aaa")
.filter(s -> s.startsWith("a"))
.zipWith(IntStreamEx.ints().boxed(), (str, num) -> (num+1)+". "+str)
.joining(", ");
// 1. abc, 2. aaa, 3. a12, 4. a34, 5. aaa
Upvotes: 3
Reputation: 6471
In your example, after all the filtering is done, Strings are collected and joined. Since you want to end up with String values, we can create a version of toString()
(forgetting for the moment that your Stream already contains Strings) that prepends the count, like this:
public static<T> Function<T, String> getCountedToStringFunc(){
AtomicInteger i = new AtomicInteger(0);
return t -> i.incrementAndGet() + ". " + t.toString();
}
You can use it like this:
myStream
.filter(...)
.map(getCountedToStringFunc())
.collect(Collectors.joining(",")));
Or, noting that having to remember to apply the counter as late as possible in the pipeline creates a maintainability risk if you should want to modify the pipeline in the future, you could take it out of the pipeline altogether and make it part of the terminating function:
myStream
.filter(...)
.collect(Collectors.mapping(getCountedToStringFunc(),joining(","))));
The nice thing about the latter approach is you could use it to create separate counters for partitioned or grouped data.
Upvotes: 1
Reputation: 9944
Streams only operate on one item at once. Each computation knows only about the element it is operating on. So trying to add in some knowledge of the item's position in the ordering is always going to be a hack.
Collecting into a List
instead of a String would allow you to do a final computation that operates on the list of all the items, rather than on single items in isolation. Here's a method that takes the List
and formats it as a comma-separated String
of numbered items:
static String enumerateAndJoin(List<String> l, String separator) {
StringBuffer sb = new StringBuffer();
ListIterator<String> it = l.listIterator();
while(it.hasNext()) {
sb.append(it.nextIndex() + 1).append(".").append(it.next());
if (it.hasNext()) {
sb.append(separator);
}
}
return sb.toString();
}
You could of course then do:
List<String> surnames = Stream.of(kids)
.filter(Kid::hasToy)
.map(Kid::getSurname)
.collect(toList());
String output = enumerateAndJoin(surnames, ", ");
But if you're using this in several places, it might be convenient to define a new collector.
Collector<String, ?, String> myCollector = collectingAndThen(toList(), list -> enumerateAndJoin(list, ", "));
String output = Stream.of(kids)
.filter(Kid::hasToy)
.map(Kid::getSurname)
.collect(myCollector);
However, all this really serves to do is obscure the code and waste cycles. Ultimately we're now doing two passes over the items. And the 'hack' solutions that just do one pass work by treating the stream as if it is an iterator, counting how many items have been encountered. That approach will only work for very straightforward ordered streams, and isn't at all what streams are intended for.
The stream is certainly appealing for its fluent filter
and map
methods. But if you can bear a bit more verbosity, why not just iterate over kids
using a real iterator in the first place?
int i = 0;
Iterator<Kid> it = new ArrayIterator<>(kids);
StringBuffer sb = new StringBuffer();
while (it.hasNext()) {
Kid kid = it.next();
if (!kid.hasToy()) continue;
sb.append(++i).append(".").append(kid.getSurname());
if (it.hasNext()) {
sb.append(", ");
}
}
For a hybrid
approach that saves a line or two and is arguably clearer, I like Guava's FluentIterable
:
int i = 0;
Iterator<Kid> kidsWithToys = FluentIterable.of(kids).filter(Kid::hasToy).iterator();
StringBuffer sb = new StringBuffer();
while (kidsWithToys.hasNext()) {
sb.append(++i).append(".").append(kidsWithToys.next().getSurname());
if (kidsWithToys.hasNext()) {
sb.append(", ");
}
}
For some problems, streams are not the answer, and I think this is one of them.
Upvotes: 1
Reputation: 21975
Note that this is really ugly and not an advice I should give here but here it is.
You may use a temporary collection that will serve us as a counter. The advantage is that we can block the reference as final but still add elements to it.
We add whatever to it each time a Kid
passes the filter and get the size for the integer in your String.
final List tmp = new ArrayList();
Kid[] kids = Kid.getSimpleArray();
String names = Stream.of(kids)
.filter(Kid::hasToy)
.peek(x -> tmp.add(1))
.map(x -> tmp.size() + "." + x.getSurname())
.collect(Collectors.joining(", "));
out.println(names);
Here are two other solutions provided by @Sam and @Tunaki
final AtomicInteger ai = new AtomicInteger();
Kid[] kids = Kid.getSimpleArray();
String names = Stream.of(kids)
.filter(Kid::hasToy)
.map(x -> ai.incrementAndGet() + "." + x.getSurname())
.collect(Collectors.joining(", "));
out.println(names);
Kid[] kids = Kid.getSimpleArray();
Kid[] filteredKids = Arrays.stream(kids).filter(Kid::hasToy).toArray(Kid[]::new);
String names = IntStream.range(0, filteredKids.length)
.mapToObj(i -> i + "." + filteredKids[i].getSurname())
.collect(Collectors.joining(", ");
out.println(names);
Upvotes: 4