11
\$\begingroup\$

This recent question Print Consecutive numbers by comparing two parameters frustrated me because I could not find a convenient way in Java 8 to support the conditions that are required. I answered it in a somewhat cumbersome way for Java 8... (the non-stream part of my answer is "OK" in terms of usability).

To reiterate the problem (as I interpreted it):

print the values in the range from a to b, where b may be less than a. e.g. from 1 to 3 will print 1, 2, 3 and from 3 to 1 will print 3, 2, 1.

What I really wanted was something like this:

IntSteam.loop(a, b, a < b ? ++ : --).foreach(System.out::println);

(yes, the above code makes no sense, treat it as pseudocode...).

Basically, stream from a value to another value, either incrementing, or decrementing the loop variable.

To make a real implementation of that, I figured the stream needs a seed, a terminal condition, and a stepping function. I implemented it like:

    int a = 10;
    int b = 5;
    IntUnaryOperator op = a < b
            ? i -> i + 1
            : i -> i - 1;
    ForIntStream.until(a, v -> v == b, op).forEach(System.out::println);

The semantics there are: start from a, go until a == b, and use the operator to change the value.

While I was doing that, I also implemented a similar loop, that, instead of running until a condition, runs while a condition is true:

    int a = 10;
    int b = 5;
    IntUnaryOperator op = a < b
            ? i -> i + 1
            : i -> i - 1;
    ForIntStream.of(a, v -> v != b, op).forEach(System.out::println);

Note that the difference there is that the 'of' stream terminates and does not include the terminating.

Here is the code that implements the above features. Any suggestions, or alternatives are welcome

import java.util.NoSuchElementException;
import java.util.PrimitiveIterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.function.IntUnaryOperator;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

public class ForIntStream {

    private static final class WhileIterator implements PrimitiveIterator.OfInt {

        private int next;
        private final IntPredicate proceed;
        private final IntUnaryOperator step;
        private boolean terminated;

        public WhileIterator(int next, IntPredicate proceed, IntUnaryOperator step) {
            this.next = next;
            this.proceed = proceed;
            this.step = step;
            this.terminated = !proceed.test(next);
        }

        @Override
        public void forEachRemaining(IntConsumer action) {
            while (!terminated) {
                action.accept(next);
                next = step.applyAsInt(next);
                terminated = !proceed.test(next);
            }
        }

        @Override
        public boolean hasNext() {
            return !terminated;
        }

        @Override
        public Integer next() {
            return nextInt();
        }

        @Override
        public int nextInt() {
            if (terminated) {
                throw new NoSuchElementException("Iterated beyond terminal condition.");
            }
            int ret = next;
            next = step.applyAsInt(next);
            terminated = !proceed.test(next);
            return ret;
        }

    }

    private static final class UntilIterator implements PrimitiveIterator.OfInt {

        private int next;
        private final IntPredicate until;
        private final IntUnaryOperator step;
        private boolean terminated;

        public UntilIterator(int next, IntPredicate until, IntUnaryOperator step) {
            this.next = next;
            this.until = until;
            this.step = step;
        }

        @Override
        public void forEachRemaining(IntConsumer action) {
            while (!terminated) {
                action.accept(next);
                if (until.test(next)) {
                    terminated = true;
                } else {
                    next = step.applyAsInt(next);
                }
            }
        }

        @Override
        public boolean hasNext() {
            return !terminated;
        }

        @Override
        public Integer next() {
            return nextInt();
        }

        @Override
        public int nextInt() {
            if (terminated) {
                throw new NoSuchElementException("Iterated beyond terminal condition.");
            }
            int ret = next;
            terminated = !until.test(next);
            next = step.applyAsInt(next);
            return ret;
        }

    }

    public static IntStream of(int seed, IntPredicate allow, IntUnaryOperator step) {
        PrimitiveIterator.OfInt it = new WhileIterator(seed, allow, step);
        return StreamSupport.intStream(Spliterators.spliterator(it, Long.MAX_VALUE, Spliterator.ORDERED), false);
    }

    public static IntStream until(int seed, IntPredicate terminator, IntUnaryOperator step) {
        PrimitiveIterator.OfInt it = new UntilIterator(seed, terminator, step);
        return StreamSupport.intStream(Spliterators.spliterator(it, Long.MAX_VALUE, Spliterator.ORDERED), false);
    }

}

if you want to experiment with it, the following will give some hints:

    public static void main(String[] args) {
        ForIntStream.of(1, i -> i != 10, i -> i + 1).forEach(System.out::println);
        ForIntStream.until(1, i -> i == 10, i -> i + 1).forEach(System.out::println);

        int a = 10;
        int b = 5;
        IntUnaryOperator op = a < b
                ? i -> i + 1
                : i -> i - 1;
        ForIntStream.until(a, i -> i == b, op).forEach(System.out::println);

    }

For another example of how this can be used... to print a collatz conjecture sequence (hailstone problem), you can stream like:

ForIntStream.until(10,
             i -> i == 1,
             i -> i % 2 == 0 ? i / 2 : (3 * i + 1) )
         .forEach(System.out::println);
\$\endgroup\$
5
\$\begingroup\$
ForIntStream.of(1, i -> i != 10, i -> i + 1).forEach(System.out::println);
ForIntStream.until(1, i -> i == 10, i -> i + 1).forEach(System.out::println);

This is going to be subjective, but I'll just put in my two cents anyways...

Looking at the statements above, I will roughly read them as

  1. (starting with index) of 1, (loop while) i != 10, (and increment by) i + 1
  2. until 1... what?

I'll suggest switching the first two parameters for until(), which I think can improve its readability in this way:

ForIntStream.until(i -> i == 10, 1, i -> i + 1).forEach(System.out::println);
  1. until i == 10, (starting with index of) 1, (and increment by) i + 1
\$\endgroup\$
  • 2
    \$\begingroup\$ Your suggestion is so... obvious. I was trying to match the order of a for loop, but you are right, this ordering is so much better for these names \$\endgroup\$ – rolfl May 14 '15 at 10:10
12
\$\begingroup\$

After reading the problem description and before reading your code, I implemented something that was very similar to what you already have, which means that your approach to the problem is perfectly fine.

Spliterator - of unknown size

Instead of using

Spliterators.spliterator(it, Long.MAX_VALUE, Spliterator.ORDERED)

You should use

Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED)

Unnecessary method

The default implementation in the super interface for next() is:

@Override
default Integer next() {
    if (Tripwire.ENABLED)
        Tripwire.trip(getClass(), "{0} calling PrimitiveIterator.OfInt.nextInt()");
    return nextInt();
}

I really don't think you gain anything by making your own implementation of this method.

Additionally, your WhileIterator's forEachRemaining method can be removed and the default implementation used instead without any change in behavior.

Naming

I find the method ForIntStream.of to be hard to understand by looking at the name alone. Of what? Of a seed, a predicate and a unary operator? How about something like whilePredicate ?

Perfect for tests

Although using System.out.println to read the result and manually check it works perfectly fine in this case, real tests wouldn't hurt. </nitpick>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.