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