Paul Grime
Paul Grime

Reputation: 15104

Combining Streams of substrings

I have the following code that iterates over Java DateTimeFormatter pattern combinations of "E".."EEEE" and "M".."MMMM".

My question is, is there an idiomatic (or just 'more idiomatic') way to use Java Streams in this case?

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class DateTimeFormattingStackOverflow {
    static LocalDateTime dateTime = LocalDateTime.now();

    static Stream<String> substrings(String str) {
        return IntStream.range(1, str.length() + 1)
                .mapToObj(i -> str.substring(0, i));
    }

    static void printDateTime(String pattern) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
        System.out.println(pattern + ", " + dtf.format(dateTime));
    }

    public static void main(String[] args) {
        Stream<String> patterns = substrings("EEEE")
                .flatMap(e -> substrings("MMMM").map(m -> e + " " + m))
                .map(em -> em + " d");

        patterns.forEach(DateTimeFormattingStackOverflow::printDateTime);
    }
}

Output

E M d, Sat 7 1
E MM d, Sat 07 1
E MMM d, Sat Jul 1
E MMMM d, Sat July 1
EE M d, Sat 7 1
EE MM d, Sat 07 1
EE MMM d, Sat Jul 1
EE MMMM d, Sat July 1
...

Upvotes: 9

Views: 816

Answers (4)

Holger
Holger

Reputation: 298143

I wouldn’t stream over substrings at all. It’s enough to stream over something representing the sixteen combinations and to construct the format string in one operation:

Stream<String> patterns = IntStream.range(0, 16).map(i -> 15-i)
    .mapToObj(i -> "EEEE".substring(i>>2)+" "+"MMMM".substring(i&3)+" d");

The fact that these are four times four combinations makes the calculation eligible for cheap bit arithmetic, but in principle, any n×m combination is possible by streaming from 0 to n×m and using i/n and i%n for selecting the element (like performing the substring operation). The preceding .map(i -> 15-i) step just reverses the order to match it with your original code; you can omit it if the order doesn’t matter.

Upvotes: 0

fps
fps

Reputation: 34460

You are using an IntStream to drive the string. That's one way to do it, and here are two other ways:

static Stream<String> substrings(String str) {
    return str.length() == 1 ? Stream.of(str) :
            Stream.concat(Stream.of(str), substrings(str.substring(1)));
}

This creates a Stream recursively, while the other way would be as follows:

static Stream<String> substrings2(String str) {
    return Stream.iterate(str, s -> s.substring(1)).limit(str.length());
}

This applies the given function to the previous result. As it creates an infinite stream, you have to use limit.

I have slightly modified your main method, so that you avoid one map operation:

substrings("EEEE")
    .flatMap(e -> substrings("MMMM").map(m -> e + " " + m + " d"))
    .forEach(DateTimeFormattingStackOverflow::printDateTime);

I really don't know if the ways above are more or less idiomatic than your way, but if you ask me, the most idiomatic way to do this task is with a nested loop:

String e = "EEEE";
String m = "MMMM";

for (int i = 0; i < e.length(); i++)
    for (int j = 0; j < m.length(); j++)
        printDateTime(e.substring(i) + " " + m.substring(j) + " d");

And this might be translated to Java 8 as follows:

IntStream.range(0, e.length()).boxed()
    .flatMap(i -> IntStream.range(0, m.length())
        .mapToObj(j -> e.substring(i) + " " + m.substring(j) + " d"))
    .forEach(DateTimeFormattingStackOverflow::printDateTime);

Upvotes: 1

Eugene
Eugene

Reputation: 120848

I don't know if that is more idiomatic, but I would squeeze that to a single method (it could be just me...)

 public static Stream<String> combinations(String first, String second, String d, LocalDateTime dateTime) {

        Stream<String> s = IntStream.range(0, first.length())
                .mapToObj(i -> first.substring(0, i + 1))
                .flatMap(x -> IntStream.range(0, second.length())
                        .mapToObj(i -> second.substring(0, i + 1))
                        .map(m -> String.join(" ", x, m))
                        .map(res -> res + " d")
                        .peek(System.out::print)
                        .map(DateTimeFormatter::ofPattern)
                        .map(dtf -> dtf.format(dateTime)));
        return s;
    }

   combinations("EEEE", "MMMM", " d", LocalDateTime.now()).forEach(x -> System.out.println(", " + x));

Upvotes: 0

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726509

I would change the way in which you generate repeated runs of a character: rather than constructing the longest string manually, and then iterating its prefixes, I would construct runs with Collection.nCopies, like this:

static Stream<String> repeatedRuns(String c, int start, int end) {
    return IntStream
        .rangeClosed(start, end)
        .mapToObj(len -> 
            Collections.nCopies(len, c).stream().collect(Collectors.joining(""))
        );
}

You would then replace substrings("EEEE") with repeatedRuns("E", 1, 4).

Demo.

Upvotes: 0

Related Questions