blob: e9a58fe951ce7baaa4f80ee9e312d108162d419d [file] [log] [blame]
/*
# Licensed Materials - Property of IBM
# Copyright IBM Corp. 2015, 2016
*/
package quarks.samples.apps;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Comparator;
import java.util.Objects;
/**
* A range of values and a way to check a value for containment in the range.
* <p>
* Useful in filtering in predicates. This is a lightweight implementation
* of a subset of the Guava Range API.
* <p>
* e.g.
* <pre>{@code
* Range.open(2,4).contains(2); // returns false
* Range.closed(2,4).contains(2); // returns true
* Range.atLeast(2).contains(2); // returns true
* Range.greaterThan(2).contains(2); // returns false
* Range.atMost(2).contains(2); // returns true
* Range.lessThan(2).contains(2); // returns false
*
* String s = Range.closed(2,4).toString();
* Range<Integer> range = Range.valueOf(s, Integer.class);
*
* TStream<Integer> intStream = ...;
* TStream<Integer> filtered = intStream.filter(tuple -> !range.contains(tuple));
*
* TStream<JsonObject> jStream = ...;
* TStream<JsonObject> filtered = jStream.filter(json -> !range.contains(json.getProperty("reading").asInteger());
* }</pre>
*
* <p>
* Compared to Guava Range:
* <ul>
* <li>Guava Range doesn't mention any constraints with respect to types for {@code <T>}.
* This Range currently supports: Integer,Long,Short,Byte,Float,Double,BigInteger,BigDecimal.
* <br>String and Character, of questionable value to Quarks apps,
* are avoided at this time due to the Guava toString()/from-string
* items noted below.</li>
* <li>Guava Range doesn't support unsigned type ranges.
* <br>This Range adds {@link #contains(Comparable, Comparator)}.
* <li>Guava Range lacks a "Range from Range.toString()" function: https://github.com/google/guava/issues/1911.
* <br>This Range adds {@link #valueOf(String, Class) valueOf}.
* Possibly consider: (a) migrating this to a new Ranges class,
* (b) introducing convenience forms - e.g., {@code valueOfInteger(String)}.</li>
* <li>Guava Range has issues with to/from Json with Gson:
* https://github.com/google/guava/issues/1911.
* <br>This Range works but as doc'd by Gson, use {@code Gson#fromJson(String, java.lang.reflect.Type)} Don't know about this Range :-)</li>
* <li>Guava Range's {@code apply(T value)} is documented as deprecated
* so this Range does not "implement Predicate".
* <li>Guava Range.toString()
* <ul>
* <li> Guava uses some unprintable characters.
* Up to the latest Guava release - 19.0, Range.toString() uses \u2025 for
* the ".." separator and uses +/-\u221E for infinity. That's caused problems:
* https://github.com/google/guava/issues/2376.
* Guava Range.toString() has been change to use ".." instead of \u2025.
* It still uses the unicode char for infinity.
* <br>This Range uses ".." for the separator like the not-yet-released Guava change.
* For convenience to users, this Range uses "*" and no leading +/- for infinity.</li>
* <li>Guava does not decorate String or Character values with \" or \' respectively.
* It does not generate an escaped encoding of the
* range separator if it is present in a value.
* Hard to guess whether this may change if/when Guava adds a
* "Range from Range.toString()" capability.
* <br>To avoid arbitrary deviations for Range types that may not be
* particularly interesting to Quarks users, at this time
* this Range does not support String or Character.
* </li>
* </ul>
* </li>
* </ul>
*
* @param <T> value type N.B. at this time {@code T} must be one of:
* Integer,Long,Short,Byte,Float,Double,BigInteger,BigDecimal
* <p>
* An IllegalArgumentException is thrown if an unsupported type
* is specified when constructing the Range.
*/
public class Range<T extends Comparable<?>> implements Serializable {
private static final long serialVersionUID = 1L;
private final T lowerBound; // null for infinity
private final T upperBound; // null for infinity
private final BoundType lbt;
private final BoundType ubt;
private transient int hashCode;
private static final Class<?> supportedTypes[] = {
Integer.class, Short.class, Byte.class, Long.class,
Float.class, Double.class, BigInteger.class, BigDecimal.class
};
private enum BoundType {/** exclusive */ OPEN, /** inclusive */ CLOSED};
private Range(T lowerBound, BoundType lbt, T upperBound, BoundType ubt) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.lbt = lbt;
this.ubt = ubt;
checkSupportedType(lowerBound);
checkSupportedType(upperBound);
if (lowerBound != null && upperBound != null) {
if (lowerBound.getClass() != upperBound.getClass())
throw new IllegalArgumentException("lowerBound and upperBound are not the same type");
}
}
private void checkSupportedType(Object obj) {
if (obj==null) return;
Class<?> objClass = obj.getClass();
for (Class<?> c : supportedTypes) {
if (c == objClass)
return;
}
throw new IllegalArgumentException("unsupported type: "+objClass);
}
// TODO defer making these public due to BoundType
private static <T extends Comparable<?>> Range<T> range(T lowerBound, BoundType b1, T upperBound, BoundType b2) { return new Range<T>(lowerBound, b1, upperBound, b2); }
// public static <T> Range<T> downTo(T v, BoundType b) { return range(v, b, null, null); }
// public static <T> Range<T> upTo(T v, BoundType b) { return range(null, null, v, b); }
/** (a..b) (both exclusive) */
public static <T extends Comparable<?>> Range<T> open(T lowerBound, T upperBound) {
return range(lowerBound, BoundType.OPEN, upperBound, BoundType.OPEN);
}
/** [a..b] (both inclusive) */
public static <T extends Comparable<?>> Range<T> closed(T lowerBound, T upperBound) {
return range(lowerBound, BoundType.CLOSED, upperBound, BoundType.CLOSED);
}
/** (a..b] (exclusive,inclusive) */
public static <T extends Comparable<?>> Range<T> openClosed(T lowerBound, T upperBound) {
return range(lowerBound, BoundType.OPEN, upperBound, BoundType.CLOSED);
}
/** [a..b) (inclusive,exclusive)*/
public static <T extends Comparable<?>> Range<T> closedOpen(T lowerBound, T upperBound) {
return range(lowerBound, BoundType.CLOSED, upperBound, BoundType.OPEN);
}
/** (a..*) (exclusive) */
public static <T extends Comparable<?>> Range<T> greaterThan(T v) {
return range(v, BoundType.OPEN, null, BoundType.OPEN);
}
/** [a..*) (inclusive) */
public static <T extends Comparable<?>> Range<T> atLeast(T v) {
return range(v, BoundType.CLOSED, null, BoundType.OPEN);
}
/** (*..b) (exclusive) */
public static <T extends Comparable<?>> Range<T> lessThan(T v) {
return range(null, BoundType.OPEN, v, BoundType.OPEN);
}
/** (*..b] (inclusive) */
public static <T extends Comparable<?>> Range<T> atMost(T v) {
return range(null, BoundType.OPEN, v, BoundType.CLOSED);
}
/** [v..v] (both inclusive) */
public static <T extends Comparable<?>> Range<T> singleton(T v) {
return range(v, BoundType.CLOSED, v, BoundType.CLOSED);
}
/**
* Returns true if o is a range having the same endpoints and bound types as this range.
*/
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (o == null) return false;
if (o instanceof Range) {
Range<?> r = (Range<?>) o;
return r.lbt.equals(lbt)
&& r.ubt.equals(ubt)
&& (r.lowerBound==null ? r.lowerBound == lowerBound
: r.lowerBound.equals(lowerBound))
&& (r.upperBound==null ? r.upperBound == upperBound
: r.upperBound.equals(upperBound));
}
return false;
}
@Override
public int hashCode() {
if (hashCode == 0)
hashCode = Objects.hash(lbt, lowerBound, ubt, upperBound);
return hashCode;
}
// Avoid making the Guava {lower,upper}Endpoint() methods public for now.
// It's not clear they have value in the absence of {lower,upper}BoundType()
// and at this time we're avoiding exposing our BoundType
// /**
// * @return true iff the Range's lower endpoint isn't unbounded.
// */
// public boolean hasLowerEndpoint() {
// return lowerBound != null;
// }
//
// /**
// * Get the range's lower endpoint / bound.
// * @return the endpoint.
// * @throws IllegalStateException if hasLowerEndpoint()==false
// */
// public T lowerEndpoint() {
// if (hasLowerEndpoint())
// return lowerBound;
// throw new IllegalStateException("unbounded");
// }
//
// /**
// * @return true iff the Range's upper endpoint isn't unbounded.
// */
// public boolean hasUpperEndpoint() {
// return upperBound != null;
// }
//
// /**
// * Get the range's upper endpoint / bound.
// * @return the endpoint.
// * @throws IllegalStateException if hasUpperEndpoint()==false
// */
// public T upperEndpoint() {
// if (hasUpperEndpoint())
// return upperBound;
// throw new IllegalStateException("unbounded");
// }
/**
* Determine if the Region contains the value.
* <p>
* {@code contains(v)} typically suffices. This
* is useful in the case where the Comparable's default
* {@code Comparable.compareTo()} isn't sufficient.
* e.g., for unsigned byte comparisons
* <pre>
* Comparator<Byte> unsignedByteComparator = new Comparator<Byte>() {
* public int compare(Byte b1, Byte b2) {
* return Integer.compareUnsigned(b1.toUnsignedInt(), b2.toUnsignedInt());
* }
* public boolean equals(Object o2) { return o2==this; }
* };
* Range<Byte> unsignedByteRange = ...;
* unsignedByteRange.contains(value, unsignedByteComparator);
* </pre>
* <p>
* N.B. Guava Range lacks such a method.
* <p>
* @param v the value to check for containment
* @param cmp the Comparator to use
* @return true if the Region contains the value
*/
public boolean contains(T v, Comparator<T> cmp) {
if (lowerBound==null) {
int r = cmp.compare(v, upperBound);
return ubt == BoundType.OPEN ? r < 0 : r <= 0;
}
if (upperBound==null) {
int r = cmp.compare(v, lowerBound);
return lbt == BoundType.OPEN ? r > 0 : r >= 0;
}
int r = cmp.compare(v, upperBound);
boolean ok1 = ubt == BoundType.OPEN ? r < 0 : r <= 0;
if (!ok1) return false;
r = cmp.compare(v, lowerBound);
return lbt == BoundType.OPEN ? r > 0 : r >= 0;
}
/**
* Determine if the Region contains the value.
* <p>
* The Comparator used is the default one for the type
* (e.g., {@code Integer#compareTo(Integer)}.
* <p>
* @param v the value to check for containment
* @return true if the Region contains the value
* @see #contains(T, Comparator)
*/
public boolean contains(T v) {
Comparator<T> cmp = getComparator(v);
return contains(v, cmp);
}
private Comparator<T> getComparator(T v) {
if (v instanceof Double)
return (lowerBound,upperBound) -> ((Double)lowerBound).compareTo((Double)upperBound);
if (v instanceof Float)
return (lowerBound,upperBound) -> ((Float)lowerBound).compareTo((Float)upperBound);
if (v instanceof Long)
return (lowerBound,upperBound) -> ((Long)lowerBound).compareTo((Long)upperBound);
if (v instanceof Integer)
return (lowerBound,upperBound) -> ((Integer)lowerBound).compareTo((Integer)upperBound);
if (v instanceof Short)
return (lowerBound,upperBound) -> ((Short)lowerBound).compareTo((Short)upperBound);
if (v instanceof Byte)
return (lowerBound,upperBound) -> ((Byte)lowerBound).compareTo((Byte)upperBound);
// if (v instanceof String)
// return (lowerBound,upperBound) -> ((String)lowerBound).compareTo((String)upperBound);
// if (v instanceof Character)
// return (lowerBound,upperBound) -> ((Character)lowerBound).compareTo((Character)upperBound);
if (v instanceof BigDecimal)
return (lowerBound,upperBound) -> ((BigDecimal)lowerBound).compareTo((BigDecimal)upperBound);
if (v instanceof BigInteger)
return (lowerBound,upperBound) -> ((BigInteger)lowerBound).compareTo((BigInteger)upperBound);
throw new IllegalArgumentException("Unsupported type: "+v.getClass());
}
/**
* Create a Range from a string produced by toString()
* <p>
* N.B. See note in classdoc wrt Guava Range behavior. i.e., it
* currently lacks a "Range from Range.toString() analog".
* <p>
* @param s value from toString()
* @param clazz the class of the values in {@code s}
*/
public static <T extends Comparable<?>> Range<T> valueOf(String s, Class<T> clazz) {
char lbm = s.charAt(0);
if (lbm != '[' && lbm != '(')
throw new IllegalArgumentException(s);
char ubm = s.charAt(s.length()-1);
if (ubm != ']' && ubm != ')')
throw new IllegalArgumentException(s);
BoundType lbt = lbm == '[' ? BoundType.CLOSED : BoundType.OPEN;
BoundType ubt = ubm == ']' ? BoundType.CLOSED : BoundType.OPEN;
s = s.substring(1,s.length()-1);
// this parsing is weak - broken for String bounds with embedded ".."
// not an issue right now since we don't support String
String[] parts = s.split("\\.\\.");
if (parts.length != 2)
throw new IllegalArgumentException("The range string bound values contains the separator sequence \"..\": " + s);
String lbs = parts[0];
String ubs = parts[1];
T lowerBound = lbs.equals("*") ? null : boundValue(lbs, clazz);
T upperBound = ubs.equals("*") ? null : boundValue(ubs, clazz);
return range(lowerBound, lbt, upperBound, ubt);
}
@SuppressWarnings("unchecked")
private static <T extends Comparable<?>> T boundValue(String strVal, Class<T> clazz) {
if (strVal.equals("*"))
return null;
if (clazz.equals(Integer.class))
return (T) Integer.valueOf(strVal);
if (clazz.equals(Long.class))
return (T) Long.valueOf(strVal);
if (clazz.equals(Short.class))
return (T) Short.valueOf(strVal);
if (clazz.equals(Byte.class))
return (T) Byte.valueOf(strVal);
if (clazz.equals(Float.class))
return (T) Float.valueOf(strVal);
if (clazz.equals(Double.class))
return (T) Double.valueOf(strVal);
// if (clazz.equals(String.class))
// return (T) String.valueOf(strVal);
// if (clazz.equals(Character.class))
// return (T) Character.valueOf(strVal.charAt(0));
if (clazz.equals(BigInteger.class))
return (T) new BigInteger(strVal);
if (clazz.equals(BigDecimal.class))
return (T) new BigDecimal(strVal);
throw new IllegalArgumentException("Unhandled type "+clazz);
}
/**
* Yields {@code <lowerBoundMarker><lowerBound>..<upperBound><upperBoundMarker>}.
* <p>
* Where the lowerBoundMarker is either "[" (inclusive/closed) or "(" (exclusive/open)
* and the upperBoundMarker is either "]" (inclusive/closed) or ")" (exclusive/open)
* <p>
* The bound value "*" is used to indicate an infinite value.
* <p>
* N.B. See note in classdoc wrt Guava Range behavior.
* <p>
* .e.g.,
* <pre>
* "[120..156)" // lowerBound=120 inclusive, upperBound=156 exclusive
* "[120..*)" // an "atLeast" 120 range
* </pre>
*/
public String toString() {
String[] parts = { "(", "*", "*", ")" };
if (lowerBound!=null) {
parts[0] = lbt==BoundType.CLOSED ? "[" : "(";
parts[1] = lowerBound.toString();
}
if (upperBound!=null) {
parts[2] = upperBound.toString();
parts[3] = ubt==BoundType.CLOSED ? "]" : ")";
}
return parts[0]+parts[1]+".."+parts[2]+parts[3];
}
}