0

I have an ordered list of Strings, let's say "aaa", "aaa", "aaa", "bbb", "bbb", "aaa". I would like to group adjacent equal strings together and count them, so the result of the operation should be a List looking like this: {"aaa":3}, {"bbb":2}, {"aaa", 1}. Note that the task is not to just group by the same values and count them (otherwise I could simply use groupingBy with counting), but group by only adjacent strings with the same value, and if the same string appears later in the list then it should be considered as separate. Basically, I need this piece of code to create a table header with appropriate colspans out of an existing data structure.

I'd like to know if there's a reasonably good way to achieve this task using Java 8 streams. I know how to do it using an old-school loop, just thought maybe streams provide a better way.

Rostislav Titov
  • 21
  • 1
  • 1
  • 4

3 Answers3

1

You could create a custom collector as far as I can see, but it does not differ much then a loop (same logic would be applied). Also notice that I've used SimpleEntry instead of a Pair to hold each element from the List.

private static Collector<String, ?, List<AbstractMap.SimpleEntry<String, Integer>>> adiacentCollector() {

    class Acc {

        private String previous;

        private List<AbstractMap.SimpleEntry<String, Integer>> result = new ArrayList<>();

        void accumulate(String elem) {
            if (previous == null) {
                previous = elem;
                result.add(new AbstractMap.SimpleEntry<String, Integer>(elem, 1));
                return;
            }

            if (previous.equals(elem)) {
                SimpleEntry<String, Integer> current = result.get(result.size() - 1);
                current.setValue(current.getValue() + 1);
                previous = elem;
            } else {
                SimpleEntry<String, Integer> oneMore = new SimpleEntry<String, Integer>(elem, 1);
                result.add(oneMore);
                previous = elem;
            }
        }

        Acc combine(Acc other) {

            SimpleEntry<String, Integer> lastEntry = result.get(result.size() - 1);
            SimpleEntry<String, Integer> firstEntry = other.result.get(0);

            if (lastEntry.getKey().equals(firstEntry.getKey())) {
                lastEntry.setValue(lastEntry.getValue() + firstEntry.getValue());
                other.result.remove(0);
            }

            result.addAll(other.result);

            return this;

        }

        List<AbstractMap.SimpleEntry<String, Integer>> finisher() {
            return result;
        }

    }
    return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::finisher);
}

And use it:

 System.out.println(Stream.of("aaa", "aaa", "aaa", "bbb", "bbb", "aaa")
            .collect(adiacentCollector()));
Eugene
  • 117,005
  • 15
  • 201
  • 306
1

Here is the solution by the collapse API provided StreamEx

List<Map<String, Long>> res = StreamEx.of("aaa", "aaa", "aaa", "bbb", "bbb", "aaa")
        .collapse(Objects::equals, 
                  Collectors.groupingBy(Function.identity(), Collectors.counting()))
        .toList();

System.out.println(res); // output: [{aaa=3}, {bbb=2}, {aaa=1}]

Or even simpler with 'runLength':

List<Map.Entry<String, Long>> res2 = StreamEx.of("aaa", "aaa", "aaa", "bbb", "bbb", "aaa")
                                             .runLengths().toList();

System.out.println(res2); // output: [{aaa=3}, {bbb=2}, {aaa=1}]
123-xyz
  • 619
  • 4
  • 5
0

StreamEx is the best solution. But here another one using a closure and reducing the stream:

The closure:

public class StatefulList {
    private List<Pair<String, Integer>> pairList;
    private int index = 0;

    public StatefulList() {
        this.pairList = new ArrayList<>();
    }

    public StatefulList add(String value) {
        if (pairList.size()==0) {
            pairList.add(new Pair<>(value, 1));
            return this;
        }
        Pair<String, Integer> last = pairList.get(index);
        if(last.getKey().equals(value)){
            pairList.set(index, new Pair<>(last.getKey(), last.getValue() + 1));
        } else {
            pairList.add(++index, new Pair<>(value, 1));
        }
        return this;
    }

    public String toString() {
        return pairList.toString();
    }
}

Reducing it:

Stream.of("aaa", "aaa", "aaa", "bbb", "bbb", "aaa")
      .reduce(new StatefulList(), StatefulList::add, (a,b) -> a);
Rinor
  • 1,790
  • 13
  • 22