org.conqat.lib.commons.math.Range Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed 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.conqat.lib.commons.math;
import java.io.Serializable;
import java.text.NumberFormat;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.test.IndexValueClass;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* A class that represents ranges that may include or exclude the upper and lower bounds. This class
* is immutable.
*
* Note: If a range is constructed where the upper and lower bounds are equal and one of them is
* exclusive, this range is considered empty, i.e. no number can be contained in it.
*/
@IndexValueClass
public class Range implements Comparable, Serializable {
/** Version for serialization. */
private static final long serialVersionUID = 1;
/** The name of the JSON property name for {@link #lower}. */
private static final String LOWER_PROPERTY = "lower";
/** The name of the JSON property name for {@link #upper}. */
private static final String UPPER_PROPERTY = "upper";
/** The lower bound. */
@JsonProperty(LOWER_PROPERTY)
private final double lower;
/** The upper bound. */
@JsonProperty(UPPER_PROPERTY)
private final double upper;
/** Flag that indicates if lower bound is inclusive or not. */
@JsonProperty("lowerIsInclusive")
private final boolean lowerIsInclusive;
/** Flag that indicates if upper bound is inclusive or not. */
@JsonProperty("upperIsInclusive")
private final boolean upperIsInclusive;
/** Create range where both bounds are inclusive. */
@JsonCreator
public Range(@JsonProperty(LOWER_PROPERTY) double lower, @JsonProperty(UPPER_PROPERTY) double upper) {
this(lower, true, upper, true);
}
/**
* Create range.
*
* @param lowerIsInclusive
* flag that indicates if lower bound is inclusive or not
* @param upperIsInclusive
* flag that indicates if upper bound is inclusive or not
*/
public Range(double lowerBound, boolean lowerIsInclusive, double upperBound, boolean upperIsInclusive) {
CCSMAssert.isFalse(Double.isNaN(lowerBound), "Lower bound must not be NaN");
CCSMAssert.isFalse(Double.isNaN(upperBound), "Upper bound must not be NaN");
lower = lowerBound;
this.lowerIsInclusive = lowerIsInclusive;
upper = upperBound;
this.upperIsInclusive = upperIsInclusive;
}
/** Checks is a number is contained in the range. */
public boolean contains(double number) {
if (lowerIsInclusive) {
if (number < lower) {
return false;
}
} else {
if (number <= lower) {
return false;
}
}
if (upperIsInclusive) {
return !(number > upper);
} else {
return !(number >= upper);
}
}
/**
* Hash code includes bound and the flags that indicate if the bounds are inclusive or not.
*/
@Override
public int hashCode() {
if (isEmpty()) {
return 0;
}
int result = hash(upper) + 37 * hash(lower);
// indicate bounds by bit flips; which bit is used is not really
// relevant
if (lowerIsInclusive) {
result ^= 0x100000;
}
if (upperIsInclusive) {
result ^= 0x4000;
}
return result;
}
/** Code for hashing a double is copied from {@link Double#hashCode()}. */
private static int hash(double number) {
long bits = Double.doubleToLongBits(number);
return (int) (bits ^ (bits >>> 32));
}
/**
* Two ranges are equal if their bounds are equal and the flags that indicate if the bounds are
* inclusive or not are equal, too. Empty ranges are considered equal regardless for there specific
* bounds.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Range)) {
return false;
}
Range other = (Range) obj;
if (isEmpty() && other.isEmpty()) {
return true;
}
return lowerIsInclusive == other.lowerIsInclusive && upperIsInclusive == other.upperIsInclusive
&& lower == other.lower && upper == other.upper;
}
/** Get lower bound. */
public double getLower() {
return lower;
}
/** Get upper bound. */
public double getUpper() {
return upper;
}
/** Get flag that indicates if the lower bound is inclusive. */
public boolean isLowerInclusive() {
return lowerIsInclusive;
}
/** Get flag that indicates if the upper bound is inclusive. */
public boolean isUpperInclusive() {
return upperIsInclusive;
}
/** Checks if a range is empty. */
public boolean isEmpty() {
if (lowerIsInclusive && upperIsInclusive) {
return lower > upper;
}
return lower >= upper;
}
/**
* Returns whether this range is a singleton (i.e. contains of a single point/number).
*/
public boolean isSingleton() {
return lower == upper && lowerIsInclusive && upperIsInclusive;
}
/**
* Returns the size of the range. In case of empty ranges this returns 0. Note that inclusiveness of
* bound is not relevant for this method.
*/
public double size() {
return Math.max(0, upper - lower);
}
/** This forwards to format(null);
. */
@Override
public String toString() {
return format(null);
}
/**
* String representation contains the bounds and brackets that indicate if the bounds are inclusive
* or exclusive.
*
* @param numberFormat
* number format used for formatting the numbers. If this is null
, no
* special formatting is applied.
*/
public String format(NumberFormat numberFormat) {
StringBuilder result = new StringBuilder();
if (lowerIsInclusive) {
result.append("[");
} else {
result.append("]");
}
result.append(StringUtils.format(lower, numberFormat) + ";" + StringUtils.format(upper, numberFormat));
if (upperIsInclusive) {
result.append("]");
} else {
result.append("[");
}
return result.toString();
}
/**
* Returns whether this range has a non-empty intersection with another range.
*/
public boolean overlaps(Range other) {
if (isEmpty() || other.isEmpty()) {
return false;
}
if (lower == other.lower) {
if (isSingleton()) {
return other.lowerIsInclusive;
}
if (other.isSingleton()) {
return lowerIsInclusive;
}
return true;
}
if (lower < other.lower) {
if (upperIsInclusive && other.lowerIsInclusive) {
return other.lower <= upper;
}
return other.lower < upper;
}
return other.overlaps(this);
}
/**
* {@inheritDoc}
*
* Compares by lower values.
*/
@Override
public int compareTo(Range other) {
int result = Double.compare(lower, other.lower);
if (result != 0) {
return result;
}
if (lowerIsInclusive && !other.lowerIsInclusive) {
return -1;
}
if (!lowerIsInclusive && other.lowerIsInclusive) {
return 1;
}
return 0;
}
/** Determines whether the other range is fully contained in this range. */
public boolean contains(Range other) {
boolean sameLowerBound = isLowerInclusive() == other.isLowerInclusive() && getLower() == other.getLower();
boolean containsLower = contains(other.getLower()) || sameLowerBound;
boolean sameUpperBound = isUpperInclusive() == other.isUpperInclusive() && getUpper() == other.getUpper();
boolean containsUpper = contains(other.getUpper()) || sameUpperBound;
return containsLower && containsUpper;
}
/**
* Calculates the union of this range with another range. The union is defined by the smaller lower
* bound and the higher upper bound of the two ranges.
*/
public Range union(Range other) {
double lowerBoundOfOtherRange = other.getLower();
double minLower;
boolean lowerInclusive;
if (lower == lowerBoundOfOtherRange) {
minLower = lower;
lowerInclusive = isLowerInclusive() || other.isLowerInclusive();
} else if (lower < lowerBoundOfOtherRange) {
minLower = lower;
lowerInclusive = isLowerInclusive();
} else {
minLower = lowerBoundOfOtherRange;
lowerInclusive = other.isLowerInclusive();
}
double upperBoundOfOtherRange = other.getUpper();
double maxUpper;
boolean upperInclusive;
if (upper == upperBoundOfOtherRange) {
maxUpper = upper;
upperInclusive = isUpperInclusive() || other.isUpperInclusive();
} else if (upper < upperBoundOfOtherRange) {
maxUpper = upperBoundOfOtherRange;
upperInclusive = other.isUpperInclusive();
} else {
maxUpper = upper;
upperInclusive = isUpperInclusive();
}
return new Range(minLower, lowerInclusive, maxUpper, upperInclusive);
}
}