IntStream factors(int n) {
    return IntStream.rangeClosed(1, n)
        .filter(x -> n % x == 0);
}

boolean isPrime(int n) {
    return n > 1 && IntStream.range(2, n)
        .noneMatch(x -> n % x == 0);
}

IntStream primeFactors(int n) {
    return factors(n).filter(x -> isPrime(x));
}

int countPrimeFactors(int n) {
    return primeFactors(n).reduce(0, (x, y) -> x + 1);
}

IntStream omega(int n) {
    return IntStream.rangeClosed(1, n)
        .map(x -> countPrimeFactors(x));
}

jshell> IntStream dot(int a, int b) {
...> return IntStream.rangeClosed(a, b)
...> .flatMap(i -> IntStream.rangeClosed(a, b)
...> .map(j -> i * j));
...> }
| replaced method dot(int,int)

Inner map creates 1, 2, 3 which is multiplied with i = 1 at round 1. Repeat this for i = 2 and i = 3. You create a [Stream(1,2,3), Stream(2,4,6), Stream(3,6,9)] Which you can then flatMap to give you [1, 2, 3, 2, 4, 6, 3, 6, 9]


jshell> Stream<IntPair> product(int a, int b) {
...> return IntStream.rangeClosed(a, b)
...> .boxed()
...> .flatMap(i -> IntStream.rangeClosed(a, b)
...> .mapToObj(j -> new IntPair(i, j)));
...> }
| created method product(int,int)

*.boxed() converts a primitive stream (IntStream, LongStream, DoubleStream) into an object stream (Stream<T> which can hold objects like Integer, String but only one type.

IntStream.of(1, 2, 3)          // IntStream of primitives
         .boxed()               // Stream<Integer> of objects

*.mapToObj() allows you to map something to an object. If you use regular .map() on an IntStream, it expects you to return another int, keeping it as a IntStream.

IntStream.rangeClosed(1, 3)
         .mapToObj(j -> new IntPair(i, j))  // int -> IntPair object
// Here, int is mapped as an argument to create a new IntPair object.

jshell> Stream<Integer> fibonacci(int n) {
...> return Stream.iterate(new IntPair(1, 1),
...> pr -> new IntPair(pr.snd(), pr.fst() + pr.snd()))
...> .map(pr -> pr.fst())
...> .limit(n);
...> }
| created method fibonacci(int)
jshell> fibonacci(10).toList()
$.. ==> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

.limit(n) stops the stream after n elements

pr -> new IntPair(pr.snd(), pr.fst() + pr.snd())

This is the function that generates the **next** pair from the current pair `pr`:
- new `fst` = old `snd`
- new `snd` = old `fst` + old `snd`

So the pairs evolve like:
(1, 1)  ->  (1, 2)  ->  (2, 3)  ->  (3, 5)  ->  (5, 8) ...