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