View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.numbers.complex;
19  
20  import java.io.BufferedReader;
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.ObjectInputStream;
26  import java.io.ObjectOutputStream;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.function.Consumer;
30  
31  import org.apache.commons.numbers.core.Precision;
32  
33  import org.junit.jupiter.api.Assertions;
34  
35  /**
36   * Test utilities. TODO: Cleanup (remove unused and obsolete methods).
37   */
38  public final class TestUtils {
39  
40      /**
41       * The option for how to process test data lines flagged (prefixed)
42       * with the {@code ;} character
43       */
44      public enum TestDataFlagOption {
45          /** Ignore the line. */
46          IGNORE,
47          /** Load the data by stripping the {@code ;} character. */
48          LOAD
49      }
50  
51      /**
52       * Collection of static methods used in math unit tests.
53       */
54      private TestUtils() {
55      }
56  
57      /**
58       * Verifies that real and imaginary parts of the two complex arguments are
59       * exactly the same as defined by {@link Double#compare(double, double)}. Also
60       * ensures that NaN / infinite components match.
61       *
62       * @param expected the expected value
63       * @param actual the actual value
64       */
65      public static void assertSame(Complex expected, Complex actual) {
66          Assertions.assertEquals(expected.getReal(), actual.getReal());
67          Assertions.assertEquals(expected.getImaginary(), actual.getImaginary());
68      }
69  
70      /**
71       * Verifies that real and imaginary parts of the two complex arguments differ by
72       * at most delta. Also ensures that NaN / infinite components match.
73       *
74       * @param expected the expected value
75       * @param actual the actual value
76       * @param delta the delta
77       */
78      public static void assertEquals(Complex expected, Complex actual, double delta) {
79          Assertions.assertEquals(expected.getReal(), actual.getReal(), delta);
80          Assertions.assertEquals(expected.getImaginary(), actual.getImaginary(), delta);
81      }
82  
83      /**
84       * Serializes an object to a bytes array and then recovers the object from the
85       * bytes array. Returns the deserialized object.
86       *
87       * @param o object to serialize and recover
88       * @return the recovered, deserialized object
89       */
90      public static Object serializeAndRecover(Object o) {
91          try {
92              // serialize the Object
93              final ByteArrayOutputStream bos = new ByteArrayOutputStream();
94              final ObjectOutputStream so = new ObjectOutputStream(bos);
95              so.writeObject(o);
96  
97              // deserialize the Object
98              final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
99              final ObjectInputStream si = new ObjectInputStream(bis);
100             return si.readObject();
101         } catch (final IOException ioe) {
102             return null;
103         } catch (final ClassNotFoundException cnfe) {
104             return null;
105         }
106     }
107 
108     /**
109      * Verifies that serialization preserves equals and hashCode. Serializes the
110      * object, then recovers it and checks equals and hash code.
111      *
112      * @param object the object to serialize and recover
113      */
114     public static void checkSerializedEquality(Object object) {
115         final Object object2 = serializeAndRecover(object);
116         Assertions.assertEquals(object, object2, "Equals check");
117         Assertions.assertEquals(object.hashCode(), object2.hashCode(), "HashCode check");
118     }
119 
120     /**
121      * Verifies that the relative error in actual vs. expected is less than or equal
122      * to relativeError. If expected is infinite or NaN, actual must be the same
123      * (NaN or infinity of the same sign).
124      *
125      * @param expected expected value
126      * @param actual observed value
127      * @param relativeError maximum allowable relative error
128      */
129     public static void assertRelativelyEquals(double expected, double actual, double relativeError) {
130         assertRelativelyEquals(null, expected, actual, relativeError);
131     }
132 
133     /**
134      * Verifies that the relative error in actual vs. expected is less than or equal
135      * to relativeError. If expected is infinite or NaN, actual must be the same
136      * (NaN or infinity of the same sign).
137      *
138      * @param msg message to return with failure
139      * @param expected expected value
140      * @param actual observed value
141      * @param relativeError maximum allowable relative error
142      */
143     public static void assertRelativelyEquals(String msg, double expected, double actual, double relativeError) {
144         if (Double.isNaN(expected)) {
145             Assertions.assertTrue(Double.isNaN(actual), msg);
146         } else if (Double.isNaN(actual)) {
147             Assertions.assertTrue(Double.isNaN(expected), msg);
148         } else if (Double.isInfinite(actual) || Double.isInfinite(expected)) {
149             Assertions.assertEquals(expected, actual, relativeError);
150         } else if (expected == 0.0) {
151             Assertions.assertEquals(actual, expected, relativeError, msg);
152         } else {
153             final double absError = Math.abs(expected) * relativeError;
154             Assertions.assertEquals(expected, actual, absError, msg);
155         }
156     }
157 
158     /**
159      * Fails iff values does not contain a number within epsilon of z.
160      *
161      * @param msg message to return with failure
162      * @param values complex array to search
163      * @param z value sought
164      * @param epsilon tolerance
165      */
166     public static void assertContains(String msg, Complex[] values, Complex z, double epsilon) {
167         for (final Complex value : values) {
168             if (Precision.equals(value.getReal(), z.getReal(), epsilon) &&
169                     Precision.equals(value.getImaginary(), z.getImaginary(), epsilon)) {
170                 return;
171             }
172         }
173         Assertions.fail(msg + " Unable to find " + z);
174     }
175 
176     /**
177      * Fails iff values does not contain a number within epsilon of z.
178      *
179      * @param values complex array to search
180      * @param z value sought
181      * @param epsilon tolerance
182      */
183     public static void assertContains(Complex[] values, Complex z, double epsilon) {
184         assertContains(null, values, z, epsilon);
185     }
186 
187     /**
188      * Fails iff values does not contain a number within epsilon of x.
189      *
190      * @param msg message to return with failure
191      * @param values double array to search
192      * @param x value sought
193      * @param epsilon tolerance
194      */
195     public static void assertContains(String msg, double[] values, double x, double epsilon) {
196         for (final double value : values) {
197             if (Precision.equals(value, x, epsilon)) {
198                 return;
199             }
200         }
201         Assertions.fail(msg + " Unable to find " + x);
202     }
203 
204     /**
205      * Fails iff values does not contain a number within epsilon of x.
206      *
207      * @param values double array to search
208      * @param x value sought
209      * @param epsilon tolerance
210      */
211     public static void assertContains(double[] values, double x, double epsilon) {
212         assertContains(null, values, x, epsilon);
213     }
214 
215     /** verifies that two arrays are close (sup norm) */
216     public static void assertEquals(String msg, Complex[] expected, Complex[] observed, double tolerance) {
217         final StringBuilder out = new StringBuilder(msg);
218         if (expected.length != observed.length) {
219             out.append("\n Arrays not same length. \n");
220             out.append("expected has length ");
221             out.append(expected.length);
222             out.append(" observed length = ");
223             out.append(observed.length);
224             Assertions.fail(out.toString());
225         }
226         boolean failure = false;
227         for (int i = 0; i < expected.length; i++) {
228             if (!Precision.equalsIncludingNaN(expected[i].getReal(), observed[i].getReal(), tolerance)) {
229                 failure = true;
230                 out.append("\n Real elements at index ");
231                 out.append(i);
232                 out.append(" differ. ");
233                 out.append(" expected = ");
234                 out.append(expected[i].getReal());
235                 out.append(" observed = ");
236                 out.append(observed[i].getReal());
237             }
238             if (!Precision.equalsIncludingNaN(expected[i].getImaginary(), observed[i].getImaginary(), tolerance)) {
239                 failure = true;
240                 out.append("\n Imaginary elements at index ");
241                 out.append(i);
242                 out.append(" differ. ");
243                 out.append(" expected = ");
244                 out.append(expected[i].getImaginary());
245                 out.append(" observed = ");
246                 out.append(observed[i].getImaginary());
247             }
248         }
249         if (failure) {
250             Assertions.fail(out.toString());
251         }
252     }
253 
254     /**
255      * Updates observed counts of values in quartiles. counts[0] <-> 1st quartile
256      * ... counts[3] <-> top quartile
257      */
258     public static void updateCounts(double value, long[] counts, double[] quartiles) {
259         if (value < quartiles[0]) {
260             counts[0]++;
261         } else if (value > quartiles[2]) {
262             counts[3]++;
263         } else if (value > quartiles[1]) {
264             counts[2]++;
265         } else {
266             counts[1]++;
267         }
268     }
269 
270     /**
271      * Eliminates points with zero mass from densityPoints and densityValues
272      * parallel arrays. Returns the number of positive mass points and collapses the
273      * arrays so that the first <returned value> elements of the input arrays
274      * represent the positive mass points.
275      */
276     public static int eliminateZeroMassPoints(int[] densityPoints, double[] densityValues) {
277         int positiveMassCount = 0;
278         for (int i = 0; i < densityValues.length; i++) {
279             if (densityValues[i] > 0) {
280                 positiveMassCount++;
281             }
282         }
283         if (positiveMassCount < densityValues.length) {
284             final int[] newPoints = new int[positiveMassCount];
285             final double[] newValues = new double[positiveMassCount];
286             int j = 0;
287             for (int i = 0; i < densityValues.length; i++) {
288                 if (densityValues[i] > 0) {
289                     newPoints[j] = densityPoints[i];
290                     newValues[j] = densityValues[i];
291                     j++;
292                 }
293             }
294             System.arraycopy(newPoints, 0, densityPoints, 0, positiveMassCount);
295             System.arraycopy(newValues, 0, densityValues, 0, positiveMassCount);
296         }
297         return positiveMassCount;
298     }
299 
300     /**
301      * Load test data from resources.
302      *
303      * <p>This method can be used to load input complex numbers and the expected result
304      * after applying a function.
305      *
306      * <p>Data is assumed to be a resource available to the class loader. The data should
307      * be space delimited doubles. Each pair of doubles on a line is converted to a
308      * Complex. For example the following represents the numbers (0.5 - 0 i) and (1.5 + 2
309      * i):
310      *
311      * <pre>
312      * 0.5 -0.0 1.5 2
313      * </pre>
314      *
315      * <p>An unmatched double not part of a pair on a line will raise an AssertionError.
316      *
317      * <p>Lines starting with the {@code #} character are ignored.
318      *
319      * <p>Lines starting with the {@code ;} character are processed using the provided
320      * flag option. This character can be used to disable tests in the data file.
321      *
322      * <p>The flagged data will be passed to the consumer.
323      *
324      * @param name the resource name
325      * @param option the option controlling processing of flagged data
326      * @param flaggedDataConsumer the flagged data consumer (can be null)
327      * @return the list
328      */
329     public static List<Complex[]> loadTestData(String name, TestDataFlagOption option,
330             Consumer<String> flaggedDataConsumer) {
331         final List<Complex[]> data = new ArrayList<>();
332         try (BufferedReader input = new BufferedReader(
333                 new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream(name)))) {
334             for  (String line = input.readLine(); line != null; line = input.readLine()) {
335                 line = preprocessTestData(line, option, flaggedDataConsumer);
336                 if (line == null) {
337                     continue;
338                 }
339                 final String[] parts = line.split(" ");
340                 if ((parts.length & 0x1) == 1) {
341                     Assertions.fail("Odd count of numbers on the line: " + line);
342                 }
343                 final Complex[] numbers = new Complex[parts.length / 2];
344                 for (int i = 0; i < parts.length; i += 2) {
345                     final double a = Double.parseDouble(parts[i]);
346                     final double b = Double.parseDouble(parts[i + 1]);
347                     numbers[i / 2] = Complex.ofCartesian(a, b);
348                 }
349                 data.add(numbers);
350             }
351         } catch (NumberFormatException | IOException e) {
352             Assertions.fail("Failed to load test data: " + name, e);
353         }
354         return data;
355     }
356 
357     /**
358      * Pre-process the next line of data from the input.
359      * Returns null when the line should be ignored.
360      *
361      * @param input the input
362      * @param option the option controlling processing of flagged data
363      * @param flaggedDataConsumer the flagged data consumer (can be null)
364      * @return the line of data (or null)
365      * @throws IOException Signals that an I/O exception has occurred.
366      */
367     private static String preprocessTestData(String line, TestDataFlagOption option,
368             Consumer<String> flaggedDataConsumer) {
369         // Skip comments and empty lines
370         if (line.isEmpty() || line.charAt(0) == '#') {
371             return null;
372         }
373         if (line.charAt(0) == ';') {
374             switch (option) {
375             case LOAD:
376                 // Strip the leading character
377                 line = line.substring(1);
378                 break;
379             case IGNORE:
380             default:
381                 if (flaggedDataConsumer != null) {
382                     flaggedDataConsumer.accept(line.substring(1));
383                 }
384                 // Ignore the line
385                 line = null;
386             }
387         }
388         return line;
389     }
390 }