/*
This file is part of the software framework JStatCom,
a library for building applications for data based analysis.
Copyright (C) 2000-2005 Markus Kraetzig
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/
package com.jstatcom.model;
import java.text.NumberFormat;
import com.jstatcom.parser.NumberRangeParser;
import com.jstatcom.util.UMath;
/**
* This class specifies an interval defined by two numbers and a type and
* provides methods to validate numbers and other NumberRange
* instances against it.
*
* The class is immutable by design, see (Joshua Bloch, "Effective Java",
* Item 13).
*
* @author Markus Kraetzig
*/
public final class NumberRange {
/**
* The lower bound of this range.
*/
public final double lowerBound;
/**
* The upper bound of this range.
*/
public final double upperBound;
/**
* The type of this range, which is either
* OPEN, CLOSED, LEFT_OPEN or RIGHT_OPEN.
*/
public final NumberRangeTypes rangeType;
/**
* Creates an open range with (-Infinity,+Infinity).
*/
public NumberRange() {
this.lowerBound = Double.NEGATIVE_INFINITY;
this.upperBound = Double.POSITIVE_INFINITY;
this.rangeType = NumberRangeTypes.OPEN;
}
/**
* Creates a range according to the given bounds and the type.
*
* @param lowerBoundNew
* the lower bound of the interval
* @param upperBoundNew
* the upper bound of the interval
* @param rangeType
* the interval type to be created
* @throws IllegalArgumentException
* if:
*
* - lowerBound > upperBound
*
- lowerBound == upperBound and rangeType is not closed
*
- lowerBound == -Infinity and rangeType is not left open
*
- upperBound == +Infinity and rangeType is not right open
*
- lowerBound == +Infinity
*
- upperBound == -Infinity
*
- upperBound or lowerBound is equal to NaN
*
- rangeType == null
*
*/
public NumberRange(double lowerBoundNew, double upperBoundNew,
NumberRangeTypes rangeType) {
checkArguments(lowerBoundNew, upperBoundNew, rangeType);
this.lowerBound = lowerBoundNew;
this.upperBound = upperBoundNew;
this.rangeType = rangeType;
}
/**
* See NumberRange constructor.
*/
private void checkArguments(double lowerBoundNew, double upperBoundNew,
NumberRangeTypes rangeType) {
if (rangeType == null)
throw new IllegalArgumentException("Range type is null.");
Double lowerB = new Double(lowerBoundNew);
Double upperB = new Double(upperBoundNew);
if (Double.isNaN(lowerB) || Double.isNaN(upperB))
throw new IllegalArgumentException(
"NaN are not allowed for bounds.");
if (lowerBoundNew == Double.POSITIVE_INFINITY
|| upperBoundNew == Double.NEGATIVE_INFINITY)
throw new IllegalArgumentException("Wrong +/-INFINITY bounds.");
if (lowerBoundNew == Double.NEGATIVE_INFINITY && !rangeType.leftOpen)
throw new IllegalArgumentException(
"Lower bound cannot be closed and -INFINITY.");
if (upperBoundNew == Double.POSITIVE_INFINITY && !rangeType.rightOpen)
throw new IllegalArgumentException(
"Upper bound cannot be closed and +INFINITY.");
if (upperBoundNew == lowerBoundNew
&& (rangeType.leftOpen || rangeType.rightOpen))
throw new IllegalArgumentException(
"If bounds are equal, interval type must be CLOSED.");
if (upperBoundNew < lowerBoundNew)
throw new IllegalArgumentException(
"Lower bound is greater than upper bound.");
}
/**
* Finds a number that is valid for this range.
*
* @return a valid number within this range
*/
public double defaultNumber() {
if (upperBound == Double.POSITIVE_INFINITY
&& lowerBound == Double.NEGATIVE_INFINITY) {
return 0;
}
if (lowerBound > Double.NEGATIVE_INFINITY && !rangeType.leftOpen) {
// lower bound is possible
return lowerBound;
}
if (upperBound < Double.POSITIVE_INFINITY && !rangeType.rightOpen) {
// upper bound is possible
return upperBound;
}
if (upperBound < Double.POSITIVE_INFINITY
&& lowerBound > Double.NEGATIVE_INFINITY) {
if (upperBound - lowerBound > 1)
return lowerBound + 1;
return (upperBound - lowerBound) / 2 + lowerBound;
}
if (lowerBound > Double.NEGATIVE_INFINITY) {
return lowerBound + 1; // upper must be +inf
}
if (upperBound < Double.POSITIVE_INFINITY) {
return upperBound - 1; // lower must be -inf
}
throw new RuntimeException("Code should never be reached.");
}
/**
* Finds a number range that is enclosed by this range. The range type of
* the returned range is always CLOSED.
*
* @return a valid number within this range
*/
public NumberRange defaultNumberRange() {
if (upperBound == Double.POSITIVE_INFINITY
&& lowerBound == Double.NEGATIVE_INFINITY) {
return NumberRange.valueOf("[0,100]"); // just a default
}
if (rangeType == NumberRangeTypes.CLOSED)
return this;
if (upperBound < Double.POSITIVE_INFINITY
&& lowerBound > Double.NEGATIVE_INFINITY) {
double lBound = 0;
double uBound = 0;
if (rangeType == NumberRangeTypes.LEFT_OPEN) {
uBound = upperBound;
if (upperBound - lowerBound > 1)
lBound = lowerBound + 1;
else
lBound = upperBound;
return new NumberRange(lBound, uBound, NumberRangeTypes.CLOSED);
}
if (rangeType == NumberRangeTypes.RIGHT_OPEN) {
lBound = lowerBound;
if (upperBound - lowerBound > 1)
uBound = upperBound - 1;
else
uBound = lowerBound;
return new NumberRange(lBound, uBound, NumberRangeTypes.CLOSED);
}
if (rangeType == NumberRangeTypes.OPEN) {
uBound = upperBound;
if (upperBound - lowerBound > 1) {
lBound = lowerBound + 1;
uBound = upperBound - 1;
return new NumberRange(lBound, uBound,
NumberRangeTypes.CLOSED);
}
double val = (upperBound - lowerBound) / 2 + lowerBound;
return new NumberRange(val, val, NumberRangeTypes.CLOSED);
}
}
// In any other case just create a one number range.
double defaultNumber = defaultNumber();
return new NumberRange(defaultNumber, defaultNumber,
NumberRangeTypes.CLOSED);
}
/**
* Tests whether a given number is enclosed in this range and returns a
* formatted message if not. If the number is either POS_INF, NEG_INF or
* NaN, it is never enclosed in the range.
*
* @param number
* the value to check
* @return null if number is contained, message if not
*/
public String encloses(double number) {
boolean leftCheck = lowerBound < number;
if (!rangeType.leftOpen && !leftCheck)
leftCheck = lowerBound == number;
boolean rightCheck = upperBound > number;
if (!rangeType.rightOpen && !rightCheck)
rightCheck = upperBound == number;
if (leftCheck && rightCheck)
return null;
String msg = number + " is not contained in " + toString() + ".";
return msg;
}
/**
* Tests whether a given number is enclosed in this range and returns a
* formatted message if not. This version also takes the given precision
* into account and rounds the number before checking. If the
* number is either POS_INF, NEG_INF or NaN, it is never enclosed in the
* range.
*
* @param number
* the value to check
* @param precision
* the number of digits right to the decimal point to take into
* account for checking
* @param formatter
* a formatter for number in the possible error
* message, can be null
*
* @return null if number is contained, message if not
* @throws IllegalArgumentException
* if if (precision < 0)
*/
public String encloses(double number, int precision, NumberFormat formatter) {
if (precision < 0)
throw new IllegalArgumentException("Precision < 0.");
double orig = number;
String rounded = "";
boolean isNaN = Double.isNaN(number);
if (!isNaN && number != Double.NEGATIVE_INFINITY
&& number != Double.POSITIVE_INFINITY) {
number = UMath.round(number, precision);
if (number - orig != 0)
rounded = " (rounded)";
}
boolean leftCheck = lowerBound < number;
if (!rangeType.leftOpen && !leftCheck)
leftCheck = lowerBound == number;
boolean rightCheck = upperBound > number;
if (!rangeType.rightOpen && !rightCheck)
rightCheck = upperBound == number;
if (leftCheck && rightCheck)
return null;
String num = number + "";
if (formatter != null) {
formatter.setMaximumFractionDigits(precision);
formatter.setMinimumFractionDigits(precision);
num = formatter.format(number);
formatter.setMaximumFractionDigits(100);
}
String msg = num + rounded + " is not contained in "
+ formattedString(formatter) + ".";
return msg;
}
/**
* Tests whether a given range is completely enclosed within this range and
* returns a formatted message if not. A range cannot be enclosed, if one of
* its bounds is POS_INF or NEG_INF.
*
* @param numberRange
* the range to check
* @return null if numberRange is contained, message if not
*/
public String encloses(NumberRange numberRange) {
boolean leftCheck = lowerBound < numberRange.lowerBound;
if (!rangeType.leftOpen && !leftCheck)
leftCheck = lowerBound == numberRange.lowerBound;
boolean rightCheck = upperBound > numberRange.upperBound;
if (!rangeType.rightOpen && !rightCheck)
rightCheck = upperBound == numberRange.upperBound;
if (leftCheck && rightCheck)
return null;
String msg = numberRange + " is not contained in " + toString() + ".";
return msg;
}
/**
* Provides logical equals check.
*
* @param o
* another range to compare with
* @return true if lower bound, upper bound and range type
* are all equal
*/
public boolean equals(Object o) {
if (!(o instanceof NumberRange))
return false;
NumberRange other = (NumberRange) o;
return lowerBound == other.lowerBound && upperBound == other.upperBound
&& rangeType == other.rangeType;
}
/**
* Gets a string representation of this range with a special formatter. If
* formatter == null, this is identical to
* toString.
*
* @param formatter
* a number formatter for the bounds, can be null
* @return a string in mathematical notation
*/
public String formattedString(NumberFormat formatter) {
StringBuffer ret = new StringBuffer();
ret.append(rangeType.leftBracket);
if (formatter == null) {
if ((Math.rint(lowerBound) - lowerBound) == 0)
ret.append(new Double(lowerBound).intValue());
else
ret.append(lowerBound);
} else
ret.append(formatter.format(lowerBound));
ret.append(", ");
if (formatter == null) {
{
if ((Math.rint(upperBound) - upperBound) == 0)
ret.append(new Double(upperBound).intValue());
else
ret.append(upperBound);
}
} else
ret.append(formatter.format(upperBound));
ret.append(rangeType.rightBracket);
return ret.toString();
}
/**
* Gets the lower bound for this range.
*
* @return lower bound
*/
public double lowerBound() {
return lowerBound;
}
/**
* Gets the interval type for this range.
*
* @return range type
*/
public NumberRangeTypes type() {
return rangeType;
}
/**
* Gets the upper bound for this range.
*
* @return upper bound
*/
public double upperBound() {
return upperBound;
}
/**
* Overrides hashCode, because equals was overwritten. The
* algorithm was taken from (Joshua Bloch, "Effective Java", Item 8).
*
* @return the hash code
*/
public int hashCode() {
int result = 17;
long f = Double.doubleToLongBits(lowerBound);
result = 37 * result + (int) (f ^ (f >>> 32));
f = Double.doubleToLongBits(upperBound);
result = 37 * result + (int) (f ^ (f >>> 32));
result = 37 * result + rangeType.hashCode();
return result;
}
/**
* Gets string representation of this range.
*
* @return a string in mathematical notation
*/
public String toString() {
StringBuffer ret = new StringBuffer();
ret.append(rangeType.leftBracket);
if ((Math.rint(lowerBound) - lowerBound) == 0)
ret.append(new Double(lowerBound).intValue());
else
ret.append(lowerBound);
ret.append(", ");
if ((Math.rint(upperBound) - upperBound) == 0)
ret.append(new Double(upperBound).intValue());
else
ret.append(upperBound);
ret.append(rangeType.rightBracket);
return ret.toString();
}
/**
* Static initialized that creates a NumberRange from a
* string representation. Valid expressions are:
*
* - [1,100]
*
- (-Infinity,100.3]
*
- (-Infinity,+Infinity)
*
- (-12E-2,23]
*
- [100,100]
*
*
*
* @param exp
* string containing a valid representation for a number range
* @return a number range instance created from exp
* @throws IllegalArgumentException
* if exp could not be parsed to a valid number
* range
* @see #NumberRange(double lowerBoundNew,double
* upperBoundNew,NumberRangeTypes rangeType)
*/
public static NumberRange valueOf(String exp) {
NumberRange range = null;
try {
range = new NumberRangeParser().parseStringToRange(exp);
} catch (Throwable ex) {
throw new IllegalArgumentException(
"NumberRange construction from \"" + exp + "\" failed.\n"
+ ex);
}
return range;
}
}
Ads help maintain this website.