A Reusable Range Implementation

Ranges are whatever lies between two values (lower and upper bound). Ranges are very common in all application maybe with different names like intervals (numbers), or periods (dates). Since all ranges need similar operations, why reinvent the wheel? Here are some generic concepts and a practical Java implementation.

Ranges are a key ingredient of any control application, from health care (temperature, blood pressure) to business intelligence (KPI monitoring). Unhappily we seldom think in terms of independent values instead of a range, for example upper and lower limit. Using a range has many advantages like a cleaner interface (a single field), avoid duplicate validation (lower bound <= upper bound ...) and simplify implementation.

Curiously, I never found a generic implementation of range concept in Java, so (time ago) I decided to write one (see below) that has worked perfectly for years. Other specific implementation exist (for example date ranges) but whenever you need a different type (for example a decimal number) you shall write new code.

In fact most operations are common to all ranges, for example check if they contain a value (is patient temperature in fever range?) getting their intersection or check if they overlap. Ranges can also be left or right open (no lower or upper bound) what makes their implementation a little trickier. Next figure shows some typical operations on ranges.

And here is the Java immutable and generic Range type supporting partially and totally bound ranges, the special empty range, usual operations, parsing and formatting. Alternatively you can download the Java class here. A small demo is included (just execute this class as Main class).

Range.java

import java.text.Format;
import java.text.ParseException;

/**
 * An immutable class to handle ranges of comparable values, like date ranges
 * and numeric ranges. A demo main() method is provided to show usage (see at
 * bottom).
 *
 * @author Franco Graziosi
 * @see 
 */
public final class Range> {

  private final E lowerBound;
  private final E upperBound;
  private final boolean empty;

  /**
   * Construct an empty range.
   */
  public Range() {
    empty = true;
    lowerBound = upperBound = null;
  }

  /**
   * Build a range given its bounds.
   * 
   * @param lowerBound
   *          Minimum value or null if unbound on bottom
   *          (left-open).
   * @param upperBound
   *          Maximum value or null if unbound on top (right-open).
   */
  public Range(E lowerBound, E upperBound) {
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.empty = isBoundConflict();
  }

  /**
   * Construct a range given a String representation and a format to parse bounds.
   * Bounds shall be separated by "..", for example "1..2". Open ranges are also
   * supported, for example "3.." or "..JAN/25/2010".
   * 
   * @param s
   *          The string to be parsed.
   * @param fmt
   *          The format to be used, if, for example it is a data range, you
   *          shall provide a date format.
   * @throws ParseException
   */
  public Range(String s, Format fmt) throws ParseException {
    int i = s.indexOf("..");
    if (i == -1)
      throw new IllegalArgumentException("Bad Range syntax: " + s);
    String sLow = s.substring(0, i).trim();
    String sHi = s.substring(i + 2).trim();
    lowerBound = parseValue(sLow, fmt);
    upperBound = parseValue(sHi, fmt);
    this.empty = isBoundConflict();
  }

  /**
   * Determine if value is contained within the range.
   */
  public boolean contains(E value) {
    if (isEmpty())
      return false;
    if (getUpperBound() != null && value.compareTo(getUpperBound()) > 0)
      return false;
    if (getLowerBound() != null && value.compareTo(getLowerBound()) < 0)
      return false;
    return true;
  }

  /**
   * Determine if this range fully contains another range.
   * 
   * @param r
   *          Another range
   * @return true if all values of r are contained within
   *         this interval
   */
  public boolean contains(Range r) {
    if (isEmpty())
      return r.isEmpty();
    else if (r.isEmpty())
      return true;
    if (getLowerBound() != null) {
      if (r.getLowerBound() == null)
        return false;
      else if (r.getLowerBound().compareTo(getLowerBound()) < 0)
        return false;
    }
    if (getUpperBound() != null) {
      if (r.getUpperBound() == null)
        return false;
      else if (r.getUpperBound().compareTo(getUpperBound()) > 0)
        return false;
    }
    return true;
  }

  /**
   * Format the range using a given mask.
   */
  public Object format(Format format) {
    if (isEmpty())
      return "";
    return (getLowerBound() == null ? "" : format.format(getLowerBound()))
        + " .. "
        + (getUpperBound() == null ? "" : format.format(getUpperBound()));
  }

  /**
   * @return lower bound or null if there is no lower bound.
   */
  public E getLowerBound() {
    return lowerBound;
  }

  /**
   * @return upper bound or null if there is no lower bound.
   */
  public E getUpperBound() {
    return upperBound;
  }

  /**
   * Determine how much two ranges intersect (overlap). For example the overlap
   * between range 1..5 and 4..9 is 4..5.
   * 
   * @param other
   *          Another range
   * @return The overlapping range or null if ranges do not
   *         overlap.
   */
  public Range intersect(Range other) {
    if (isEmpty())
      return this;
    else if (other.isEmpty())
      return other;
    E low = max(lowerBound, other.lowerBound, false);
    E high = min(upperBound, other.upperBound, false);
    return new Range(low, high);
  }

  /**
   * Determine if range is bound. A range is bound if it is not empty and both
   * lower and upper bounds are defined.
   */
  public boolean isBound() {
    return !isEmpty() && getLowerBound() != null && getUpperBound() != null;

  }

  private boolean isBoundConflict() {
    if (lowerBound != null && upperBound != null
        && lowerBound.compareTo(upperBound) > 0)
      return true;
    return false;
  }

  /**
   * Determine if range is degenerate. A range is degenerate when it contains a
   * single value.
   */
  public boolean isDegenerate() {
    return isBound() && getLowerBound().equals(getUpperBound());
  }

  /**
   * Determine if the range is empty. An empty range contains no values.
   */
  public boolean isEmpty() {
    return empty;
  }

  /**
   * Determine if lower bound is defined.
   */
  public boolean isLowerBounded() {
    return lowerBound != null;
  }

  /**
   * Determine if two ranges are overlapping.
   */
  public boolean isOverlapping(Range r2) {
    return !intersect(r2).isEmpty();
  }
  
  /**
   * Determine if upper bound is defined.
   */
  public boolean isUpperBounded() {
    return upperBound != null;
  }

  /**
   * Join two ranges and anything between them, for example 1..4 joined with
   * 2..7 give 1..7 and 1..4 joinde with 9..12 gives 1..12.
   * 
   * @param other
   *          the other range
   * @return a range corresponding to the joined ranges.
   */
  public Range join(Range other) {
    if (isEmpty())
      return other;
    else if (other.isEmpty())
      return this;
    E low = min(lowerBound, other.lowerBound, true);
    E high = max(upperBound, other.upperBound, true);
    return new Range(low, high);
  }

  private E max(E x1, E x2, boolean keepUndefined) {
    if (x1 == null)
      return keepUndefined ? x1 : x2;
    else if (x2 == null)
      return keepUndefined ? x2 : x1;
    if (x1.compareTo(x2) >= 0)
      return x1;
    return x2;
  }

  private E min(E x1, E x2, boolean keepUndefined) {
    if (x1 == null)
      return keepUndefined ? x1 : x2;
    else if (x2 == null)
      return keepUndefined ? x2 : x1;
    if (x1.compareTo(x2) <= 0)
      return x1;
    return x2;
  }

  @SuppressWarnings("unchecked")
  private E parseValue(String s, Format fmt) throws ParseException {
    if (s.length() == 0)
      return null;
    E result = (E) fmt.parseObject(s);
    return result;
  }

  public String toString() {
    if (isEmpty())
      return "empty";
    return (getLowerBound() == null ? "" : getLowerBound()) + " .. "
        + (getUpperBound() == null ? "" : getUpperBound());
  }

  // Demonstration code follows
  
  private static class Demo {
    private Range threeToSeven = new Range(3, 7);

    private void run() {

      Range oneToFive = new Range(1, 5);
      Range oneToEleven = new Range(9, 11);
      Range upToFifteer = new Range(null, 15);
      Range fourAndUp = new Range(4, null);
      Range infiniteRange = new Range((Integer) null,
          (Integer) null);
      Range emptyRange = new Range();
      Range degenerate = new Range(9, 9);

      show(oneToFive);
      show(oneToEleven);
      show(upToFifteer);
      show(fourAndUp);
      show(infiniteRange);
      show(emptyRange);
      show(degenerate);
      show(threeToSeven);

    }

    private void show(Range r2) {
      System.out.println("______________________");
      System.out.println(threeToSeven + " intersect(" + r2 + ") = "
          + threeToSeven.intersect(r2));
      System.out.println(threeToSeven + " isOverlapping(" + r2 + ") = "
          + threeToSeven.isOverlapping(r2));
      System.out.println(threeToSeven + " join(" + r2 + ") = "
          + threeToSeven.join(r2));
      System.out.println("isBound(" + r2 + ") = " + r2.isBound());
      System.out.println("isDegenerate(" + r2 + ") = " + r2.isDegenerate());
    }
  }

  public static void main(String[] args) {
    (new Demo()).run();
  }
  
}

Note: I decided to use the double dot notation between bounds, so 1..12 means 1 to 12, 7.. means 7 or more, ..15 means up to 15 and just .. means an infinite range. Empty range prints as “empty”. Different from math notation (using braces) but more clear and simple to type for end user.

In next post I will show how ranges can be conveniently used to define control intervals instead of high /low pairs commonly used in dashboards. This is part of my goal to show practical and effective business intelligence and analytic techniques to consultants and developers. Stay tuned by subscribing my mailing list!

Last modified on 2011-05-23 by Administrator