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