blob: 485b04660902e03e571ce352d8c6208890ccf967 [file] [log] [blame]
/*
* Copyright (C) 2012 The Guava Authors
*
* 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 com.google.common.math;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.math.DoubleUtils.isFinite;
import static java.lang.Double.NaN;
import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtIncompatible;
import com.google.errorprone.annotations.concurrent.LazyInit;
/**
* The representation of a linear transformation between real numbers {@code x} and {@code y}.
* Graphically, this is the specification of a straight line on a plane. The transformation can be
* expressed as {@code y = m * x + c} for finite {@code m} and {@code c}, unless it is a vertical
* transformation in which case {@code x} has a constant value for all {@code y}. In the
* non-vertical case, {@code m} is the slope of the transformation (and a horizontal transformation
* has zero slope).
*
* @author Pete Gillin
* @since 20.0
*/
@Beta
@GwtIncompatible
public abstract class LinearTransformation {
/**
* Start building an instance which maps {@code x = x1} to {@code y = y1}. Both arguments must be
* finite. Call either {@link LinearTransformationBuilder#and} or {@link
* LinearTransformationBuilder#withSlope} on the returned object to finish building the instance.
*/
public static LinearTransformationBuilder mapping(double x1, double y1) {
checkArgument(isFinite(x1) && isFinite(y1));
return new LinearTransformationBuilder(x1, y1);
}
/**
* This is an intermediate stage in the construction process. It is returned by {@link
* LinearTransformation#mapping}. You almost certainly don't want to keep instances around, but
* instead use method chaining. This represents a single point mapping, i.e. a mapping between one
* {@code x} and {@code y} value pair.
*
* @since 20.0
*/
public static final class LinearTransformationBuilder {
private final double x1;
private final double y1;
private LinearTransformationBuilder(double x1, double y1) {
this.x1 = x1;
this.y1 = y1;
}
/**
* Finish building an instance which also maps {@code x = x2} to {@code y = y2}. These values
* must not both be identical to the values given in the first mapping. If only the {@code x}
* values are identical, the transformation is vertical. If only the {@code y} values are
* identical, the transformation is horizontal (i.e. the slope is zero).
*/
public LinearTransformation and(double x2, double y2) {
checkArgument(isFinite(x2) && isFinite(y2));
if (x2 == x1) {
checkArgument(y2 != y1);
return new VerticalLinearTransformation(x1);
} else {
return withSlope((y2 - y1) / (x2 - x1));
}
}
/**
* Finish building an instance with the given slope, i.e. the rate of change of {@code y} with
* respect to {@code x}. The slope must not be {@code NaN}. It may be infinite, in which case
* the transformation is vertical. (If it is zero, the transformation is horizontal.)
*/
public LinearTransformation withSlope(double slope) {
checkArgument(!Double.isNaN(slope));
if (isFinite(slope)) {
double yIntercept = y1 - x1 * slope;
return new RegularLinearTransformation(slope, yIntercept);
} else {
return new VerticalLinearTransformation(x1);
}
}
}
/**
* Builds an instance representing a vertical transformation with a constant value of {@code x}.
* (The inverse of this will be a horizontal transformation.)
*/
public static LinearTransformation vertical(double x) {
checkArgument(isFinite(x));
return new VerticalLinearTransformation(x);
}
/**
* Builds an instance representing a horizontal transformation with a constant value of {@code y}.
* (The inverse of this will be a vertical transformation.)
*/
public static LinearTransformation horizontal(double y) {
checkArgument(isFinite(y));
double slope = 0.0;
return new RegularLinearTransformation(slope, y);
}
/**
* Builds an instance for datasets which contains {@link Double#NaN}. The {@link #isHorizontal}
* and {@link #isVertical} methods return {@code false} and the {@link #slope}, and {@link
* #transform} methods all return {@link Double#NaN}. The {@link #inverse} method returns the same
* instance.
*/
public static LinearTransformation forNaN() {
return NaNLinearTransformation.INSTANCE;
}
/** Returns whether this is a vertical transformation. */
public abstract boolean isVertical();
/** Returns whether this is a horizontal transformation. */
public abstract boolean isHorizontal();
/**
* Returns the slope of the transformation, i.e. the rate of change of {@code y} with respect to
* {@code x}. This must not be called on a vertical transformation (i.e. when {@link
* #isVertical()} is true).
*/
public abstract double slope();
/**
* Returns the {@code y} corresponding to the given {@code x}. This must not be called on a
* vertical transformation (i.e. when {@link #isVertical()} is true).
*/
public abstract double transform(double x);
/**
* Returns the inverse linear transformation. The inverse of a horizontal transformation is a
* vertical transformation, and vice versa. The inverse of the {@link #forNaN} transformation is
* itself. In all other cases, the inverse is a transformation such that applying both the
* original transformation and its inverse to a value gives you the original value give-or-take
* numerical errors. Calling this method multiple times on the same instance will always return
* the same instance. Calling this method on the result of calling this method on an instance will
* always return that original instance.
*/
public abstract LinearTransformation inverse();
private static final class RegularLinearTransformation extends LinearTransformation {
final double slope;
final double yIntercept;
@LazyInit LinearTransformation inverse;
RegularLinearTransformation(double slope, double yIntercept) {
this.slope = slope;
this.yIntercept = yIntercept;
this.inverse = null; // to be lazily initialized
}
RegularLinearTransformation(double slope, double yIntercept, LinearTransformation inverse) {
this.slope = slope;
this.yIntercept = yIntercept;
this.inverse = inverse;
}
@Override
public boolean isVertical() {
return false;
}
@Override
public boolean isHorizontal() {
return (slope == 0.0);
}
@Override
public double slope() {
return slope;
}
@Override
public double transform(double x) {
return x * slope + yIntercept;
}
@Override
public LinearTransformation inverse() {
LinearTransformation result = inverse;
return (result == null) ? inverse = createInverse() : result;
}
@Override
public String toString() {
return String.format("y = %g * x + %g", slope, yIntercept);
}
private LinearTransformation createInverse() {
if (slope != 0.0) {
return new RegularLinearTransformation(1.0 / slope, -1.0 * yIntercept / slope, this);
} else {
return new VerticalLinearTransformation(yIntercept, this);
}
}
}
private static final class VerticalLinearTransformation extends LinearTransformation {
final double x;
@LazyInit LinearTransformation inverse;
VerticalLinearTransformation(double x) {
this.x = x;
this.inverse = null; // to be lazily initialized
}
VerticalLinearTransformation(double x, LinearTransformation inverse) {
this.x = x;
this.inverse = inverse;
}
@Override
public boolean isVertical() {
return true;
}
@Override
public boolean isHorizontal() {
return false;
}
@Override
public double slope() {
throw new IllegalStateException();
}
@Override
public double transform(double x) {
throw new IllegalStateException();
}
@Override
public LinearTransformation inverse() {
LinearTransformation result = inverse;
return (result == null) ? inverse = createInverse() : result;
}
@Override
public String toString() {
return String.format("x = %g", x);
}
private LinearTransformation createInverse() {
return new RegularLinearTransformation(0.0, x, this);
}
}
private static final class NaNLinearTransformation extends LinearTransformation {
static final NaNLinearTransformation INSTANCE = new NaNLinearTransformation();
@Override
public boolean isVertical() {
return false;
}
@Override
public boolean isHorizontal() {
return false;
}
@Override
public double slope() {
return NaN;
}
@Override
public double transform(double x) {
return NaN;
}
@Override
public LinearTransformation inverse() {
return this;
}
@Override
public String toString() {
return "NaN";
}
}
}