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  package org.apache.commons.numbers.fraction;
18  
19  import java.util.Locale;
20  import org.junit.jupiter.api.Assertions;
21  import org.junit.jupiter.api.Test;
22  import org.junit.jupiter.params.ParameterizedTest;
23  import org.junit.jupiter.params.provider.ValueSource;
24  
25  /**
26   * Tests for {@link ContinuedFraction}.
27   */
28  class ContinuedFractionTest {
29      /** Golden ratio constant. */
30      private static final double GOLDEN_RATIO = 1.618033988749894848204586834365638117720309;
31  
32      /**
33       * Evaluates the golden ratio.
34       *
35       * <pre>
36       * 1 +        1
37       *     ----------------
38       *     1 +      1
39       *         ------------
40       *         1 +     1
41       *              -------
42       *              1 + ...
43       * </pre>
44       *
45       * <p>This is used to test various conditions for the {@link ContinuedFraction}.
46       *
47       * @see <a href="https://mathworld.wolfram.com/GoldenRatio.html">MathWorld Golden
48       * Ratio equation 17</a>
49       */
50      private static final class GoldenRatio extends ContinuedFraction {
51          private static final GoldenRatio INSTANCE = new GoldenRatio();
52  
53          /**
54           * @return single instance of GoldenRatio
55           */
56          static GoldenRatio getInstance() {
57              return INSTANCE;
58          }
59  
60          /** {@inheritDoc} */
61          @Override
62          public double getA(int n, double x) {
63              return 1;
64          }
65  
66          /** {@inheritDoc} */
67          @Override
68          public double getB(int n, double x) {
69              return 1;
70          }
71      }
72  
73      @Test
74      void testGoldenRatio() {
75          final double eps = 1e-8;
76          final double gr = GoldenRatio.getInstance().evaluate(0, eps);
77          Assertions.assertEquals(GOLDEN_RATIO, gr, GOLDEN_RATIO * eps);
78      }
79  
80      /**
81       * Test a continued fraction where the leading term is zero.
82       * Evaluates the reciprocal of the golden ratio.
83       */
84      @Test
85      void testGoldenRatioReciprocal() {
86          final double eps = 1e-8;
87          final ContinuedFraction cf = new ContinuedFraction() {
88              @Override
89              public double getA(int n, double x) {
90                  // Check this is not called with n=0
91                  Assertions.assertNotEquals(0, n, "a0 should never require evaluation");
92                  return 1;
93              }
94  
95              @Override
96              public double getB(int n, double x) {
97                  // b0 = 0
98                  return n == 0 ? 0 : 1;
99              }
100         };
101         final double gr = cf.evaluate(0, eps);
102         Assertions.assertEquals(1 / GOLDEN_RATIO, gr, eps / GOLDEN_RATIO);
103     }
104 
105     /**
106      * Test an invalid epsilon (zero, negative or NaN).
107      * See NUMBERS-173.
108      *
109      * @param epsilon Epsilon
110      */
111     @ParameterizedTest
112     @ValueSource(doubles = {0, -1, Double.NaN})
113     void testGoldenRatioEpsilonZero(double epsilon) {
114         // An invalid epsilon is set to the minimum epsilon.
115         // It should converge to 1 ULP.
116         final double tolerance = Math.ulp(GOLDEN_RATIO);
117         Assertions.assertEquals(GOLDEN_RATIO, GoldenRatio.getInstance().evaluate(0, epsilon), tolerance);
118     }
119 
120     @Test
121     void test415Over93() {
122         // https://en.wikipedia.org/wiki/Continued_fraction
123         // 415             1
124         // ---  = 4 + ---------
125         //  93        2 +   1
126         //                -----
127         //                6 + 1
128         //                    -
129         //                    7
130         //      = [4; 2, 6, 7]
131 
132         final ContinuedFraction cf = new ContinuedFraction() {
133             @Override
134             public double getA(int n, double x) {
135                 return n <= 3 ? 1 : 0;
136             }
137 
138             @Override
139             public double getB(int n, double x) {
140                 switch (n) {
141                 case 0:
142                     return 4;
143                 case 1:
144                     return 2;
145                 case 2:
146                     return 6;
147                 case 3:
148                     return 7;
149                 default:
150                     return 1;
151                 }
152             }
153         };
154 
155         final double eps = 1e-8;
156         final double gr = cf.evaluate(0, eps, 5);
157         Assertions.assertEquals(415.0 / 93.0, gr, eps);
158     }
159 
160     @Test
161     void testMaxIterationsThrows() {
162         final ContinuedFraction cf = GoldenRatio.getInstance();
163 
164         final double eps = 1e-8;
165         final int maxIterations = 3;
166         final Throwable t = Assertions.assertThrows(FractionException.class, () -> cf.evaluate(0, eps, maxIterations));
167         assertExceptionMessageContains(t, "max");
168     }
169 
170     @Test
171     void testNaNThrows() {
172         // Create a NaN during the iteration
173         final ContinuedFraction cf = new ContinuedFraction() {
174             @Override
175             public double getA(int n, double x) {
176                 return 1;
177             }
178 
179             @Override
180             public double getB(int n, double x) {
181                 return n == 0 ? 1 : Double.NaN;
182             }
183         };
184 
185         final double eps = 1e-8;
186         final Throwable t = Assertions.assertThrows(FractionException.class, () -> cf.evaluate(0, eps, 5));
187         assertExceptionMessageContains(t, "nan");
188     }
189 
190     @Test
191     void testInfThrows() {
192         // Create an infinity during the iteration:
193         // a / cPrev => a_1 / b_0 => Double.MAX_VALUE / 0.5
194         final ContinuedFraction cf = new ContinuedFraction() {
195             @Override
196             public double getA(int n, double x) {
197                 return n == 0 ? 1 : Double.MAX_VALUE;
198             }
199 
200             @Override
201             public double getB(int n, double x) {
202                 return 0.5;
203             }
204         };
205 
206         final double eps = 1e-8;
207         final Throwable t = Assertions.assertThrows(FractionException.class, () -> cf.evaluate(0, eps, 5));
208         assertExceptionMessageContains(t, "infinity");
209     }
210 
211     private static void assertExceptionMessageContains(Throwable t, String text) {
212         Assertions.assertTrue(t.getMessage().toLowerCase(Locale.ROOT).contains(text),
213             () -> "Missing '" + text + "' from exception message: " + t.getMessage());
214     }
215 
216     // NUMBERS-46
217     @Test
218     void testOneIteration() {
219         final double eps = 0.5;
220         final double gr = GoldenRatio.getInstance().evaluate(0, eps, 1);
221         // Expected: 1 + 1 / 1
222         Assertions.assertEquals(2.0, gr);
223     }
224 
225     // NUMBERS-46
226     @Test
227     void testTwoIterations() {
228         final double eps = 0.25;
229         final double gr = GoldenRatio.getInstance().evaluate(0, eps, 2);
230         // Expected: 1 + 1 / (1 + 1 / 1)
231         Assertions.assertEquals(1.5, gr);
232     }
233 }