1 Replies - 8644 Views - Last Post: 11 November 2017 - 03:06 AM

#1 IOExceptional   User is offline

  • D.I.C Head
  • member icon

Reputation: 28
  • View blog
  • Posts: 141
  • Joined: 15-September 16

Ranges/Progressions in Java

Posted 05 September 2017 - 11:13 AM

I think I speak for all Java programmers when I say that the enhanced-for (or for-each, if you prefer) loop is great. It's simple, high-level, and does (mostly) what you want ... Unless what you want is to maintain an index throughout the iteration.

In those cases, you're forced to either use a traditional for loop or add another, sometimes fairly large, dependency to the project; all the while being silently -- or not so silently -- envious of the standard range functions of languages like Python.

Well, might never get them included into the JavaSE API (and no, I don't count IntStream.range(). Lambdas are great, but not always what you want), but in this snippet, I present to you a tertiary option. A set of three small classes. Copy them, stick them in a utility package, and you're good to go.

package xyz.ioexceptional.dreamincode.snippets.range;

import java.lang.reflect.Array;
import java.util.Iterator;

public class Range implements Iterable<Integer> {
   private static final int DEFAULT_STEP = 1;
   private static final int DEFAULT_START = 1;
   
   private final int start;
   private final int end;
   private final int step;
   
   private Range(int from, int to, int step) {
      this.start = from;
      this.end = to;
      this.step = step;
   }
   
   @Override
   public Iterator<Integer> iterator() {
      if (start > end) {
         return new ReverseRangeIterator(start, end, step);
      }
      return new RangeIterator(start, end, step);
   }
   
   public static Range range(int from, int to, int step) {
      return new Range(from, to, step);
   }
   
   public static Range range(int from, int to) {
      return range(from, to, DEFAULT_STEP);
   }
   
   public static Range range(int to) {
      return range(DEFAULT_START, to);
   }
   
   public static Range range(Object array) {
      return range(0, length(array) - 1);
   }
   
   public static Range reverseRange(Object array) {
      return range(length(array) - 1, 0);
   }
   
   private static int length(Object array) {
      return Array.getLength(array);
   }
}



This class represents the API of the three classes and doesn't do much work. It simply holds three variables which are later passed off to Iterators. I'll get to those in a bit. Yes, I know that, with the step value, this would technically be a progression, but "Range" just sounds better, alright?

For the sake of a nice API, I made the constructor private and provided several static factory methods to cater for the user's progression needs. I leave it as an exercise to the reader to add or remove them as needed. Said factory methods are named "range" so that the user can statically import the class while maintaining readability.

For those unfamiliar, I should mention that by range(Object) accept an Object, I removed the need for specialisations for the different types of primitive array. This comes at the cost of delving into the java.lang.reflect package, which I generally don't like to do, but since I only need to get the passed array's length, I decided that it was safe enough.

With that said, let's move on to the iterators.

package xyz.ioexceptional.dreamincode.snippets.range;

import java.util.Iterator;

class RangeIterator implements Iterator<Integer> {
   private final int end;
   private final int step;
      
   private int current;
      
   RangeIterator(int start, int end, int step) {
      this.current = start;
      this.end = end;
      this.step = step;
   }
      
   @Override
   public boolean hasNext() {
      return current <= end;
   }
   
   @Override
   public Integer next() {
      int old = current;
      current += step;
      return old;
   }
}



package xyz.ioexceptional.dreamincode.snippets.range;

import java.util.Iterator;

class ReverseRangeIterator implements Iterator<Integer> {
   private final int end;
   private final int step;
   
   private int current;
   
   ReverseRangeIterator(int start, int end, int step) {
      this.current = start;
      this.end = end;
      /* In case the user thinks a negative step is 
         necessary for a reversed range. */
      this.step = Math.abs(step);
   }
   
   @Override
   public boolean hasNext() {
      return current >= end;
   }
   
   @Override
   public Integer next() {
      int old = current;
      current -= step;
      return old;
   }
}



These two class do the heavy lifting, so to speak. Yes, I could have used just one, but a class that behaves very differently based on a single condition is -- in my opinion, at least -- evidence of a bad design. Feel free to change it in your implementation if you feel strongly enough about it.

The way RangeIterator works is quite simple really, every time the next() method is called, the 'current' value (starting at the start value given to the Range instance) is returned, then incremented by the given 'step' value. This is achieved by storing the 'old' value before 'current' is incremented. This could have been achieved with a try-finally construct. Something like this:
try {
   return current;
} finally {
   current += step;
}


However, I thought that the current version was more readable, and readability is far more important than one fewer variable. Again, feel free to change as needed.

The hasNext() implementation is even simpler. It simply checks if the 'current' value is less than or equal to the given 'end' value. I used '<=' because I thought that an 'inclusive' end would be more useful. Once again, change as needed.

The ReverseRangeIterator works the same way, but backwards.

Note that for both these iterators, in the interest of keeping things simple, there is no protection against infinite iteration. It is up to the user to provide good arguments. To make it a bit less dangerous, I restricted the class to package-private access. RangeIterators should only be created through calls to Range.iterator().

Finally, some examples. The classes can be used quite easily, and very much like their Pythonic equivalents.

A simple range from 1 to 5:
for (int i : range(5)) {
   /* Returns 1, 2, 3, 4, 5 */
}



A range over an array:
double[] doubles = {2.0, 3.0, 1.5};
for (int i : range(doubles)) {
   /* Returns 0, 1, 2 */
}



A range over an array, backwards:
double[] doubles = {2.0, 3.0, 1.5};
for (int i : reverseRange(doubles)) {
   /* Returns 2, 1, 0 */
}



And so on. Remember to add
import static xyz.ioexceptional.dreamincode.snippets.range.Range.range;
import static xyz.ioexceptional.dreamincode.snippets.range.Range.reverseRange;


to the top of the file, with "xyz.ioexceptional.dreamincode.snippets.range" substituted as necessary;


Aaaand that's it. Sorry for the long read, but I wanted to make the code and all design decisions clear to you. If you feel as though I've failed in that somehow or that I could have done something better (which is almost certainly true), feel free to mention it below.

Is This A Good Question/Topic? 1
  • +

Replies To: Ranges/Progressions in Java

#2 IOExceptional   User is offline

  • D.I.C Head
  • member icon

Reputation: 28
  • View blog
  • Posts: 141
  • Joined: 15-September 16

Re: Ranges/Progressions in Java

Posted 11 November 2017 - 03:06 AM

In case anyone is interested, I have since updated (and vastly improved, I think) the design and added some new features. I might do another write-up on it some time, but for now, you can check it out on GitHub. The basic idea is still the same.
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1