Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 reduce to 2 strings

Can I do this with streams?

StringBuilder text = new StringBuilder();
StringBuilder dupText = new StringBuilder();
String lastLetter = "";

for (Container cont : containersList) {
    String letter = cont.getLetter();
    text.append(letter);
    if (letter.equals(lastLetter) == false) {
        dupText.append(letter);
    }
    lastLetter = letter;
}

System.out.println(text);
System.out.println(dupText);

I go over list of continers, each one has a char. I need to assemble two strings - one is all the chars combine, and the other one is all the chars but without coupled duplicates (ABABAAAB -> ABABAB)

Can this be done with streams?

I tried doing it like this:

Optional<String> text = containersList.stream()
            .map(Container::getLetter)
            .reduce((letter,accumalator) -> accumalator += letter);

Optional<String> dupText = session.containersList().stream()
            .map(Container::getLetter)
            .reduce((letter, accumalator) ->{
                if ((accumalator.endsWith(letter) == false)) {
                    accumalator += letter;
                }
                return accumalator;
            });
like image 753
Ido Barash Avatar asked Dec 14 '22 09:12

Ido Barash


2 Answers

Using StreamEx library

You can do this in a single Stream pipeline using the StreamEx library.

List<Container> containersList = Arrays.asList(new Container("A"), new Container("B"), new Container("A"), new Container("A"), new Container("B"));
    
String[] result =
        StreamEx.of(containersList)
                .map(Container::getLetter)
                .groupRuns(Object::equals)
                .collect(MoreCollectors.pairing(
                    MoreCollectors.flatMapping(List::stream, Collectors.joining()),
                    MoreCollectors.mapping(l -> l.get(0), Collectors.joining()),
                    (s1, s2) -> new String[] { s1, s2 }
                ));
    
System.out.println(result[0]);
System.out.println(result[1]);

This code creates a Stream of the containers and maps each of those to their letter.

Then, the method groupRuns collapses into a List the successive elements that matches the given predicate. In this case, the predicate is the equality of the String: so if you start with the stream [A, A, B], this method will collapse it into the Stream [List(A, A), List(B)] (the first element is the list of 2 A successive elements in the input).

Finally, this is collected with the pairing collector that allows to collect into two different collector. The first one joins the flat map result of each list while the second one joins only the first element of the list (hence removing the successive elements).

The result is stored inside an array which just serves as a holder for two values.

Output:

ABAAB
ABAB

Using the Stream API directly

If you want to stay with the current API and not using a library, your best bet would be to write a custom Collector:

public static void main(String[] args) {
    List<Container> containersList = Arrays.asList(new Container("A"), new Container("B"), new Container("A"), new Container("A"), new Container("B"));
    
    String[] result = containersList.stream().parallel().map(Container::getLetter).collect(ContainerCollector.collector());
    
    System.out.println(result[0]);
    System.out.println(result[1]);
}

private static final class ContainerCollector {
    
    private StringBuilder text = new StringBuilder();
    private StringBuilder dupText = new StringBuilder();
    
    private void accept(String letter) {
        text.append(letter);
        if (dupText.indexOf(letter, dupText.length() - letter.length()) < 0) {
            dupText.append(letter);
        }
    }
    
    private ContainerCollector combine(ContainerCollector other) {
        text.append(other.text);
        other.dupText.codePoints().forEach(i -> {
            String letter = new String(Character.toChars(i));
            if (dupText.indexOf(letter, dupText.length() - letter.length()) < 0) {
                dupText.append(letter);
            }
        });
        return this;
    }
    
    private String[] finish() {
        return new String[] { text.toString(), dupText.toString() };
    }
    
    private static Collector<String, ?, String[]> collector() {
        return Collector.of(ContainerCollector::new, ContainerCollector::accept, ContainerCollector::combine, ContainerCollector::finish);
    }
    
}

This custom collector builds the text and dupText when each letter is accepted. For the text String, the letter is always appended. For the dupText, the letter is only appended if the last one is different.

The combiner code (ran in case of parallel execution) is a bit tricky for the dupText: the second one is appended if it does not start with the end of the first one. Otherwise, the first letter is dropped and the rest is appended.

The output is the same.

like image 184
Tunaki Avatar answered Jan 02 '23 09:01

Tunaki


I would make it in two separate operations. First, to get the text with duplicates:

String dupText = containersList.stream()
        .map(Container::getLetter)
        .collect(Collectors.joining());

And the second to remove the duplicates using the regexp:

String text = dupText.replaceAll("(.)\\1+", "$1");

While it's technically two-pass solution, it does not traverse input container twice and, I believe, it should be quite fast, at least not slower than other proposed solutions. And it's simple and does not require third-party libraries.

like image 28
Tagir Valeev Avatar answered Jan 02 '23 08:01

Tagir Valeev