001/*
002 * Copyright (C) 2012 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.math;
016
017import static com.google.common.base.Preconditions.checkArgument;
018import static com.google.common.math.DoubleUtils.isFinite;
019import static java.lang.Double.NaN;
020
021import com.google.common.annotations.Beta;
022import com.google.common.annotations.GwtIncompatible;
023
024/**
025 * The representation of a linear transformation between real numbers {@code x} and {@code y}.
026 * Graphically, this is the specification of a straight line on a plane. The transformation can be
027 * expressed as {@code y = m * x + c} for finite {@code m} and {@code c}, unless it is a vertical
028 * transformation in which case {@code x} has a constant value for all {@code y}. In the
029 * non-vertical case, {@code m} is the slope of the transformation (and a horizontal transformation
030 * has zero slope).
031 *
032 * @author Pete Gillin
033 * @since 20.0
034 */
035@Beta
036@GwtIncompatible
037public abstract class LinearTransformation {
038
039  /**
040   * Start building an instance which maps {@code x = x1} to {@code y = y1}. Both arguments must be
041   * finite. Call either {@link LinearTransformationBuilder#and} or
042   * {@link LinearTransformationBuilder#withSlope} on the returned object to finish building the
043   * instance.
044   */
045  public static LinearTransformationBuilder mapping(double x1, double y1) {
046    checkArgument(isFinite(x1) && isFinite(y1));
047    return new LinearTransformationBuilder(x1, y1);
048  }
049
050  /**
051   * This is an intermediate stage in the construction process. It is returned by
052   * {@link LinearTransformation#mapping}. You almost certainly don't want to keep instances around,
053   * but instead use method chaining. This represents a single point mapping, i.e. a mapping between
054   * one {@code x} and {@code y} value pair.
055   */
056  public static final class LinearTransformationBuilder {
057
058    private final double x1;
059    private final double y1;
060
061    private LinearTransformationBuilder(double x1, double y1) {
062      this.x1 = x1;
063      this.y1 = y1;
064    }
065
066    /**
067     * Finish building an instance which also maps {@code x = x2} to {@code y = y2}. These values
068     * must not both be identical to the values given in the first mapping. If only the {@code x}
069     * values are identical, the transformation is vertical. If only the {@code y} values are
070     * identical, the transformation is horizontal (i.e. the slope is zero).
071     */
072    public LinearTransformation and(double x2, double y2) {
073      checkArgument(isFinite(x2) && isFinite(y2));
074      if (x2 == x1) {
075        checkArgument(y2 != y1);
076        return new VerticalLinearTransformation(x1);
077      } else {
078        return withSlope((y2 - y1) / (x2 - x1));
079      }
080    }
081
082    /**
083     * Finish building an instance with the given slope, i.e. the rate of change of {@code y} with
084     * respect to {@code x}. The slope must not be {@code NaN}. It may be infinite, in which case
085     * the transformation is vertical. (If it is zero, the transformation is horizontal.)
086     */
087    public LinearTransformation withSlope(double slope) {
088      checkArgument(!Double.isNaN(slope));
089      if (isFinite(slope)) {
090        double yIntercept = y1 - x1 * slope;
091        return new RegularLinearTransformation(slope, yIntercept);
092      } else {
093        return new VerticalLinearTransformation(x1);
094      }
095    }
096  }
097
098  /**
099   * Builds an instance representing a vertical transformation with a constant value of {@code x}.
100   * (The inverse of this will be a horizontal transformation.)
101   */
102  public static LinearTransformation vertical(double x) {
103    checkArgument(isFinite(x));
104    return new VerticalLinearTransformation(x);
105  }
106
107  /**
108   * Builds an instance representing a horizontal transformation with a constant value of {@code y}.
109   * (The inverse of this will be a vertical transformation.)
110   */
111  public static LinearTransformation horizontal(double y) {
112    checkArgument(isFinite(y));
113    double slope = 0.0;
114    return new RegularLinearTransformation(slope, y);
115  }
116
117  /**
118   * Builds an instance for datasets which contains {@link Double#NaN}. The {@link #isHorizontal}
119   * and {@link #isVertical} methods return {@code false} and the {@link #slope}, and
120   * {@link #transform} methods all return {@link Double#NaN}. The {@link #inverse} method returns
121   * the same instance.
122   */
123  public static LinearTransformation forNaN() {
124    return NaNLinearTransformation.INSTANCE;
125  }
126
127  /**
128   * Returns whether this is a vertical transformation.
129   */
130  public abstract boolean isVertical();
131
132  /**
133   * Returns whether this is a horizontal transformation.
134   */
135  public abstract boolean isHorizontal();
136
137  /**
138   * Returns the slope of the transformation, i.e. the rate of change of {@code y} with respect to
139   * {@code x}. This must not be called on a vertical transformation (i.e. when
140   * {@link #isVertical()} is true).
141   */
142  public abstract double slope();
143
144  /**
145   * Returns the {@code y} corresponding to the given {@code x}. This must not be called on a
146   * vertical transformation (i.e. when {@link #isVertical()} is true).
147   */
148  public abstract double transform(double x);
149
150  /**
151   * Returns the inverse linear transformation. The inverse of a horizontal transformation is a
152   * vertical transformation, and vice versa. The inverse of the {@link #forNaN} transformation is
153   * itself. In all other cases, the inverse is a transformation such that applying both the
154   * original transformation and its inverse to a value gives you the original value give-or-take
155   * numerical errors. Calling this method multiple times on the same instance will always return
156   * the same instance. Calling this method on the result of calling this method on an instance will
157   * always return that original instance.
158   */
159  public abstract LinearTransformation inverse();
160
161  private static final class RegularLinearTransformation extends LinearTransformation {
162
163    final double slope;
164    final double yIntercept;
165
166    LinearTransformation inverse;
167
168    RegularLinearTransformation(double slope, double yIntercept) {
169      this.slope = slope;
170      this.yIntercept = yIntercept;
171      this.inverse = null; // to be lazily initialized
172    }
173
174    RegularLinearTransformation(double slope, double yIntercept, LinearTransformation inverse) {
175      this.slope = slope;
176      this.yIntercept = yIntercept;
177      this.inverse = inverse;
178    }
179
180    @Override
181    public boolean isVertical() {
182      return false;
183    }
184
185    @Override
186    public boolean isHorizontal() {
187      return (slope == 0.0);
188    }
189
190    @Override
191    public double slope() {
192      return slope;
193    }
194
195    @Override
196    public double transform(double x) {
197      return x * slope + yIntercept;
198    }
199
200    @Override
201    public LinearTransformation inverse() {
202      LinearTransformation result = inverse;
203      return (result == null) ? inverse = createInverse() : result;
204    }
205
206    @Override
207    public String toString() {
208      return String.format("y = %g * x + %g", slope, yIntercept);
209    }
210
211    private LinearTransformation createInverse() {
212      if (slope != 0.0) {
213        return new RegularLinearTransformation(1.0 / slope, -1.0 * yIntercept / slope, this);
214      } else {
215        return new VerticalLinearTransformation(yIntercept, this);
216      }
217    }
218  }
219
220  private static final class VerticalLinearTransformation extends LinearTransformation {
221
222    final double x;
223
224    LinearTransformation inverse;
225
226    VerticalLinearTransformation(double x) {
227      this.x = x;
228      this.inverse = null; // to be lazily initialized
229    }
230
231    VerticalLinearTransformation(double x, LinearTransformation inverse) {
232      this.x = x;
233      this.inverse = inverse;
234    }
235
236    @Override
237    public boolean isVertical() {
238      return true;
239    }
240
241    @Override
242    public boolean isHorizontal() {
243      return false;
244    }
245
246    @Override
247    public double slope() {
248      throw new IllegalStateException();
249    }
250
251    @Override
252    public double transform(double x) {
253      throw new IllegalStateException();
254    }
255
256    @Override
257    public LinearTransformation inverse() {
258      LinearTransformation result = inverse;
259      return (result == null) ? inverse = createInverse() : result;
260    }
261
262    @Override
263    public String toString() {
264      return String.format("x = %g", x);
265    }
266
267    private LinearTransformation createInverse() {
268      return new RegularLinearTransformation(0.0, x, this);
269    }
270  }
271
272  private static final class NaNLinearTransformation extends LinearTransformation {
273
274    static final NaNLinearTransformation INSTANCE = new NaNLinearTransformation();
275
276    @Override
277    public boolean isVertical() {
278      return false;
279    }
280
281    @Override
282    public boolean isHorizontal() {
283      return false;
284    }
285
286    @Override
287    public double slope() {
288      return NaN;
289    }
290
291    @Override
292    public double transform(double x) {
293      return NaN;
294    }
295
296    @Override
297    public LinearTransformation inverse() {
298      return this;
299    }
300
301    @Override
302    public String toString() {
303      return "NaN";
304    }
305  }
306}