| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.edgent.analytics.sensors; |
| |
| import java.io.Serializable; |
| import java.util.Comparator; |
| import java.util.Objects; |
| |
| import org.apache.edgent.function.Function; |
| import org.apache.edgent.function.Predicate; |
| |
| /** |
| * A generic immutable range of values and a way to |
| * check a value for containment in the range. |
| * <p> |
| * Useful in filtering in predicates. |
| * <p> |
| * A Range consists of a lower and upper endpoint and a {@link BoundType} |
| * for each endpoint. |
| * <p> |
| * {@link BoundType#CLOSED} includes the endpoint's value in the range and |
| * is represented as "[" or "]" in the string form for a lower and upper bound |
| * type respectively. |
| * {@link BoundType#OPEN} excludes the endpoint's value in the range and |
| * is represented as "(" or ")" in the string form for a lower and upper bound |
| * type respectively. e.g. {@code "[2..5)"} |
| * <p> |
| * Typically, the convenience methods in {@link Ranges} are used to |
| * construct a Range. |
| * <p> |
| * {@link #contains(Comparable) contains()} is used to check for containment: |
| * e.g. |
| * <pre>{@code |
| * Ranges.closed(2,4).contains(2); // returns true |
| * Ranges.open(2,4).contains(2); // returns false |
| * Ranges.atLeast(2).contains(2); // returns true |
| * Ranges.greaterThan(2).contains(2); // returns false |
| * Ranges.atMost(2).contains(2); // returns true |
| * Ranges.lessThan(2).contains(2); // returns false |
| * }</pre> |
| * |
| * {@link #toString()} yields a convenient string representation and a |
| * Range may be created from the string representation. |
| * <pre>{@code |
| * String s = Ranges.closed(2,4).toString(); // yields "[2..4]" |
| * Range<Integer> range = Ranges.valueOfInteger(s); // yields a Range<Integer>(2,4) |
| * }</pre> |
| * <p> |
| * Range works with Gson ({@code new Gson().toJson(Range<T>)}. |
| * As as documented by Gson, for generic types you must use |
| * {@code Gson.fromJson(String, java.lang.reflect.Type)} to |
| * create a Range from its json. |
| * <p> |
| * Sample use in a TStream context: |
| * <pre>{@code |
| * TStream<JsonObject> jStream = ...; |
| * TStream<JsonObject> filtered = Filters.deadband(jStream, |
| * json -> json.getProperty("pressureReading").asInteger(), |
| * Ranges.closed(10, 30)); |
| * }</pre> |
| * |
| * @param <T> a {@link Comparable} value type |
| */ |
| public final class Range<T extends Comparable<?>> implements Predicate<T>, Serializable { |
| /* |
| * Useful in understanding some of the motivations/issues: |
| * |
| * This is a lightweight implementation of a subset of the Guava Range. |
| * Guava Range is great, but... |
| * |
| * Compared to Guava Range: |
| * - Guava Range isn't easily separable from the "bulky" Guave package. |
| * Guava Range itself isn't particularly lightweight (depends on numerous |
| * other Guava classes). |
| * This Range is very lightweight. |
| * - Guava Range has a large set of static convenience constructors, |
| * whereas we've chosen to make them members of our Ranges class |
| * (along with "Range from Range.toString()" methods) |
| * - Guava Range doesn't support unsigned byte/short/int/long type ranges. |
| * This Range adds {@link #contains(Comparable, Comparator)} |
| * and {@link #toStringUnsigned()}. |
| * - Guava Range lacks a "Range from Range.toString()" function: https://github.com/google/guava/issues/1911. |
| * Ranges has valueOf*() for convenience with commonly used numeric types, |
| * and Range has valueOf() for general use. |
| * - Guava Range has issues with to/from Json with Gson: |
| * https://github.com/google/guava/issues/1911. |
| * This Range works but as doc'd by Gson, you must use |
| * {@code Gson#fromJson(String, java.lang.reflect.Type)}</li> |
| * - Guava Range's {@code apply(T value)} is documented as deprecated. |
| * None the less there was a desire that our Range "implement Predicate" |
| * so we do. |
| * - Guava Range.toString() |
| * - 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. |
| * 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> |
| * - 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. |
| */ |
| |
| private static final long serialVersionUID = 1L; |
| |
| private final T lowerEndpoint; // null for infinity |
| private final T upperEndpoint; // null for infinity |
| private final BoundType lbt; |
| private final BoundType ubt; |
| private transient int hashCode; |
| |
| /** |
| * Exclude or include an endpoint value in the range. |
| */ |
| public static enum BoundType {/** exclusive */ OPEN, /** inclusive */ CLOSED}; |
| |
| /** |
| * Create a new Range<T>. Private like Guava Range. |
| * <p> |
| * See {@link Ranges} for a collection of convenience constructors. |
| * @param lowerEndpoint null for an infinite value (and lbt must be OPEN) |
| * @param lbt {@link BoundType} for the lowerEndpoint |
| * @param upperEndpoint null for an infinite value (and ubt must be OPEN) |
| * @param ubt {@link BoundType} for the upperEndpoint |
| */ |
| private Range(T lowerEndpoint, BoundType lbt, T upperEndpoint, BoundType ubt) { |
| this.lowerEndpoint = lowerEndpoint; |
| this.upperEndpoint = upperEndpoint; |
| this.lbt = lbt; |
| this.ubt = ubt; |
| if (lowerEndpoint != null && upperEndpoint != null) { |
| if (lowerEndpoint.getClass() != upperEndpoint.getClass()) |
| throw new IllegalArgumentException("lowerEndpoint and upperEndpoint are not the same type"); |
| } |
| if (lowerEndpoint == null && lbt != BoundType.OPEN) |
| throw new IllegalArgumentException("endpoint is null and BoundType != OPEN"); |
| if (upperEndpoint == null && ubt != BoundType.OPEN) |
| throw new IllegalArgumentException("endpoint is null and BoundType != OPEN"); |
| } |
| |
| /** |
| * Create a new Range<T> |
| * <p> |
| * See {@link Ranges} for a collection of convenience constructors. |
| * |
| * @param <T> a Comparable type |
| * @param lowerEndpoint null for an infinite value (and lbt must be OPEN) |
| * @param lbt {@link BoundType} for the lowerEndpoint |
| * @param upperEndpoint null for an infinite value (and ubt must be OPEN) |
| * @param ubt {@link BoundType} for the upperEndpoint |
| * @return the Range |
| */ |
| public static <T extends Comparable<?>> Range<T> range(T lowerEndpoint, BoundType lbt, T upperEndpoint, BoundType ubt) { |
| // matchs Guava Range.range param order |
| return new Range<T>(lowerEndpoint, lbt, upperEndpoint, ubt); |
| } |
| |
| /** |
| * Returns true if o is a Range having equal endpoints and bound types to this Range. |
| */ |
| @Override |
| public boolean equals(Object o) { |
| if (o == this) return true; |
| if (o instanceof Range) { |
| Range<?> r = (Range<?>) o; |
| return r.lbt.equals(lbt) |
| && r.ubt.equals(ubt) |
| && (r.lowerEndpoint==null ? r.lowerEndpoint == lowerEndpoint |
| : r.lowerEndpoint.equals(lowerEndpoint)) |
| && (r.upperEndpoint==null ? r.upperEndpoint == upperEndpoint |
| : r.upperEndpoint.equals(upperEndpoint)); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| if (hashCode == 0) |
| hashCode = Objects.hash(lbt, lowerEndpoint, ubt, upperEndpoint); |
| return hashCode; |
| } |
| |
| /** |
| * @return true iff the Range's lower endpoint isn't unbounded/infinite |
| */ |
| public boolean hasLowerEndpoint() { |
| return lowerEndpoint != null; |
| } |
| |
| /** |
| * Get the range's lower endpoint. |
| * @return the endpoint |
| * @throws IllegalStateException if hasLowerEndpoint()==false |
| */ |
| public T lowerEndpoint() { |
| if (hasLowerEndpoint()) |
| return lowerEndpoint; |
| throw new IllegalStateException("unbounded"); |
| } |
| |
| /** |
| * @return true iff the Range's upper endpoint isn't unbounded/infinite |
| */ |
| public boolean hasUpperEndpoint() { |
| return upperEndpoint != null; |
| } |
| |
| /** |
| * Get the range's upper endpoint. |
| * @return the endpoint |
| * @throws IllegalStateException if hasUpperEndpoint()==false |
| */ |
| public T upperEndpoint() { |
| if (hasUpperEndpoint()) |
| return upperEndpoint; |
| throw new IllegalStateException("unbounded"); |
| } |
| |
| /** |
| * Get the BoundType for the lowerEndpoint. |
| * @return the BoundType |
| */ |
| public BoundType lowerBoundType() { |
| return lbt; |
| } |
| |
| /** |
| * Get the BoundType for the upperEndpoint. |
| * @return the BoundType |
| */ |
| public BoundType upperBoundType() { |
| return ubt; |
| } |
| |
| /** |
| * Determine if the Region contains the value. |
| * <p> |
| * {@code contains(v)} typically suffices. This |
| * can be used in cases where it 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(Byte.toUnsignedInt(b1), Byte.toUnsignedInt(b2)); |
| * } |
| * public boolean equals(Object o2) { return o2==this; } |
| * }; |
| * Range<Byte> unsignedByteRange = Ranges.valueOfByte("[0..255]"); |
| * unsignedByteRange.contains(byteValue, unsignedByteComparator); |
| * </pre> |
| * |
| * @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) { |
| // N.B. Guava Range lacks such a method. |
| if (lowerEndpoint==null) { |
| int r = cmp.compare(v, upperEndpoint); |
| return ubt == BoundType.OPEN ? r < 0 : r <= 0; |
| } |
| if (upperEndpoint==null) { |
| int r = cmp.compare(v, lowerEndpoint); |
| return lbt == BoundType.OPEN ? r > 0 : r >= 0; |
| } |
| int r = cmp.compare(v, upperEndpoint); |
| boolean ok1 = ubt == BoundType.OPEN ? r < 0 : r <= 0; |
| if (!ok1) |
| return false; |
| r = cmp.compare(v, lowerEndpoint); |
| return lbt == BoundType.OPEN ? r > 0 : r >= 0; |
| } |
| |
| /** |
| * Determine if the Region contains the value. |
| * <p> |
| * The Comparable's compareTo() is used for the comparisons. |
| * <p> |
| * @param v the value to check for containment |
| * @return true if the Region contains the value |
| * @see #contains(Comparable, Comparator) |
| */ |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| public boolean contains(T v) { |
| if (lowerEndpoint==null) { |
| int r = ((Comparable)v).compareTo(upperEndpoint); |
| return ubt == BoundType.OPEN ? r < 0 : r <= 0; |
| } |
| if (upperEndpoint==null) { |
| int r = ((Comparable)v).compareTo(lowerEndpoint); |
| return lbt == BoundType.OPEN ? r > 0 : r >= 0; |
| } |
| int r = ((Comparable)v).compareTo(upperEndpoint); |
| boolean ok1 = ubt == BoundType.OPEN ? r < 0 : r <= 0; |
| if (!ok1) |
| return false; |
| r = ((Comparable)v).compareTo(lowerEndpoint); |
| return lbt == BoundType.OPEN ? r > 0 : r >= 0; |
| } |
| |
| /** |
| * Predicate.test() implementation. Identical to contains(value). |
| */ |
| @Override |
| public boolean test(T value) { |
| return contains(value); |
| } |
| |
| /** |
| * Parse a String from {@link #toString()} |
| * |
| * @param str the String |
| * @return Four element array with the range's component Strings |
| * @throws IllegalArgumentException |
| */ |
| private static String[] parse(String s) { |
| char lbc = s.charAt(0); |
| if (lbc != '[' && lbc != '(') |
| throw new IllegalArgumentException(s); |
| String lbs = lbc=='[' ? "[" : "("; |
| char ubc = s.charAt(s.length()-1); |
| if (ubc != ']' && ubc != ')') |
| throw new IllegalArgumentException(s); |
| String ubs = ubc==']' ? "]" : ")"; |
| |
| s = s.substring(1,s.length()-1); |
| // problematic case: String endpoint with embedded ".." |
| // special case Character endpoint with value "." |
| String[] parts = new String[2]; |
| if (s.length() == "....".length()) { |
| parts[0] = String.valueOf(s.charAt(0)); |
| parts[1] = String.valueOf(s.charAt(3)); |
| } |
| else { |
| parts = s.split("\\.\\."); |
| if (parts.length != 2) |
| throw new IllegalArgumentException("A range string endpoint value contains the separator sequence \"..\": " + s); |
| if (parts[0].isEmpty() && parts[1].length() > 1) { |
| // handles the case Range<String>(".", "anythingElse") |
| // Range<String>("anything", ".") falls out OK from split |
| parts[0] = parts[1].substring(0, 1); |
| parts[1] = parts[1].substring(1); |
| } |
| } |
| |
| String les = parts[0]; |
| String ues = parts[1]; |
| |
| return new String[]{ lbs, les, ues, ubs }; |
| } |
| |
| /** |
| * Create a Range from a String produced by toString(). |
| * <p> |
| * See {@link Ranges} for a collection of valueOf methods |
| * for several types of {@code T}. |
| * |
| * @param <T> a Comparable type |
| * @param toStringValue value from toString() or has the same syntax. |
| * @param fromString function to create a T from its String value from |
| * the parsed toStringValue. Should throw an IllegalArgumentException |
| * if unable to perform the conversion. |
| * @return the Range |
| * @throws IllegalArgumentException if unable to parse or convert to |
| * endpoint in toStringValue to a T. |
| */ |
| public static <T extends Comparable<?>> Range<T> valueOf(String toStringValue, Function<String,T> fromString) { |
| // N.B. See note in classdoc wrt Guava Range behavior. i.e., it |
| // currently lacks a "Range from Range.toString() analog". |
| |
| String[] parts = parse(toStringValue); |
| BoundType lbt = parts[0] == "[" ? BoundType.CLOSED : BoundType.OPEN; |
| String les = parts[1]; |
| String ues = parts[2]; |
| BoundType ubt = parts[3] == "]" ? BoundType.CLOSED : BoundType.OPEN; |
| |
| T lowerEndpoint = les.equals("*") ? null : fromString.apply(les); |
| T upperEndpoint = ues.equals("*") ? null : fromString.apply(ues); |
| |
| return new Range<T>(lowerEndpoint, lbt, upperEndpoint, ubt); |
| } |
| |
| /** |
| * Yields {@code "<lowerBoundType><lowerEndpoint>..<upperEndpoint><upperBoundType>"}. |
| * <p> |
| * lowerBoundType is either "[" or "(" for BoundType.CLOSED or OPEN respectively. |
| * upperBoundType is either "]" or ")" for BoundType.CLOSED or OPEN respectively. |
| * <p> |
| * The endpoint value "*" is used to indicate an infinite value. |
| * Otherwise, endpoint.toString() is used to get the endpoint's value. |
| * <p> |
| * Likely yields an undesirable result when wanting to treat |
| * a Byte, Short, Integer, or Long T in an unsigned fashion. |
| * See toStringUnsigned(). |
| * <p> |
| * No special processing is performed to escape/encode a "." present |
| * in an endpoint.toString() value. Hence Range<T>.toString() for |
| * a {@code T} of {@code String} (of value "." or with embedded ".."), |
| * or some other non-numeric type may yield values that are not amenable |
| * to parsing by {@link #valueOf(String, Function)}. |
| * <br> |
| * .e.g., |
| * <pre> |
| * "[120..156)" // lowerEndpoint=120 inclusive, upperEndpoint=156 exclusive |
| * "[120..*)" // an "atLeast" 120 range |
| * </pre> |
| */ |
| @Override |
| public String toString() { |
| return toString(endpoint -> endpoint.toString()); |
| } |
| |
| /** |
| * Return a String treating the endpoints as an unsigned value. |
| * @return String with same form as {@link #toString()} |
| * @throws IllegalArgumentException if the Range is not one of |
| * Byte, Short, Integer, Long |
| */ |
| public String toStringUnsigned() { |
| return toString(endpoint -> toUnsignedString(endpoint)); |
| } |
| |
| private String toString(Function<T,String> toStringFn) { |
| // N.B. See note in classdoc wrt Guava Range behavior. |
| String[] parts = { "(", "*", "*", ")" }; |
| if (lowerEndpoint!=null) { |
| parts[0] = lbt==BoundType.CLOSED ? "[" : "("; |
| parts[1] = toStringFn.apply(lowerEndpoint); |
| } |
| if (upperEndpoint!=null) { |
| parts[2] = toStringFn.apply(upperEndpoint); |
| parts[3] = ubt==BoundType.CLOSED ? "]" : ")"; |
| } |
| |
| return parts[0]+parts[1]+".."+parts[2]+parts[3]; |
| } |
| |
| private static <T> String toUnsignedString(T v) { |
| if (v instanceof Byte) |
| return Integer.toUnsignedString(Byte.toUnsignedInt((Byte)v)); |
| else if (v instanceof Short) |
| return Integer.toUnsignedString(Short.toUnsignedInt((Short)v)); |
| else if (v instanceof Integer) |
| return Integer.toUnsignedString((Integer)v); |
| else if (v instanceof Long) |
| return Long.toUnsignedString((Long)v); |
| throw new IllegalArgumentException("Not Range of Byte,Short,Integer, or Long"+v.getClass()); |
| } |
| |
| } |