blob: b8f9446557b62f50823471637808bcf6834ae820 [file] [log] [blame]
/*
* 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.sis.measure;
import java.math.BigDecimal;
import java.math.BigInteger;
import javax.measure.UnitConverter;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.StringBuilders;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.LenientComparable;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.math.Fraction;
import org.apache.sis.util.internal.Numerics;
import org.apache.sis.util.internal.DoubleDouble;
/**
* Conversions between units that can be represented by a linear operation (scale or offset).
* Note that the "linear" word in this class does not have the same meaning than the same word
* in the {@link #isLinear()} method inherited from JSR-385.
*
* <h2>Implementation note</h2>
* for performance reason we should create specialized subtypes for the case where there is only a scale to apply,
* or only an offset, <i>etc.</i> But we don't do that in Apache SIS implementation because we will rarely use the
* {@code UnitConverter} for converting a lot of values. We rather use {@code MathTransform} for operations on
* <var>n</var>-dimensional tuples, and unit conversions are only a special case of those more generic operations.
* The {@code org.apache.sis.referencing} module provided the specialized implementations needed for efficient
* operations and know how to copy the {@code UnitConverter} coefficients into an affine transform matrix.
*
* @author Martin Desruisseaux (Geomatys)
*/
final class LinearConverter extends AbstractConverter implements LenientComparable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -3759983642723729926L;
/**
* The scale to apply for converting values, before division by {@link #divisor}.
*/
private final double scale;
/**
* The offset to apply after the scale, before division by {@link #divisor}.
*/
private final double offset;
/**
* A divisor applied after the conversion.
* The complete formula used by Apache SIS is {@code y = (x*scale + offset) / divisor}.
* This division is mathematically unneeded since we could divide the offset and scale factor directly,
* but we keep it for accuracy reasons because most unit conversion factors are defined in base 10 and
* IEEE 754 cannot represent fractional values in base 10 accurately.
*/
private final double divisor;
/**
* The scale and offset factors represented in base 10, computed when first needed.
* Those terms are pre-divided by the {@linkplain #divisor}.
*/
private transient volatile BigDecimal scale10, offset10;
/**
* The inverse of this unit converter. Computed when first needed.
*/
private transient volatile LinearConverter inverse;
/**
* Creates a new linear converter for the given scale and offset.
* The complete formula applied is {@code y = (x*scale + offset) / divisor}.
*/
LinearConverter(final double scale, final double offset, final double divisor) {
this.scale = scale;
this.offset = offset;
this.divisor = divisor;
}
/**
* Creates a linear converter from the given scale and offset, which may be {@link BigDecimal} instances.
* This is the implementation of public {@link Units#converter(Number, Number)} method.
*/
static AbstractConverter create(final Number scale, final Number offset) {
final double numerator, divisor;
double shift = (offset != null) ? doubleValue(offset) : 0;
if (scale instanceof Fraction) {
numerator = ((Fraction) scale).numerator;
divisor = ((Fraction) scale).denominator;
shift *= divisor;
} else {
numerator = (scale != null) ? doubleValue(scale) : 1;
divisor = 1;
}
if (shift == 0 && numerator == divisor) {
return IdentityConverter.INSTANCE;
}
final LinearConverter c = new LinearConverter(numerator, shift, divisor);
if (scale instanceof BigDecimal) c.scale10 = (BigDecimal) scale;
if (offset instanceof BigDecimal) c.offset10 = (BigDecimal) offset;
return c;
}
/**
* Returns a linear converter for the given ratio. The scale factor is specified as a ratio because
* the unit conversion factors are often defined with a value in base 10. That value is considered
* exact by definition, but IEEE 754 has no exact representation of decimal fraction digits.
*
* <p>It is caller's responsibility to skip this method call when {@code numerator} = {@code denominator}.
* This method does not perform this check because it is usually already done (indirectly) by the caller.</p>
*/
static LinearConverter scale(final double numerator, final double denominator) {
return new LinearConverter(numerator, 0, denominator);
}
/**
* Returns a converter for the given shift. The translation is specified as a fraction because the
* unit conversion terms are often defined with a value in base 10. That value is considered exact
* by definition, but IEEE 754 has no exact representation of decimal fraction digits.
*
* <p>It is caller's responsibility to skip this method call when {@code numerator} = 0.
* This method does not perform this check because it is usually already done by the caller.</p>
*/
static LinearConverter offset(final double numerator, final double denominator) {
return new LinearConverter(denominator, numerator, denominator);
}
/**
* Raises the given converter to the given power. This method assumes that the given converter
* {@linkplain #isLinear() is linear} (this is not verified) and takes only the scale factor;
* the offset (if any) is ignored.
*
* <p>It is caller's responsibility to skip this method call when {@code n} = 1.
* This method does not perform this check because it is usually already done (indirectly) by the caller.</p>
*
* @param converter the converter to raise to the given power.
* @param n the exponent.
* @param root {@code true} for raising to 1/n instead of n.
* @return the converter raised to the given power.
*/
static LinearConverter pow(final UnitConverter converter, final int n, final boolean root) {
double numerator, denominator;
if (converter instanceof LinearConverter) {
final LinearConverter lc = (LinearConverter) converter;
numerator = lc.scale;
denominator = lc.divisor;
} else {
// Subtraction by convert(0) is a paranoiac safety.
numerator = converter.convert(1d) - converter.convert(0d);
denominator = 1;
}
if (root) {
switch (n) {
case 1: break;
case 2: numerator = Math.sqrt(numerator);
denominator = Math.sqrt(denominator);
break;
case 3: numerator = Math.cbrt(numerator);
denominator = Math.cbrt(denominator);
break;
default: final double r = 1.0 / n;
numerator = Math.pow(numerator, r);
denominator = Math.pow(denominator, r);
break;
}
} else {
numerator = (numerator == 10) ? MathFunctions.pow10(n) : Math.pow(numerator, n);
denominator = (denominator == 10) ? MathFunctions.pow10(n) : Math.pow(denominator, n);
}
return scale(numerator, denominator);
}
/**
* Indicates if this converter is linear.
* JSR-385 defines a converter as linear if:
*
* <ul>
* <li>{@code convert(u + v) == convert(u) + convert(v)}</li>
* <li>{@code convert(r * u) == r * convert(u)}</li>
* </ul>
*
* Note that this definition allows scale factors but does not allow offsets.
* Consequently, this is a different definition of "linear" than this class and the rest of Apache SIS.
*/
@Override
public boolean isLinear() {
return offset == 0;
}
/**
* Returns {@code true} if the effective scale factor is 1 and the offset is zero.
*/
@Override
public boolean isIdentity() {
return scale == divisor && offset == 0;
}
/**
* Returns {@code true} if this converter is close to an identity converter.
*/
final boolean almostIdentity() {
return epsilonEquals(scale, divisor) && epsilonEquals(offset, 0);
}
/**
* Returns the inverse of this unit converter.
* Given that the formula applied by this converter is:
*
* <pre class="math">
* y = (x⋅scale + offset) ∕ divisor</pre>
*
* the inverse formula is:
*
* <pre class="math">
* x = (y⋅divisor - offset) ∕ scale</pre>
*/
@Override
public synchronized UnitConverter inverse() {
if (inverse == null) {
inverse = isIdentity() ? this : new LinearConverter(divisor, -offset, scale);
inverse.inverse = this;
}
return inverse;
}
/**
* Returns the coefficient of this linear conversion.
* Coefficients are offset and scale, in that order.
*/
@Override
@SuppressWarnings("fallthrough")
final Number[] coefficients() {
final Number[] c = new Number[(scale != divisor) ? 2 : (offset != 0) ? 1 : 0];
switch (c.length) {
case 2: c[1] = ratio(scale, divisor);
case 1: c[0] = ratio(offset, divisor);
case 0: break;
}
return c;
}
/**
* Returns the given ratio as a {@link Fraction} if possible, or as a {@link Double} otherwise.
* The use of {@link Fraction} allows the {@link org.apache.sis.referencing.operation.matrix}
* package to perform more accurate calculations.
*/
private static Number ratio(final double value, final double divisor) {
if (value != 0) {
final int numerator = (int) value;
if (numerator == value) {
final int denominator = (int) divisor;
if (denominator == divisor) {
return (denominator == 1) ? numerator : new Fraction(numerator, denominator);
}
}
}
return value / divisor;
}
/**
* Applies the linear conversion on the given IEEE 754 floating-point value.
*/
@Override
public double convert(final double value) {
return Math.fma(value, scale, offset) / divisor;
}
/**
* Applies the linear conversion on the given value. This method uses {@link BigDecimal} arithmetic if
* the given value is an instance of {@code BigDecimal}, or double-double arithmetic if the value is an
* instance of {@link DoubleDouble}, or IEEE 754 floating-point arithmetic otherwise.
*/
@Override
public Number convert(Number value) {
ArgumentChecks.ensureNonNull("value", value);
if (!isIdentity()) {
if (value instanceof DoubleDouble) {
var dd = (DoubleDouble) value;
return dd.multiply(scale, true).add(offset, true).divide(divisor, true);
}
if (value instanceof BigInteger) {
value = new BigDecimal((BigInteger) value);
}
if (value instanceof BigDecimal) {
BigDecimal scale10 = this.scale10;
BigDecimal offset10 = this.offset10;
if (scale10 == null || offset10 == null) {
final BigDecimal divisor = BigDecimal.valueOf(this.divisor);
scale10 = BigDecimal.valueOf(scale) .divide(divisor);
offset10 = BigDecimal.valueOf(offset).divide(divisor);
this.scale10 = scale10; // Volatile fields
this.offset10 = offset10;
}
value = ((BigDecimal) value).multiply(scale10).add(offset10);
} else {
value = convert(doubleValue(value));
}
}
return value;
}
/**
* Returns the derivative of the conversion at the given value.
* For a linear converter, the derivative is the same everywhere.
*/
@Override
public double derivative(double value) {
return scale / divisor;
}
/**
* Concatenates this converter with another converter. The resulting converter is equivalent to first converting
* by the specified converter (right converter), and then converting by this converter (left converter). In the
* following equations, the 1 subscript is for the specified converter and the 2 subscript is for this converter:
*
* <pre class="math">
* t = (x⋅scale₁ + offset₁) ∕ divisor₁
* y = (t⋅scale₂ + offset₂) ∕ divisor₂</pre>
*
* We rewrite as:
*
* <pre class="math">
* y = (x⋅scale₁⋅scale₂ + offset₁⋅scale₂ + divisor₁⋅offset₂) ∕ (divisor₁⋅divisor₂)</pre>
*/
@Override
public UnitConverter concatenate(final UnitConverter converter) {
ArgumentChecks.ensureNonNull("converter", converter);
if (converter.isIdentity()) {
return this;
}
if (isIdentity()) {
return converter;
}
double otherScale, otherOffset, otherDivisor;
if (converter instanceof LinearConverter) {
final LinearConverter lc = (LinearConverter) converter;
otherScale = lc.scale;
otherOffset = lc.offset;
otherDivisor = lc.divisor;
} else if (converter.isLinear()) {
/*
* Fallback for foreigner implementations. Note that `otherOffset` should be restricted to zero
* according JSR-385 definition of `isLinear()`, but let be safe; maybe we are not the only one
* to have a different interpretation about the meaning of "linear".
*/
otherOffset = converter.convert(0.0);
otherScale = converter.convert(1.0) - otherOffset;
otherDivisor = 1;
} else {
return new ConcatenatedConverter(converter, this);
}
otherScale *= scale;
otherOffset = otherOffset * scale + otherDivisor * offset;
otherDivisor *= divisor;
/*
* Following loop is a little bit similar to simplifying a fraction, but checking only for the
* powers of 10 since unit conversions are often such values. Algorithm is not very efficient,
* but the loop should not be executed often.
*/
if (otherScale != 0 || otherOffset != 0 || otherDivisor != 0) {
double cf, f = 1;
do {
cf = f;
f *= 10;
} while (otherScale % f == 0 && otherOffset % f == 0 && otherDivisor % f == 0);
otherScale /= cf;
otherOffset /= cf;
otherDivisor /= cf;
}
if (otherOffset == 0 && otherScale == otherDivisor) {
return IdentityConverter.INSTANCE;
}
return new LinearConverter(otherScale, otherOffset, otherDivisor);
}
/**
* Returns a hash code value for this unit converter.
*/
@Override
public int hashCode() {
return Long.hashCode(Double.doubleToLongBits(scale)
+ 31 * (Double.doubleToLongBits(offset)
+ 37 * Double.doubleToLongBits(divisor)));
}
/**
* Compares this converter with the given object for equality.
*/
@Override
public boolean equals(final Object other) {
if (other instanceof LinearConverter) {
final LinearConverter o = (LinearConverter) other;
return Numerics.equals(scale, o.scale) &&
Numerics.equals(offset, o.offset) &&
Numerics.equals(divisor, o.divisor);
}
if (other instanceof IdentityConverter) {
return isIdentity();
}
return false;
}
/**
* Compares this converter with the given object for equality, optionally ignoring rounding errors.
*/
@Override
public boolean equals(final Object other, final ComparisonMode mode) {
if (mode.isApproximate()) {
if (other instanceof LinearConverter) {
return equivalent((LinearConverter) other);
} else if (other instanceof IdentityConverter) {
return almostIdentity();
} else {
return false;
}
} else {
return equals(other);
}
}
/**
* Returns {@code true} if the given converter perform the same conversion than this converter,
* except for rounding errors.
*/
final boolean equivalent(final LinearConverter other) {
return epsilonEquals(scale * other.divisor, other.scale * divisor) &&
epsilonEquals(offset * other.divisor, other.offset * divisor);
}
/**
* Returns a string representation of this converter for debugging purpose.
* This string representation may change in any future SIS release.
* Current format is of the form "y = scale⋅x + offset".
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder().append("y = ");
if (offset != 0) {
buffer.append('(');
}
if (scale != 1) {
StringBuilders.trimFractionalPart(buffer.append(scale));
buffer.append(AbstractUnit.MULTIPLY);
}
buffer.append('x');
if (offset != 0) {
StringBuilders.trimFractionalPart(buffer.append(" + ").append(offset));
buffer.append(')');
}
if (divisor != 1) {
StringBuilders.trimFractionalPart(buffer.append(AbstractUnit.DIVIDE).append(divisor));
}
return buffer.toString();
}
}