/* File: Datum.java
* Copyright (C) 2002-2003 The University of Iowa
* Created by: Jeremy Faden A Datum is a number in the context of a Unit, for example "15 microseconds."
* A Datum object has methods for
*
* Also a Datum's precision can be limited to improve formatting.
* @author jbf
*/
public class Datum implements Comparable, Serializable {
protected Units units;
protected Number value;
private double resolution;
private transient DatumFormatter formatter;
/**
* class backing Datums with a double.
*/
public static class Double extends Datum {
Double( Number value, Units units ) {
super( value, units, 0. );
}
Double( double value, Units units ) {
super( value, units, 0. );
}
Double( double value ) {
super( value, Units.dimensionless, 0. );
}
Double( double value, Units units, double resolution ) {
super( value, units, units.getDatumFormatterFactory().defaultFormatter(), resolution );
}
}
/**
* class backing Datums with a long, such as with CDF_TT2000.
*/
public static class Long extends Datum {
public Long( long value, Units units ) {
super(value, units, 0. );
}
public long longValue(Units u) {
if ( u!=this.units ) throw new IllegalArgumentException("Units must be "+this.units);
return this.value.longValue();
}
}
private Datum(Number value, Units units, double resolution ) {
this( value, units, units.getDatumFormatterFactory().defaultFormatter(), resolution );
}
private Datum(Number value, Units units, DatumFormatter formatter, double resolution ) {
if ( value==null ) throw new IllegalArgumentException("value is null");
this.value = value;
this.units = units;
this.resolution= resolution;
this.formatter = formatter;
}
/**
* returns the datum's double value. This protected method allows subclasses and classes
* within the package to peek at the double value.
*
* @return the double value of the datum in the context of its units.
*/
protected double doubleValue() {
return this.getValue().doubleValue();
}
/**
* returns a double representing the datum in the context of units
.
*
* @param units the Units in which the double should be returned
* @return a double in the context of the provided units.
*/
public double doubleValue(Units units) {
if ( units!=getUnits() ) {
return getUnits().getConverter(units).convert(this.getValue()).doubleValue();
} else {
return this.getValue().doubleValue();
}
}
/**
* returns the double value without the unit, as long as the Units indicate this is a ratio measurement, and there is a meaningful 0.
* For example "5 Kg" → 5, but "2012-02-16T00:00" would throw an IllegalArgumentException. Note this was introduced because often we just need
* to check to see if a value is zero.
* @return
*/
public double value() {
if ( UnitsUtil.isRatioMeasurement(units) ) {
return this.doubleValue( this.getUnits() );
} else {
throw new IllegalArgumentException("datum is not ratio measurement: "+this );
}
}
/**
* return the absolute value (magnitude) of this Datum. If this
* datum is fill then the result is fill. This will have the same
* units as the datum.
* @return
* @throws IllegalArgumentException if the datum is not a ratio measurement (like a timetag).
* @see #value() which returns the double value.
*/
public Datum abs() {
if ( UnitsUtil.isRatioMeasurement(units) ) {
if ( this.getUnits().isFill(value) ) {
return this;
} else if ( this.value.doubleValue()>=0 ) {
return this;
} else {
return this.getUnits().createDatum( Math.abs(value.doubleValue()) );
}
} else {
throw new IllegalArgumentException("datum is not a ratio measurement: "+this );
}
}
/**
* returns the resolution (or precision) of the datum. This is metadata for the datum, used
* primarily to limit the number of decimal places displayed in a string representation,
* but operators like add and multiply will propagate errors through the calculation.
*
* @param units the Units in which the double resolution should be returned. Note
* the units must be convertible to this.getUnits().getOffsetUnits().
* @return the double resolution of the datum in the context of units.
*/
public double getResolution( Units units ) {
Units offsetUnits= getUnits().getOffsetUnits();
if ( units!=offsetUnits ) {
return offsetUnits.getConverter(units).convert(this.resolution);
} else {
return this.resolution;
}
}
/**
* returns the datum's int value. This protected method allows subclasses and classes
* within the package to peek at the value as an integer. (The intent was that a
* Datum might be backed by an integer instead of a double, so that numerical
* round-off issues can be avoided.)
*
* @return the integer value of the datum in the context of its units.
*/
protected int intValue() {
return this.getValue().intValue();
}
/**
* returns a int representing the datum in the context of units
.
*
* @param units the Units in which the int should be returned
* @return a double in the context of the provided units.
*/
public int intValue(Units units) {
if ( units!=getUnits() ) {
return getUnits().getConverter(units).convert(this.getValue()).intValue();
} else {
return this.getValue().intValue();
}
}
/**
* returns the datum's units. For example, UT times might have the units
* Units.us2000.
*
* @return the datum's units.
*/
public Units getUnits() {
return this.units;
}
/**
* returns the Number representing the datum's location in the space identified by its units.
* This protected method allows subclasses and classes
* within the package to peek at the value. (The intent was that a
* Datum might be backed by an integer, float, or double, depending on the application.)
* @return a Number in the context of the provided units.
*/
protected Number getValue() {
return this.value;
}
/**
* convenience method for checking to see if a datum is a fill datum.
* @return true if the value is fill as defined by the Datum's units.
*/
public boolean isFill() {
return getUnits().isFill(getValue());
}
/**
* Groovy scripting language uses this for overloading.
* @param datum Datum to add, that is convertible to this.getUnits().
* @return a Datum that is the sum of the two values in this Datum's units.
*/
public Datum plus( Datum datum ) {
return add(datum);
}
/**
* Groovy scripting language uses this for overloading.
* @param datum Datum to subtract, that is convertible to this.getUnits().
* @return a Datum that is the sum of the two values in this Datum's units.
*/
public Datum minus( Datum datum ) {
return subtract(datum);
}
/**
* Groovy scripting language uses this for overloading.
* @param datum Datum to divide, that is convertible to this.getUnits().
* @return a Datum that is the divide of the two values in this Datum's units.
*/
public Datum div( Datum datum ) {
return divide(datum);
}
/**
* Groovy scripting language uses this for overloading a**b.
* @param b double to exponentiate, that is dimensionless.
* @return a Datum that is the exponentiate of the two values in this Datum's units.
*/
public Datum power( double b ) {
if ( UnitsUtil.isRatioMeasurement(units) ) {
return Datum.create( Math.pow( this.value(),2 ), Units.dimensionless );
} else {
throw new IllegalArgumentException("power argument must be dimensionless");
}
}
/**
* Groovy scripting language uses this for overloading a**b.
* @param datum Datum to exponentiate, that is dimensionless.
* @return a Datum that is the sum of the two values in this Datum's units.
*/
public Datum power( Datum datum ) {
if ( datum.getUnits()==Units.dimensionless && UnitsUtil.isRatioMeasurement(units) ) {
return Datum.create( Math.pow( this.value(),datum.value() ), Units.dimensionless );
} else {
throw new IllegalArgumentException("power argument must be dimensionless");
}
}
/**
* return log10 of this datum.
* @return log10 of this datum.
*/
public Datum log10() {
if ( this.getUnits()==Units.dimensionless ) {
return Units.dimensionless.createDatum( Math.log10( this.value() ) );
} else {
throw new IllegalArgumentException("units are not dimensionless");
}
}
/**
* return Math.pow(10,v)
* @return Math.pow(10,v)
*/
public Datum exp10() {
if ( this.getUnits()==Units.dimensionless ) {
return Units.dimensionless.createDatum( Math.pow(10,this.value() ) );
} else {
throw new IllegalArgumentException("units are not dimensionless");
}
}
/**
* Groovy scripting language uses this negation operator.
* @return a Datum such that it plus this is 0.
* @throws IllegalArgumentException when the units are location units.
*/
public Datum negative() {
if ( UnitsUtil.isRatioMeasurement(units) ) {
return Datum.create( -1*value(), units );
} else if ( UnitsUtil.isNominalMeasurement(units) ) {
throw new IllegalArgumentException("units are Stevens ordinal");
} else if ( UnitsUtil.isIntervalMeasurement(units) ) {
throw new IllegalArgumentException("units are Stevens interval measurement");
} else {
throw new IllegalArgumentException("units are not Stevens ratio measurement");
}
}
/**
* Groovy scripting language uses this positive operator.
* @return this datum.
*/
public Datum positive() {
return this;
}
/**
* returns a Datum whose value is the sum of {@code this} and {@code datum}, in {@code this.getUnits()}.
* @param datum Datum to add, that is convertible to this.getUnits().
* @return a Datum that is the sum of the two values in this Datum's units.
*/
public Datum add( Datum datum ) {
Datum result= add( datum.getValue(), datum.getUnits() );
result.resolution= Math.sqrt( datum.resolution * datum.resolution + this.resolution * this.resolution );
return result;
}
/**
* returns a Datum whose value is the sum of {@code this} and value in
* the context of {@code units}, in {@code this.getUnits()}.
* @param value a Number to add in the context of units.
* @param units units defining the context of value. There should be a converter from
* units to this Datum's units.
* @return value Datum that is the sum of the two values in this Datum's units.
*/
public Datum add( Number value, Units units ) {
return getUnits().add( getValue(), value, units );
}
/**
* returns a Datum whose value is the sum of {@code this} and value in
* the context of {@code units}, in {@code this.getUnits()}.
* @param d a Number to add in the context of units.
* @param units units defining the context of value. There should be a converter from
* units to this Datum's units.
* @return value Datum that is the sum of the two values in this Datum's units.
*/
public Datum add( double d, Units units ) {
return add( (Number)d, units );
}
/**
* returns a Datum whose value is the difference of {@code this} and {@code value}.
* The returned Datum will have units according to the type of units subtracted.
* For example, "1979-01-02T00:00" - "1979-01-01T00:00" = "24 hours" (this datum's unit's offset units),
* while "1979-01-02T00:00" - "1 hour" = "1979-01-01T23:00" (this datum's units.)
*
* Note also the resolution of the result is calculated.
*
* @return a Datum that is the sum of the two values in this Datum's units.
* @param datum Datum to add, that is convertible to this.getUnits() or offset units.
*/
public Datum subtract( Datum datum ) {
Datum result= subtract( datum.getValue(), datum.getUnits() );
result.resolution= Math.sqrt( datum.resolution * datum.resolution + this.resolution * this.resolution );
return result;
}
/**
* returns a Datum whose value is the difference of {@code this} and value in
* the context of {@code units}.
* The returned Datum will have units according to the type of units subtracted.
* For example, "1979-01-02T00:00" - "1979-01-01T00:00" = "24 hours" (this datum's unit's offset units),
* while "1979-01-02T00:00" - "1 hour" = "1979-01-01T23:00" (this datum's units.)
*
* @param a a Number to add in the context of units.
* @param units units defining the context of value. There should be a converter from
* units to this Datum's units or offset units.
* @return value Datum that is the difference of the two values in this Datum's units.
*/
public Datum subtract( Number a, Units units ) {
Datum result= getUnits().subtract( getValue(), a, units );
return result;
}
/**
* returns a Datum whose value is the difference of {@code this} and value in
* the context of {@code units}.
* The returned Datum will have units according to the type of units subtracted.
* For example, "1979-01-02T00:00" - "1979-01-01T00:00" = "24 hours" (this datum's unit's offset units),
* while "1979-01-02T00:00" - "1 hour" = "1979-01-01T23:00" (this datum's units.)
*
* @param d a Number to add in the context of units.
* @param units units defining the context of value. There should be a converter from
* units to this Datum's units or offset units.
* @return value Datum that is the difference of the two values in this Datum's units.
*/
public Datum subtract( double d, Units units ) {
return subtract( (Number)d, units );
}
private static double relativeErrorMult( double x, double dx, double y, double dy ) {
return Math.sqrt( dx/x * dx/x + dy/y * dy/y );
}
/**
* divide this by the datum {@code a}. Currently, only division is only supported:
* between convertable units, resulting in a Units.dimensionless quantity, or * by a Units.dimensionless quantity, and a datum with this datum's units is returned.* This may change, as a generic SI units class is planned. * * @param a the datum divisor. * @return the quotient. */ public Datum divide( Datum a ) { Datum result= divide( a.getValue(), a.getUnits() ); result.resolution= Math.abs( result.doubleValue() ) * relativeErrorMult( doubleValue(), resolution, a.doubleValue(), a.resolution ); return result; } /** * divide this by the Number provided in the context of units. Currently, only division is only supported:
* between convertable units, resulting in a Units.dimensionless quantity, or * by a Units.dimensionless quantity, and a datum with this datum's units is returned.* This may change, as a generic SI units class is planned. * @param a the magnitude of the divisor. * @param units the units of the divisor. * @return the quotient. */ public Datum divide( Number a, Units units ) { return getUnits().divide( getValue(), a, units ); } /** * divide this by the dimensionless double. * @param d the magnitude of the divisor. * @return the quotient. */ public Datum divide( double d ) { return divide( d, Units.dimensionless ); } /** * multiply this by the datum {@code a}. Currently, only multiplication is only supported * by a dimensionless datum, or when this is dimensionless. * This may change, as a generic SI units class is planned. * * This should also throw an IllegalArgumentException if the units are LocationUnits (e.g. UT time), but doesn't. This may * change. * * @param a the datum to multiply * @return the product. */ public Datum multiply( Datum a ) { Datum result= multiply( a.getValue(), a.getUnits() ); result.resolution= result.doubleValue() * relativeErrorMult( doubleValue(), resolution, a.doubleValue(), a.resolution ); return result; } /** * multiply this by the Number provided in the context of units. Currently, only multiplication is only supported * by a dimensionless datum, or when this is dimensionless. * This may change, as a generic SI units class is planned. * * This should also throw an IllegalArgumentException if the units are LocationUnits (e.g. UT time), but doesn't. This may * change. * * @param a the magnitude of the multiplier. * @param units the units of the multiplier. * @return the product. */ public Datum multiply( Number a, Units units ) { return getUnits().multiply( getValue(), a, units ); } /** * multiply by a dimensionless number. * * This should also throw an IllegalArgumentException if the units are LocationUnits (e.g. UT time), but doesn't. This may * change. * * @param d the multiplier. * @return the product. */ public Datum multiply( double d ) { return multiply( d, Units.dimensionless ); } /** * creates an equivalent datum using a different unit. For example,
* x= Datum.create( 5, Units.seconds );
* System.err.println( x.convertTo( Units.seconds ) );
*
* @param units the new Datum's units
* @throws java.lang.IllegalArgumentException if the datum cannot be converted to the given units.
* @return a datum with the new units, that is equal to the original datum.
*/
public Datum convertTo( Units units ) throws IllegalArgumentException {
UnitsConverter muc= this.units.getConverter(units);
Datum result= units.createDatum( muc.convert( this.getValue() ) );
if ( this.resolution!=0. ) {
muc= this.units.getOffsetUnits().getConverter(units.getOffsetUnits());
result.resolution= muc.convert(this.resolution);
}
return result;
}
/**
* returns a hashcode that is a function of the value and the units.
* @return a hashcode for the datum
*/
@Override
public int hashCode() {
long bits = (long) getValue().hashCode();
int doubleHash= (int)(bits ^ (bits >>> 32));
int unitsHash= units.hashCode(); //TODO: this should probably be converted to canonical unit to match equals. ("1 km".equals("1000 m")) is true.
return doubleHash ^ unitsHash;
}
/**
* returns true if the two datums are equal. That is, their double values are equal when converted to the same units.
* @param a the Object to compare to.
* @throws java.lang.IllegalArgumentException if the Object is not a datum or the units are not convertable.
* @return true if the datums are equal.
*/
@Override
public boolean equals( Object a ) throws IllegalArgumentException {
return ( a!=null && (a instanceof Datum) && this.equals( (Datum)a ) );
}
/**
* returns true if the two datums are equal. That is, their double values are equal when converted to the same units.
* @param a the datum to compare
* @throws java.lang.IllegalArgumentException if the units are not convertable.
* @return true if the datums are equal.
*/
public boolean equals( Datum a ) throws IllegalArgumentException {
return ( a!=null && this.getUnits().isConvertibleTo( a.getUnits() ) && this.compareTo(a)==0 );
}
/**
* returns true if this is less than {@code a}.
* @param a a datum convertible to this Datum's units.
* @throws java.lang.IllegalArgumentException if the two don't have convertible units.
* @return true if this is less than {@code a}.
*/
public boolean lt( Datum a ) throws IllegalArgumentException {
return (this.compareTo(a)<0);
}
/**
* returns true if this is greater than {@code a}.
* @param a a datum convertible to this Datum's units.
* @throws java.lang.IllegalArgumentException if the two don't have convertible units.
* @return true if this is greater than {@code a}.
*/
public boolean gt( Datum a ) throws IllegalArgumentException {
return (this.compareTo(a)>0);
}
/**
* returns true if this is less than or equal to {@code a}.
* @param a a datum convertible to this Datum's units.
* @throws java.lang.IllegalArgumentException if the two don't have convertible units.
* @return true if this is less than or equal to {@code a}.
*/
public boolean le( Datum a ) throws IllegalArgumentException {
return (this.compareTo(a)<=0);
}
/**
* returns true if this is greater than or equal to {@code a}.
* @param a a datum convertible to this Datum's units.
* @throws java.lang.IllegalArgumentException if the two don't have convertible units.
* @return true if this is greater than or equal to {@code a}.
*/
public boolean ge( Datum a ) throws IllegalArgumentException {
return (this.compareTo(a)>=0);
}
/**
* compare the datum to the object.
* @return an int < 0 if this comes before Datum a in this Datum's units space, 0 if they are equal, and > 0 otherwise.
* @param a the Object to compare this datum to.
* @throws IllegalArgumentException if a is not a Datum or is not convertible to this Datum's units.
*/
@Override
public int compareTo( Object a ) throws IllegalArgumentException {
if ( ! (a instanceof Datum) ) throw new IllegalArgumentException("comparable type mismatch");
return compareTo((Datum)a);
}
/**
* compare this to another datum.
* @return an int < 0 if this comes before Datum a in this Datum's units space,
* 0 if they are equal, and > 0 otherwise.
* @param a the Datum to compare this datum to.
* @throws IllegalArgumentException if a is not convertible to this Datum's
* units.
*/
public int compareTo( Datum a ) throws IllegalArgumentException {
if ( this.units != a.units ) {
a= a.convertTo(this.units);
}
if ( UnitsUtil.isNominalMeasurement(units) ) {
return this.toString().compareTo(a.toString());
}
double d= this.getValue().doubleValue() - a.getValue().doubleValue();
if (d==0.) {
return 0;
} else if ( d<0. ) {
return -1;
} else {
return 1;
}
//This should be studied carefully, running all tests!
//return java.lang.Double.compare( this.getValue().doubleValue(), a.getValue().doubleValue() );
}
/**
* returns true if the value is finite, that is not INFINITY or NaN.
* @return true if the value is finite, that is not INFINITY or NaN.
*/
public boolean isFinite() {
return ( value.doubleValue()!=java.lang.Double.POSITIVE_INFINITY )
&& ( value.doubleValue()!=java.lang.Double.NEGATIVE_INFINITY )
&& ( !java.lang.Double.isNaN( value.doubleValue() ) );
}
/**
* returns a human readable String representation of the Datum, which should also be parseable with
* Units.parse()
* @return a human readable String representation of the Datum, which should also be parseable with
* Units.parse()
*/
@Override
public String toString() {
Datum d= this;
if ( this.getUnits().isConvertibleTo(Units.seconds ) ) {
d= DatumUtil.asOrderOneUnits(d);
}
if (formatter==null) {
return units.getDatumFormatterFactory().defaultFormatter().format(d);
} else {
return formatter.format(d);
}
}
/**
* convenient method for creating a dimensionless Datum with the given value.
* @param value the magnitude of the datum.
* @return a dimensionless Datum with the given value.
*/
public static Datum create(double value) {
return Units.dimensionless.createDatum(value);
}
/**
* creates a datum with the given units and value, for example,
* {@code Datum.create( 54, Units.milliseconds )}
* @param value the magnitude of the datum.
* @param units the units of the datum.
* @return a Datum with the given units and value.
*/
public static Datum create( double value, Units units ) {
if ( units==null ) {
throw new NullPointerException("Units are null");
}
return units.createDatum( value );
}
/**
* Returns a Datum with a specific DatumFormatter attached to
* it. This was was used to limit resolution before limited resolution
* Datums were introduced.
*
* @param value the magnitude of the datum.
* @param units the units of the datum.
* @param formatter the DatumFormatter that should be used to format this datum, which will be
* returned by getFormatter().
* @return a Datum with the given units and value, that should return the given formatter when asked.
*/
public static Datum create( double value, Units units, DatumFormatter formatter ) {
Datum result= create( value, units);
result.formatter= formatter;
return result;
}
/**
* Returns a Datum with the given value and limited to the given resolution.
* When formatted, the formatter should use this resolution to limit the
* precision displayed.
* @param value the magnitude of the datum, or value to be interpreted in the context of units.
* @param units the units of the datum.
* @param resolution the limit to which the datum's precision is known.
* @return a Datum with the given units and value.
*/
public static Datum create( double value, Units units, double resolution ) {
Datum result= units.createDatum( value, resolution );
Datum res= units.getOffsetUnits().createDatum(resolution);
if ( UnitsUtil.isTimeLocation(units) ) {
double nanos= res.doubleValue( Units.nanoseconds );
if ( nanos<1000 ) {
result.formatter= TimeDatumFormatter.formatterForScale( TimeUtil.NANO, null );
} else if ( nanos<1000000 ) {
result.formatter= TimeDatumFormatter.formatterForScale( TimeUtil.MICRO, null );
} else {
result.formatter= units.getDatumFormatterFactory().defaultFormatter();
}
} else {
result.formatter= units.getDatumFormatterFactory().defaultFormatter();
}
return result;
}
/**
* Returns a Datum with the given value and limited to the given resolution.
* When formatted, the formatter should use this resolution to limit the
* precision displayed.
* @param value the magnitude of the datum, or value to be interpreted in the context of units.
* @param units the units of the datum.
* @param resolution the limit to which the datum's precision is known.
* @param formatter the DatumFormatter that should be used to format this datum, which will be
* returned by getFormatter().
* @return a Datum with the given units and value.
*/
public static Datum create( double value, Units units, double resolution, DatumFormatter formatter ) {
Datum result= units.createDatum( value, resolution );
result.formatter= formatter;
return result;
}
/**
* convenient method for creating a dimensionless Datum with the given value.
* @param value the magnitude of the datum.
* @return a dimensionless Datum with the given value.
*/
public static Datum create(long value) {
return Units.dimensionless.createDatum(value);
}
/**
* creates a datum with the given units and value, for example,
* {@code Datum.create( 54, Units.milliseconds )}
* @param value the magnitude of the datum.
* @param units the units of the datum.
* @return a Datum with the given units and value.
*/
public static Datum create( long value, Units units ) {
if ( units==null ) {
throw new NullPointerException("Units are null");
}
return units.createDatum( value );
}
/**
* Returns a Datum with a specific DatumFormatter attached to
* it. This was was used to limit resolution before limited resolution
* Datums were introduced.
*
* @param value the magnitude of the datum.
* @param units the units of the datum.
* @param formatter the DatumFormatter that should be used to format this datum, which will be
* returned by getFormatter().
* @return a Datum with the given units and value, that should return the given formatter when asked.
*/
public static Datum create( long value, Units units, DatumFormatter formatter ) {
Datum result= create( value, units);
result.formatter= formatter;
return result;
}
/**
* creates a dimensionless datum backed by an int.
* @return a dimensionless Datum with the given value.
* @param value the magnitude of the dimensionless datum.
*/
public static Datum create( int value ) {
return Units.dimensionless.createDatum( value );
}
/**
* creates a datum backed by an int with the given units.
* @return a Datum with the given units and value.
* @param value the magnitude of the datum
* @param units the units of the datum
*/
public static Datum create( int value, Units units ) {
return units.createDatum( value );
}
/**
* returns a formatter suitable for formatting this datum as a string.
* @return a formatter to be used to format this Datum into a String.
*/
public DatumFormatter getFormatter() {
return this.formatter;
}
}