• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.math;
18 
19 import static com.google.common.math.Quantiles.median;
20 import static com.google.common.math.Quantiles.percentiles;
21 import static com.google.common.math.Quantiles.quartiles;
22 import static com.google.common.truth.Truth.assertThat;
23 import static com.google.common.truth.Truth.assertWithMessage;
24 import static java.lang.Double.NEGATIVE_INFINITY;
25 import static java.lang.Double.NaN;
26 import static java.lang.Double.POSITIVE_INFINITY;
27 import static java.math.RoundingMode.CEILING;
28 import static java.math.RoundingMode.FLOOR;
29 import static java.math.RoundingMode.UNNECESSARY;
30 import static org.junit.Assert.assertThrows;
31 
32 import com.google.common.collect.ImmutableList;
33 import com.google.common.collect.ImmutableMap;
34 import com.google.common.collect.Ordering;
35 import com.google.common.math.Quantiles.ScaleAndIndexes;
36 import com.google.common.primitives.Doubles;
37 import com.google.common.primitives.Ints;
38 import com.google.common.primitives.Longs;
39 import com.google.common.truth.Correspondence;
40 import com.google.common.truth.Correspondence.BinaryPredicate;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.List;
44 import java.util.Random;
45 import junit.framework.TestCase;
46 import org.checkerframework.checker.nullness.qual.Nullable;
47 
48 /**
49  * Tests for {@link Quantiles}.
50  *
51  * @author Pete Gillin
52  */
53 public class QuantilesTest extends TestCase {
54 
55   /*
56    * Since Quantiles provides a fluent-style API, each test covers a chain of methods resulting in
57    * the computation of one or more quantiles (or in an error) rather than individual methods. The
58    * tests are divided into three sections:
59    * 1. Tests on a hardcoded dataset for chains starting with median(), quartiles(), and scale(10);
60    * 2. Tests on hardcoded datasets include non-finite values for chains starting with scale(10);
61    * 3. Tests on a mechanically generated dataset for chains starting with percentiles();
62    * 4. Tests of illegal usages of the API.
63    */
64 
65   /*
66    * Covering every combination would lead to an explosion in the number of tests. So we cover only:
67    * - median with compute taking a double-collection and with computeInPlace;
68    * - quartiles with index and with indexes taking int-varargs, and with compute taking a
69    *   double-collection and with computeInPlace;
70    * - scale with index and with indexes taking int-varargs, and with all overloads of compute
71    *   taking a double-collection and with computeInPlace;
72    * - scale with indexes taking integer-collection with compute taking a double-collection and with
73    *   computeInPlace;
74    * - (except that, for non-finite values, we don't do all combinations exhaustively);
75    * - percentiles with index and with indexes taking int-varargs, and with compute taking a
76    *   double-collection and with computeInPlace.
77    */
78 
79   private static final double ALLOWED_ERROR = 1.0e-10;
80 
81   /**
82    * A {@link Correspondence} which accepts finite values within {@link #ALLOWED_ERROR} of each
83    * other.
84    */
85   private static final Correspondence<Number, Number> FINITE_QUANTILE_CORRESPONDENCE =
86       Correspondence.tolerance(ALLOWED_ERROR);
87 
88   /**
89    * A {@link Correspondence} which accepts either finite values within {@link #ALLOWED_ERROR} of
90    * each other or identical non-finite values.
91    */
92   private static final Correspondence<Double, Double> QUANTILE_CORRESPONDENCE =
93       Correspondence.from(
94           new BinaryPredicate<Double, Double>() {
95             @Override
96             public boolean apply(@Nullable Double actual, @Nullable Double expected) {
97               // Test for equality to allow non-finite values to match; otherwise, use the finite
98               // test.
99               return actual.equals(expected)
100                   || FINITE_QUANTILE_CORRESPONDENCE.compare(actual, expected);
101             }
102           },
103           "is identical to or " + FINITE_QUANTILE_CORRESPONDENCE);
104 
105   // 1. Tests on a hardcoded dataset for chains starting with median(), quartiles(), and scale(10):
106 
107   /** The squares of the 16 integers from 0 to 15, in an arbitrary order. */
108   private static final ImmutableList<Double> SIXTEEN_SQUARES_DOUBLES =
109       ImmutableList.of(
110           25.0, 100.0, 0.0, 144.0, 9.0, 121.0, 4.0, 225.0, 169.0, 64.0, 49.0, 16.0, 36.0, 1.0, 81.0,
111           196.0);
112 
113   private static final ImmutableList<Long> SIXTEEN_SQUARES_LONGS =
114       ImmutableList.of(
115           25L, 100L, 0L, 144L, 9L, 121L, 4L, 225L, 169L, 64L, 49L, 16L, 36L, 1L, 81L, 196L);
116   private static final ImmutableList<Integer> SIXTEEN_SQUARES_INTEGERS =
117       ImmutableList.of(25, 100, 0, 144, 9, 121, 4, 225, 169, 64, 49, 16, 36, 1, 81, 196);
118   private static final double SIXTEEN_SQUARES_MIN = 0.0;
119   private static final double SIXTEEN_SQUARES_DECILE_1 = 0.5 * (1.0 + 4.0);
120   private static final double SIXTEEN_SQUARES_QUARTILE_1 = 0.25 * 9.0 + 0.75 * 16.0;
121   private static final double SIXTEEN_SQUARES_MEDIAN = 0.5 * (49.0 + 64.0);
122   private static final double SIXTEEN_SQUARES_QUARTILE_3 = 0.75 * 121.0 + 0.25 * 144.0;
123   private static final double SIXTEEN_SQUARES_DECILE_8 = 144.0;
124   private static final double SIXTEEN_SQUARES_MAX = 225.0;
125 
testMedian_compute_doubleCollection()126   public void testMedian_compute_doubleCollection() {
127     assertThat(median().compute(SIXTEEN_SQUARES_DOUBLES))
128         .isWithin(ALLOWED_ERROR)
129         .of(SIXTEEN_SQUARES_MEDIAN);
130   }
131 
testMedian_computeInPlace()132   public void testMedian_computeInPlace() {
133     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
134     assertThat(median().computeInPlace(dataset)).isWithin(ALLOWED_ERROR).of(SIXTEEN_SQUARES_MEDIAN);
135     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES);
136   }
137 
testQuartiles_index_compute_doubleCollection()138   public void testQuartiles_index_compute_doubleCollection() {
139     assertThat(quartiles().index(1).compute(SIXTEEN_SQUARES_DOUBLES))
140         .isWithin(ALLOWED_ERROR)
141         .of(SIXTEEN_SQUARES_QUARTILE_1);
142   }
143 
testQuartiles_index_computeInPlace()144   public void testQuartiles_index_computeInPlace() {
145     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
146     assertThat(quartiles().index(1).computeInPlace(dataset))
147         .isWithin(ALLOWED_ERROR)
148         .of(SIXTEEN_SQUARES_QUARTILE_1);
149     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES);
150   }
151 
testQuartiles_indexes_varargs_compute_doubleCollection()152   public void testQuartiles_indexes_varargs_compute_doubleCollection() {
153     assertThat(quartiles().indexes(1, 3).compute(SIXTEEN_SQUARES_DOUBLES))
154         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
155         .containsExactly(1, SIXTEEN_SQUARES_QUARTILE_1, 3, SIXTEEN_SQUARES_QUARTILE_3);
156   }
157 
testQuartiles_indexes_varargs_computeInPlace()158   public void testQuartiles_indexes_varargs_computeInPlace() {
159     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
160     assertThat(quartiles().indexes(1, 3).computeInPlace(dataset))
161         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
162         .containsExactly(
163             1, SIXTEEN_SQUARES_QUARTILE_1,
164             3, SIXTEEN_SQUARES_QUARTILE_3);
165     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES);
166   }
167 
testScale_index_compute_doubleCollection()168   public void testScale_index_compute_doubleCollection() {
169     assertThat(Quantiles.scale(10).index(1).compute(SIXTEEN_SQUARES_DOUBLES))
170         .isWithin(ALLOWED_ERROR)
171         .of(SIXTEEN_SQUARES_DECILE_1);
172   }
173 
testScale_index_compute_longCollection()174   public void testScale_index_compute_longCollection() {
175     assertThat(Quantiles.scale(10).index(1).compute(SIXTEEN_SQUARES_LONGS))
176         .isWithin(ALLOWED_ERROR)
177         .of(SIXTEEN_SQUARES_DECILE_1);
178   }
179 
testScale_index_compute_integerCollection()180   public void testScale_index_compute_integerCollection() {
181     assertThat(Quantiles.scale(10).index(1).compute(SIXTEEN_SQUARES_INTEGERS))
182         .isWithin(ALLOWED_ERROR)
183         .of(SIXTEEN_SQUARES_DECILE_1);
184   }
185 
testScale_index_compute_doubleVarargs()186   public void testScale_index_compute_doubleVarargs() {
187     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
188     assertThat(Quantiles.scale(10).index(1).compute(dataset))
189         .isWithin(ALLOWED_ERROR)
190         .of(SIXTEEN_SQUARES_DECILE_1);
191     assertThat(dataset)
192         .usingExactEquality()
193         .containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES)
194         .inOrder();
195   }
196 
testScale_index_compute_longVarargs()197   public void testScale_index_compute_longVarargs() {
198     long[] dataset = Longs.toArray(SIXTEEN_SQUARES_LONGS);
199     assertThat(Quantiles.scale(10).index(1).compute(dataset))
200         .isWithin(ALLOWED_ERROR)
201         .of(SIXTEEN_SQUARES_DECILE_1);
202     assertThat(dataset).asList().isEqualTo(SIXTEEN_SQUARES_LONGS);
203   }
204 
testScale_index_compute_intVarargs()205   public void testScale_index_compute_intVarargs() {
206     int[] dataset = Ints.toArray(SIXTEEN_SQUARES_INTEGERS);
207     assertThat(Quantiles.scale(10).index(1).compute(dataset))
208         .isWithin(ALLOWED_ERROR)
209         .of(SIXTEEN_SQUARES_DECILE_1);
210     assertThat(dataset).asList().isEqualTo(SIXTEEN_SQUARES_INTEGERS);
211   }
212 
testScale_index_computeInPlace()213   public void testScale_index_computeInPlace() {
214     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
215     assertThat(Quantiles.scale(10).index(1).computeInPlace(dataset))
216         .isWithin(ALLOWED_ERROR)
217         .of(SIXTEEN_SQUARES_DECILE_1);
218     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES);
219   }
220 
testScale_index_computeInPlace_explicitVarargs()221   public void testScale_index_computeInPlace_explicitVarargs() {
222     assertThat(Quantiles.scale(10).index(5).computeInPlace(78.9, 12.3, 45.6))
223         .isWithin(ALLOWED_ERROR)
224         .of(45.6);
225   }
226 
testScale_indexes_varargs_compute_doubleCollection()227   public void testScale_indexes_varargs_compute_doubleCollection() {
228     // Note that we specify index 1 twice, which by the method contract should be ignored.
229     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(SIXTEEN_SQUARES_DOUBLES))
230         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
231         .containsExactly(
232             0, SIXTEEN_SQUARES_MIN,
233             10, SIXTEEN_SQUARES_MAX,
234             5, SIXTEEN_SQUARES_MEDIAN,
235             1, SIXTEEN_SQUARES_DECILE_1,
236             8, SIXTEEN_SQUARES_DECILE_8);
237   }
238 
testScale_indexes_varargs_compute_doubleCollection_snapshotsIndexes()239   public void testScale_indexes_varargs_compute_doubleCollection_snapshotsIndexes() {
240     // This test is the same as testScale_indexes_varargs_compute_doubleCollection except that the
241     // array of indexes to be calculated is modified between the calls to indexes and compute: since
242     // the contract is that it is snapshotted, this shouldn't make any difference to the result.
243     int[] indexes = {0, 10, 5, 1, 8, 10};
244     ScaleAndIndexes intermediate = Quantiles.scale(10).indexes(indexes);
245     indexes[0] = 3;
246     assertThat(intermediate.compute(SIXTEEN_SQUARES_DOUBLES))
247         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
248         .containsExactly(
249             0, SIXTEEN_SQUARES_MIN,
250             10, SIXTEEN_SQUARES_MAX,
251             5, SIXTEEN_SQUARES_MEDIAN,
252             1, SIXTEEN_SQUARES_DECILE_1,
253             8, SIXTEEN_SQUARES_DECILE_8);
254   }
255 
testScale_indexes_largeVarargs_compute_doubleCollection()256   public void testScale_indexes_largeVarargs_compute_doubleCollection() {
257     int scale = Integer.MAX_VALUE;
258     int otherIndex = (Integer.MAX_VALUE - 1) / 3; // this divides exactly
259     // For the otherIndex calculation, we have q=Integer.MAX_VALUE, k=(Integer.MAX_VALUE-1)/3, and
260     // N=16. Therefore k*(N-1)/q = 5-5/Integer.MAX_VALUE, which has floor 4 and fractional part
261     // (1-5/Integer.MAX_VALUE).
262     double otherValue = 16.0 * 5.0 / Integer.MAX_VALUE + 25.0 * (1.0 - 5.0 / Integer.MAX_VALUE);
263     assertThat(
264             Quantiles.scale(scale).indexes(0, scale, otherIndex).compute(SIXTEEN_SQUARES_DOUBLES))
265         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
266         .containsExactly(
267             0, SIXTEEN_SQUARES_MIN, scale, SIXTEEN_SQUARES_MAX, otherIndex, otherValue);
268   }
269 
testScale_indexes_varargs_compute_longCollection()270   public void testScale_indexes_varargs_compute_longCollection() {
271     // Note that we specify index 1 twice, which by the method contract should be ignored.
272     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(SIXTEEN_SQUARES_LONGS))
273         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
274         .containsExactly(
275             0, SIXTEEN_SQUARES_MIN,
276             10, SIXTEEN_SQUARES_MAX,
277             5, SIXTEEN_SQUARES_MEDIAN,
278             1, SIXTEEN_SQUARES_DECILE_1,
279             8, SIXTEEN_SQUARES_DECILE_8);
280   }
281 
testScale_indexes_varargs_compute_integerCollection()282   public void testScale_indexes_varargs_compute_integerCollection() {
283     // Note that we specify index 1 twice, which by the method contract should be ignored.
284     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(SIXTEEN_SQUARES_INTEGERS))
285         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
286         .containsExactly(
287             0, SIXTEEN_SQUARES_MIN,
288             10, SIXTEEN_SQUARES_MAX,
289             5, SIXTEEN_SQUARES_MEDIAN,
290             1, SIXTEEN_SQUARES_DECILE_1,
291             8, SIXTEEN_SQUARES_DECILE_8);
292   }
293 
testScale_indexes_varargs_compute_indexOrderIsMaintained()294   public void testScale_indexes_varargs_compute_indexOrderIsMaintained() {
295     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(SIXTEEN_SQUARES_INTEGERS))
296         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
297         .containsExactly(
298             0, SIXTEEN_SQUARES_MIN,
299             10, SIXTEEN_SQUARES_MAX,
300             5, SIXTEEN_SQUARES_MEDIAN,
301             1, SIXTEEN_SQUARES_DECILE_1,
302             8, SIXTEEN_SQUARES_DECILE_8)
303         .inOrder();
304   }
305 
testScale_indexes_varargs_compute_doubleVarargs()306   public void testScale_indexes_varargs_compute_doubleVarargs() {
307     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
308     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(dataset))
309         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
310         .containsExactly(
311             0, SIXTEEN_SQUARES_MIN,
312             10, SIXTEEN_SQUARES_MAX,
313             5, SIXTEEN_SQUARES_MEDIAN,
314             1, SIXTEEN_SQUARES_DECILE_1,
315             8, SIXTEEN_SQUARES_DECILE_8);
316     assertThat(dataset)
317         .usingExactEquality()
318         .containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES)
319         .inOrder();
320   }
321 
testScale_indexes_varargs_compute_longVarargs()322   public void testScale_indexes_varargs_compute_longVarargs() {
323     long[] dataset = Longs.toArray(SIXTEEN_SQUARES_LONGS);
324     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(dataset))
325         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
326         .containsExactly(
327             0, SIXTEEN_SQUARES_MIN,
328             10, SIXTEEN_SQUARES_MAX,
329             5, SIXTEEN_SQUARES_MEDIAN,
330             1, SIXTEEN_SQUARES_DECILE_1,
331             8, SIXTEEN_SQUARES_DECILE_8);
332     assertThat(dataset).asList().isEqualTo(SIXTEEN_SQUARES_LONGS);
333   }
334 
testScale_indexes_varargs_compute_intVarargs()335   public void testScale_indexes_varargs_compute_intVarargs() {
336     int[] dataset = Ints.toArray(SIXTEEN_SQUARES_INTEGERS);
337     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).compute(dataset))
338         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
339         .containsExactly(
340             0, SIXTEEN_SQUARES_MIN,
341             10, SIXTEEN_SQUARES_MAX,
342             5, SIXTEEN_SQUARES_MEDIAN,
343             1, SIXTEEN_SQUARES_DECILE_1,
344             8, SIXTEEN_SQUARES_DECILE_8);
345     assertThat(dataset).asList().isEqualTo(SIXTEEN_SQUARES_INTEGERS);
346   }
347 
testScale_indexes_varargs_computeInPlace()348   public void testScale_indexes_varargs_computeInPlace() {
349     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
350     assertThat(Quantiles.scale(10).indexes(0, 10, 5, 1, 8, 1).computeInPlace(dataset))
351         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
352         .containsExactly(
353             0, SIXTEEN_SQUARES_MIN,
354             10, SIXTEEN_SQUARES_MAX,
355             5, SIXTEEN_SQUARES_MEDIAN,
356             1, SIXTEEN_SQUARES_DECILE_1,
357             8, SIXTEEN_SQUARES_DECILE_8);
358     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES);
359   }
360 
testScale_indexes_varargs_computeInPlace_explicitVarargs()361   public void testScale_indexes_varargs_computeInPlace_explicitVarargs() {
362     assertThat(Quantiles.scale(10).indexes(0, 10).computeInPlace(78.9, 12.3, 45.6))
363         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
364         .containsExactly(
365             0, 12.3,
366             10, 78.9);
367   }
368 
testScale_indexes_collection_compute_doubleCollection()369   public void testScale_indexes_collection_compute_doubleCollection() {
370     // Note that we specify index 1 twice, which by the method contract should be ignored.
371     assertThat(
372             Quantiles.scale(10)
373                 .indexes(ImmutableList.of(0, 10, 5, 1, 8, 1))
374                 .compute(SIXTEEN_SQUARES_DOUBLES))
375         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
376         .containsExactly(
377             0, SIXTEEN_SQUARES_MIN,
378             10, SIXTEEN_SQUARES_MAX,
379             5, SIXTEEN_SQUARES_MEDIAN,
380             1, SIXTEEN_SQUARES_DECILE_1,
381             8, SIXTEEN_SQUARES_DECILE_8);
382   }
383 
testScale_indexes_collection_computeInPlace()384   public void testScale_indexes_collection_computeInPlace() {
385     double[] dataset = Doubles.toArray(SIXTEEN_SQUARES_DOUBLES);
386     assertThat(
387             Quantiles.scale(10)
388                 .indexes(ImmutableList.of(0, 10, 5, 1, 8, 1))
389                 .computeInPlace(dataset))
390         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
391         .containsExactly(
392             0, SIXTEEN_SQUARES_MIN,
393             10, SIXTEEN_SQUARES_MAX,
394             5, SIXTEEN_SQUARES_MEDIAN,
395             1, SIXTEEN_SQUARES_DECILE_1,
396             8, SIXTEEN_SQUARES_DECILE_8);
397     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(SIXTEEN_SQUARES_DOUBLES);
398   }
399 
400   // 2. Tests on hardcoded datasets include non-finite values for chains starting with scale(10):
401 
402   private static final ImmutableList<Double> ONE_TO_FIVE_AND_POSITIVE_INFINITY =
403       ImmutableList.of(3.0, 5.0, POSITIVE_INFINITY, 1.0, 4.0, 2.0);
404   private static final ImmutableList<Double> ONE_TO_FIVE_AND_NEGATIVE_INFINITY =
405       ImmutableList.of(3.0, 5.0, NEGATIVE_INFINITY, 1.0, 4.0, 2.0);
406   private static final ImmutableList<Double> NEGATIVE_INFINITY_AND_FIVE_POSITIVE_INFINITIES =
407       ImmutableList.of(
408           POSITIVE_INFINITY,
409           POSITIVE_INFINITY,
410           NEGATIVE_INFINITY,
411           POSITIVE_INFINITY,
412           POSITIVE_INFINITY,
413           POSITIVE_INFINITY);
414   private static final ImmutableList<Double> ONE_TO_FIVE_AND_NAN =
415       ImmutableList.of(3.0, 5.0, NaN, 1.0, 4.0, 2.0);
416 
testScale_indexes_varargs_compute_doubleCollection_positiveInfinity()417   public void testScale_indexes_varargs_compute_doubleCollection_positiveInfinity() {
418     assertThat(
419             Quantiles.scale(10)
420                 .indexes(0, 1, 2, 8, 9, 10)
421                 .compute(ONE_TO_FIVE_AND_POSITIVE_INFINITY))
422         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
423         .containsExactly(
424             0, 1.0,
425             1, 1.5,
426             2, 2.0,
427             8, 5.0,
428             9, POSITIVE_INFINITY, // interpolating between 5.0 and POSITIVE_INFINITY
429             10, POSITIVE_INFINITY);
430   }
431 
testScale_index_compute_doubleCollection_positiveInfinity()432   public void testScale_index_compute_doubleCollection_positiveInfinity() {
433     // interpolating between 5.0 and POSITIVE_INFINITY
434     assertThat(Quantiles.scale(10).index(9).compute(ONE_TO_FIVE_AND_POSITIVE_INFINITY))
435         .isPositiveInfinity();
436   }
437 
testScale_indexes_varargs_compute_doubleCollection_negativeInfinity()438   public void testScale_indexes_varargs_compute_doubleCollection_negativeInfinity() {
439     assertThat(
440             Quantiles.scale(10)
441                 .indexes(0, 1, 2, 8, 9, 10)
442                 .compute(ONE_TO_FIVE_AND_NEGATIVE_INFINITY))
443         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
444         .containsExactly(
445             0, NEGATIVE_INFINITY,
446             1, NEGATIVE_INFINITY, // interpolating between NEGATIVE_INFINITY and 1.0
447             2, 1.0,
448             8, 4.0,
449             9, 4.5,
450             10, 5.0);
451   }
452 
testScale_index_compute_doubleCollection_negativeInfinity()453   public void testScale_index_compute_doubleCollection_negativeInfinity() {
454     // interpolating between NEGATIVE_INFINITY and 1.0
455     assertThat(Quantiles.scale(10).index(1).compute(ONE_TO_FIVE_AND_NEGATIVE_INFINITY))
456         .isNegativeInfinity();
457   }
458 
testScale_indexes_varargs_compute_doubleCollection_bothInfinities()459   public void testScale_indexes_varargs_compute_doubleCollection_bothInfinities() {
460     assertThat(
461             Quantiles.scale(10)
462                 .indexes(0, 1, 2, 8, 9, 10)
463                 .compute(NEGATIVE_INFINITY_AND_FIVE_POSITIVE_INFINITIES))
464         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
465         .containsExactly(
466             0, NEGATIVE_INFINITY,
467             1, NaN, // interpolating between NEGATIVE_ and POSITIVE_INFINITY values
468             2, POSITIVE_INFINITY,
469             8, POSITIVE_INFINITY,
470             9, POSITIVE_INFINITY, // interpolating between two POSITIVE_INFINITY values
471             10, POSITIVE_INFINITY);
472   }
473 
testScale_indexes_varargs_compute_doubleCollection_nan()474   public void testScale_indexes_varargs_compute_doubleCollection_nan() {
475     assertThat(Quantiles.scale(10).indexes(0, 1, 2, 8, 9, 10).compute(ONE_TO_FIVE_AND_NAN))
476         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
477         .containsExactly(
478             0, NaN,
479             1, NaN,
480             2, NaN,
481             8, NaN,
482             9, NaN,
483             10, NaN);
484   }
485 
testScale_index_compute_doubleCollection_nan()486   public void testScale_index_compute_doubleCollection_nan() {
487     assertThat(Quantiles.scale(10).index(5).compute(ONE_TO_FIVE_AND_NAN)).isNaN();
488   }
489 
490   // 3. Tests on a mechanically generated dataset for chains starting with percentiles():
491 
492   private static final int PSEUDORANDOM_DATASET_SIZE = 9951;
493   private static final ImmutableList<Double> PSEUDORANDOM_DATASET = generatePseudorandomDataset();
494   private static final ImmutableList<Double> PSEUDORANDOM_DATASET_SORTED =
495       Ordering.natural().immutableSortedCopy(PSEUDORANDOM_DATASET);
496 
generatePseudorandomDataset()497   private static ImmutableList<Double> generatePseudorandomDataset() {
498     Random random = new Random(2211275185798966364L);
499     ImmutableList.Builder<Double> largeDatasetBuilder = ImmutableList.builder();
500     for (int i = 0; i < PSEUDORANDOM_DATASET_SIZE; i++) {
501       largeDatasetBuilder.add(random.nextGaussian());
502     }
503     return largeDatasetBuilder.build();
504   }
505 
expectedLargeDatasetPercentile(int index)506   private static double expectedLargeDatasetPercentile(int index) {
507     // We have q=100, k=index, and N=9951. Therefore k*(N-1)/q is 99.5*index. If index is even, that
508     // is an integer 199*index/2. If index is odd, that is halfway between floor(199*index/2) and
509     // ceil(199*index/2).
510     if (index % 2 == 0) {
511       int position = IntMath.divide(199 * index, 2, UNNECESSARY);
512       return PSEUDORANDOM_DATASET_SORTED.get(position);
513     } else {
514       int positionFloor = IntMath.divide(199 * index, 2, FLOOR);
515       int positionCeil = IntMath.divide(199 * index, 2, CEILING);
516       double lowerValue = PSEUDORANDOM_DATASET_SORTED.get(positionFloor);
517       double upperValue = PSEUDORANDOM_DATASET_SORTED.get(positionCeil);
518       return (lowerValue + upperValue) / 2.0;
519     }
520   }
521 
testPercentiles_index_compute_doubleCollection()522   public void testPercentiles_index_compute_doubleCollection() {
523     for (int index = 0; index <= 100; index++) {
524       assertWithMessage("quantile at index " + index)
525           .that(percentiles().index(index).compute(PSEUDORANDOM_DATASET))
526           .isWithin(ALLOWED_ERROR)
527           .of(expectedLargeDatasetPercentile(index));
528     }
529   }
530 
531   @AndroidIncompatible // slow
testPercentiles_index_computeInPlace()532   public void testPercentiles_index_computeInPlace() {
533     // Assert that the computation gives the correct result for all possible percentiles.
534     for (int index = 0; index <= 100; index++) {
535       double[] dataset = Doubles.toArray(PSEUDORANDOM_DATASET);
536       assertWithMessage("quantile at index " + index)
537           .that(percentiles().index(index).computeInPlace(dataset))
538           .isWithin(ALLOWED_ERROR)
539           .of(expectedLargeDatasetPercentile(index));
540     }
541 
542     // Assert that the dataset contains the same elements after the in-place computation (although
543     // they may be reordered). We only do this for one index rather than for all indexes, as it is
544     // quite expensive (quadratic in the size of PSEUDORANDOM_DATASET).
545     double[] dataset = Doubles.toArray(PSEUDORANDOM_DATASET);
546     @SuppressWarnings("unused")
547     double actual = percentiles().index(33).computeInPlace(dataset);
548     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(PSEUDORANDOM_DATASET);
549   }
550 
testPercentiles_indexes_varargsPairs_compute_doubleCollection()551   public void testPercentiles_indexes_varargsPairs_compute_doubleCollection() {
552     for (int index1 = 0; index1 <= 100; index1++) {
553       for (int index2 = 0; index2 <= 100; index2++) {
554         ImmutableMap.Builder<Integer, Double> expectedBuilder = ImmutableMap.builder();
555         expectedBuilder.put(index1, expectedLargeDatasetPercentile(index1));
556         if (index2 != index1) {
557           expectedBuilder.put(index2, expectedLargeDatasetPercentile(index2));
558         }
559         assertThat(percentiles().indexes(index1, index2).compute(PSEUDORANDOM_DATASET))
560             .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
561             .containsExactlyEntriesIn(expectedBuilder.buildOrThrow());
562       }
563     }
564   }
565 
testPercentiles_indexes_varargsAll_compute_doubleCollection()566   public void testPercentiles_indexes_varargsAll_compute_doubleCollection() {
567     ArrayList<Integer> indexes = new ArrayList<>();
568     ImmutableMap.Builder<Integer, Double> expectedBuilder = ImmutableMap.builder();
569     for (int index = 0; index <= 100; index++) {
570       indexes.add(index);
571       expectedBuilder.put(index, expectedLargeDatasetPercentile(index));
572     }
573     Random random = new Random(770683168895677741L);
574     Collections.shuffle(indexes, random);
575     assertThat(percentiles().indexes(Ints.toArray(indexes)).compute(PSEUDORANDOM_DATASET))
576         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
577         .containsExactlyEntriesIn(expectedBuilder.buildOrThrow());
578   }
579 
580   @AndroidIncompatible // slow
testPercentiles_indexes_varargsAll_computeInPlace()581   public void testPercentiles_indexes_varargsAll_computeInPlace() {
582     double[] dataset = Doubles.toArray(PSEUDORANDOM_DATASET);
583     List<Integer> indexes = new ArrayList<>();
584     ImmutableMap.Builder<Integer, Double> expectedBuilder = ImmutableMap.builder();
585     for (int index = 0; index <= 100; index++) {
586       indexes.add(index);
587       expectedBuilder.put(index, expectedLargeDatasetPercentile(index));
588     }
589     Random random = new Random(770683168895677741L);
590     Collections.shuffle(indexes, random);
591     assertThat(percentiles().indexes(Ints.toArray(indexes)).computeInPlace(dataset))
592         .comparingValuesUsing(QUANTILE_CORRESPONDENCE)
593         .containsExactlyEntriesIn(expectedBuilder.buildOrThrow());
594     assertThat(dataset).usingExactEquality().containsExactlyElementsIn(PSEUDORANDOM_DATASET);
595   }
596 
597   // 4. Tests of illegal usages of the API:
598 
599   private static final ImmutableList<Double> EMPTY_DATASET = ImmutableList.of();
600 
testScale_zero()601   public void testScale_zero() {
602     assertThrows(IllegalArgumentException.class, () -> Quantiles.scale(0));
603   }
604 
testScale_negative()605   public void testScale_negative() {
606     assertThrows(IllegalArgumentException.class, () -> Quantiles.scale(-4));
607   }
608 
testScale_index_negative()609   public void testScale_index_negative() {
610     Quantiles.Scale intermediate = Quantiles.scale(10);
611     assertThrows(IllegalArgumentException.class, () -> intermediate.index(-1));
612   }
613 
testScale_index_tooHigh()614   public void testScale_index_tooHigh() {
615     Quantiles.Scale intermediate = Quantiles.scale(10);
616     assertThrows(IllegalArgumentException.class, () -> intermediate.index(11));
617   }
618 
testScale_indexes_varargs_negative()619   public void testScale_indexes_varargs_negative() {
620     Quantiles.Scale intermediate = Quantiles.scale(10);
621     assertThrows(IllegalArgumentException.class, () -> intermediate.indexes(1, -1, 3));
622   }
623 
testScale_indexes_varargs_tooHigh()624   public void testScale_indexes_varargs_tooHigh() {
625     Quantiles.Scale intermediate = Quantiles.scale(10);
626     assertThrows(IllegalArgumentException.class, () -> intermediate.indexes(1, 11, 3));
627   }
628 
testScale_indexes_collection_negative()629   public void testScale_indexes_collection_negative() {
630     Quantiles.Scale intermediate = Quantiles.scale(10);
631     assertThrows(
632         IllegalArgumentException.class, () -> intermediate.indexes(ImmutableList.of(1, -1, 3)));
633   }
634 
testScale_indexes_collection_tooHigh()635   public void testScale_indexes_collection_tooHigh() {
636     Quantiles.Scale intermediate = Quantiles.scale(10);
637     assertThrows(
638         IllegalArgumentException.class, () -> intermediate.indexes(ImmutableList.of(1, 11, 3)));
639   }
640 
testScale_index_compute_doubleCollection_empty()641   public void testScale_index_compute_doubleCollection_empty() {
642     Quantiles.ScaleAndIndex intermediate = Quantiles.scale(10).index(3);
643     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(EMPTY_DATASET));
644   }
645 
testScale_index_compute_doubleVarargs_empty()646   public void testScale_index_compute_doubleVarargs_empty() {
647     Quantiles.ScaleAndIndex intermediate = Quantiles.scale(10).index(3);
648     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(new double[] {}));
649   }
650 
testScale_index_compute_longVarargs_empty()651   public void testScale_index_compute_longVarargs_empty() {
652     Quantiles.ScaleAndIndex intermediate = Quantiles.scale(10).index(3);
653     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(new long[] {}));
654   }
655 
testScale_index_compute_intVarargs_empty()656   public void testScale_index_compute_intVarargs_empty() {
657     Quantiles.ScaleAndIndex intermediate = Quantiles.scale(10).index(3);
658     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(new int[] {}));
659   }
660 
testScale_index_computeInPlace_empty()661   public void testScale_index_computeInPlace_empty() {
662     Quantiles.ScaleAndIndex intermediate = Quantiles.scale(10).index(3);
663     assertThrows(
664         IllegalArgumentException.class, () -> intermediate.computeInPlace(new double[] {}));
665   }
666 
testScale_indexes_varargs_compute_doubleCollection_empty()667   public void testScale_indexes_varargs_compute_doubleCollection_empty() {
668     Quantiles.ScaleAndIndexes intermediate = Quantiles.scale(10).indexes(1, 3, 5);
669     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(EMPTY_DATASET));
670   }
671 
testScale_indexes_varargs_compute_doubleVarargs_empty()672   public void testScale_indexes_varargs_compute_doubleVarargs_empty() {
673     Quantiles.ScaleAndIndexes intermediate = Quantiles.scale(10).indexes(1, 3, 5);
674     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(new double[] {}));
675   }
676 
testScale_indexes_varargs_compute_longVarargs_empty()677   public void testScale_indexes_varargs_compute_longVarargs_empty() {
678     Quantiles.ScaleAndIndexes intermediate = Quantiles.scale(10).indexes(1, 3, 5);
679     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(new long[] {}));
680   }
681 
testScale_indexes_varargs_compute_intVarargs_empty()682   public void testScale_indexes_varargs_compute_intVarargs_empty() {
683     Quantiles.ScaleAndIndexes intermediate = Quantiles.scale(10).indexes(1, 3, 5);
684     assertThrows(IllegalArgumentException.class, () -> intermediate.compute(new int[] {}));
685   }
686 
testScale_indexes_varargs_computeInPlace_empty()687   public void testScale_indexes_varargs_computeInPlace_empty() {
688     Quantiles.ScaleAndIndexes intermediate = Quantiles.scale(10).indexes(1, 3, 5);
689     assertThrows(
690         IllegalArgumentException.class, () -> intermediate.computeInPlace(new double[] {}));
691   }
692 
testScale_indexes_indexes_computeInPlace_empty()693   public void testScale_indexes_indexes_computeInPlace_empty() {
694     int[] emptyIndexes = {};
695     assertThrows(
696         IllegalArgumentException.class,
697         () -> {
698           Quantiles.ScaleAndIndexes unused = Quantiles.scale(10).indexes(emptyIndexes);
699         });
700   }
701 }
702