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    *      https://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.text.numbers;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertThrows;
21  
22  import java.text.DecimalFormat;
23  import java.text.DecimalFormatSymbols;
24  import java.util.Locale;
25  import java.util.Random;
26  import java.util.function.DoubleFunction;
27  import java.util.function.Function;
28  import java.util.stream.Stream;
29  
30  import org.junit.jupiter.api.Test;
31  import org.junit.jupiter.params.ParameterizedTest;
32  import org.junit.jupiter.params.provider.Arguments;
33  import org.junit.jupiter.params.provider.MethodSource;
34  
35  class DoubleFormatTest {
36  
37      private static void assertLocalizedFormatsAreEqual(final double d, final DecimalFormat df,
38              final DoubleFunction<String> fmt, final Locale loc) {
39          // NOTE: Perform the string comparison only on non-format characters. This is required because
40          // JDK 16 adds directionality characters to strings for certain locales, such as Arabic, whereas
41          // previous JDKs do not. We will match the behavior of the previous versions here and ignore formatting
42          // for test purposes.
43          final String dfStr = trimFormatChars(df.format(d));
44          final String fmtStr = trimFormatChars(fmt.apply(d));
45  
46          try {
47              assertEquals(dfStr, fmtStr,
48                  () -> "Unexpected output for locale [" + loc.toLanguageTag() + "] and double value " + d);
49          } catch (final AssertionError e) {
50              // Note:
51              // The DecimalFormat may omit the fraction component if it is zero
52              // when using the ENGINEERING format "##0.0##E0".
53              // e.g. new DecimalFormat("##0.0##E0").format(1.1299999e-4) => 113E-6.
54              // The DoubleFormat class either always includes the zero or removes it
55              // with the setting includeFractionPlaceholder(false).
56              // Since we expect this mismatch we can remove the decimal point followed
57              // by a zero from the DoubleFormat output.
58              // This effectively checks: abcExyz == abc.0Exyz
59              final DecimalFormatSymbols dfs = new DecimalFormatSymbols(loc);
60              final char decimalSeparator = dfs.getDecimalSeparator();
61              final char zeroDigit = dfs.getZeroDigit();
62              final String updated = fmtStr.replace(new String(new char[] {decimalSeparator, zeroDigit}), "");
63              if (dfStr.equals(updated)) {
64                  return;
65              }
66              throw e;
67          }
68      }
69  
70      private static void checkDefaultFormatSpecial(final DoubleFunction<String> fmt) {
71          checkFormat(fmt, 0.0, "0.0");
72          checkFormat(fmt, -0.0, "-0.0");
73          checkFormat(fmt, Double.NaN, "NaN");
74          checkFormat(fmt, Double.POSITIVE_INFINITY, "Infinity");
75          checkFormat(fmt, Double.NEGATIVE_INFINITY, "-Infinity");
76      }
77  
78      private static void checkFormat(final DoubleFunction<String> fmt, final double d, final String str) {
79          assertEquals(str, fmt.apply(d));
80      }
81  
82      /**
83       * Check that the given double value can be exactly recovered from formatted string representation
84       * produced by the format instance.
85       * @param fmt format instance
86       * @param d input double value
87       */
88      private static void checkFormatAccuracy(final DoubleFunction<String> fmt, final double d) {
89          final String str = fmt.apply(d);
90          final double parsed = Double.parseDouble(str);
91          assertEquals(d, parsed, () -> "Formatted double string [" + str + "] did not match input value");
92      }
93  
94      /**
95       * Check that the given format type correctly formats doubles when using the
96       * default configuration options. The format itself is not checked; only the
97       * fact that the input double can be successfully recovered using {@link Double#parseDouble(String)}
98       * is asserted.
99       * @param type format type
100      */
101     private static void checkFormatAccuracyWithDefaults(final DoubleFormat type) {
102         final DoubleFunction<String> fmt = type.builder().get();
103 
104         checkDefaultFormatSpecial(fmt);
105 
106         checkFormatAccuracy(fmt, Double.MIN_VALUE);
107         checkFormatAccuracy(fmt, -Double.MIN_VALUE);
108 
109         checkFormatAccuracy(fmt, Double.MIN_NORMAL);
110         checkFormatAccuracy(fmt, -Double.MIN_NORMAL);
111 
112         checkFormatAccuracy(fmt, Double.MAX_VALUE);
113         checkFormatAccuracy(fmt, -Double.MAX_VALUE);
114 
115         checkFormatAccuracy(fmt, Math.PI);
116         checkFormatAccuracy(fmt, Math.E);
117 
118         final Random rnd = new Random(10L);
119         final int cnt = 1000;
120         for (int i = 0; i < cnt; ++i) {
121             checkFormatAccuracy(fmt, randomDouble(rnd));
122         }
123     }
124 
125     private static void checkLocalizedFormat(final Locale loc, final String pattern,
126             final Function<Locale, DoubleFunction<String>> factory) {
127         // arrange
128         final DecimalFormat df = new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(loc));
129         final DoubleFunction<String> fmt = factory.apply(loc);
130 
131         // act/assert
132         assertLocalizedFormatsAreEqual(0.0, df, fmt, loc);
133         assertLocalizedFormatsAreEqual(Double.POSITIVE_INFINITY, df, fmt, loc);
134         assertLocalizedFormatsAreEqual(Double.NEGATIVE_INFINITY, df, fmt, loc);
135         assertLocalizedFormatsAreEqual(Double.NaN, df, fmt, loc);
136 
137         assertLocalizedFormatsAreEqual(1.0, df, fmt, loc);
138         assertLocalizedFormatsAreEqual(-1.0, df, fmt, loc);
139         assertLocalizedFormatsAreEqual(Math.PI, df, fmt, loc);
140         assertLocalizedFormatsAreEqual(Math.E, df, fmt, loc);
141 
142         // Locales are tested using:
143         // DecimalFormat   DoubleFormat
144         // ##0.0##E0     : ENGINEERING  maPrecision=6
145         // 0.0##         : PLAIN        minDecimalExponent(-3)
146         // #,##0.0##     : PLAIN        minDecimalExponent(-3)
147         // 0.0##E0       : SCIENTIFIC   maPrecision=4
148         // The data should not test full precision (17 digits) of the PLAIN format.
149         // Set the exponent range to create decimals with exponents of approximately
150         // 10^7 to 10^-7: log2(1e7) = 23.25.
151         final Random rnd = new Random(12L);
152         final int minExp = -24;
153         final int maxExp = 24;
154         final int cnt = 1000;
155         for (int i = 0; i < cnt; ++i) {
156             assertLocalizedFormatsAreEqual(randomDouble(minExp, maxExp, rnd), df, fmt, loc);
157         }
158     }
159 
160     private static void checkLocalizedFormats(final String pattern, final Function<Locale, DoubleFunction<String>> factory) {
161         for (final Locale loc : Locale.getAvailableLocales()) {
162             checkLocalizedFormat(loc, pattern, factory);
163         }
164     }
165 
166     /**
167      * Create a random double value with exponent in the range {@code [minExp, maxExp]}.
168      * @param minExp minimum exponent; must be less than {@code maxExp}
169      * @param maxExp maximum exponent; must be greater than {@code minExp}
170      * @param rnd random number generator
171      * @return random double
172      */
173     private static double randomDouble(final int minExp, final int maxExp, final Random rnd) {
174         // Create random doubles using random bits in the sign bit and the mantissa.
175         final long mask = (1L << 52) - 1 | 1L << 63;
176         final long bits = rnd.nextLong() & mask;
177         // The exponent must be unsigned so + 1023 to the signed exponent
178         final long exp = rnd.nextInt(maxExp - minExp + 1) + minExp + 1023;
179         return Double.longBitsToDouble(bits | exp << 52);
180     }
181 
182     /**
183      * Create a random double value using the full range of exponent values.
184      * @param rnd random number generator
185      * @return random double
186      */
187     private static double randomDouble(final Random rnd) {
188         return randomDouble(Double.MIN_EXPONENT, Double.MAX_EXPONENT, rnd);
189     }
190 
191     static Stream<Arguments> testMaximumPrecision() {
192         return Stream.of(
193             // Example of different Double.toString representations across JDKs
194             // JDK 17: -9.3540047119774374E17
195             // JDK 21: -9.354004711977437E17
196             Arguments.of(DoubleFormat.PLAIN.builder().get(), -9.3540047119774374E17),
197             Arguments.of(DoubleFormat.SCIENTIFIC.builder().get(), -9.3540047119774374E17)
198         );
199     }
200 
201     /**
202      * Remove Unicode {@link Character#FORMAT format} characters from the given string.
203      * @param str input string
204      * @return input string with format characters removed
205      */
206     private static String trimFormatChars(final String str) {
207         final StringBuilder sb = new StringBuilder();
208         for (final char c : str.toCharArray()) {
209             if (Character.getType(c) != Character.FORMAT) {
210                 sb.append(c);
211             }
212         }
213         return sb.toString();
214     }
215 
216     @Test
217     void testBuilder_illegalArgs() {
218         // arrange
219         final DoubleFormat.Builder builder = DoubleFormat.PLAIN.builder();
220 
221         // act/assert
222         assertThrows(NullPointerException.class, () -> builder.digits(null));
223         assertThrows(IllegalArgumentException.class, () -> builder.digits("a"));
224         assertThrows(IllegalArgumentException.class, () -> builder.digits("0123456789a"));
225 
226         assertThrows(NullPointerException.class, () -> builder.exponentSeparator(null));
227         assertThrows(NullPointerException.class, () -> builder.infinity(null));
228         assertThrows(NullPointerException.class, () -> builder.nan(null));
229         assertThrows(NullPointerException.class, () -> builder.formatSymbols(null));
230     }
231 
232     @Test
233     void testCustomDigitString() {
234         // arrange
235         final String digits = "abcdefghij";
236         final DoubleFunction<String> plain = DoubleFormat.PLAIN.builder().digits(digits).get();
237         final DoubleFunction<String> sci = DoubleFormat.SCIENTIFIC.builder().digits(digits).get();
238         final DoubleFunction<String> eng = DoubleFormat.ENGINEERING.builder().digits(digits).get();
239         final DoubleFunction<String> mixed = DoubleFormat.MIXED.builder().digits(digits).get();
240 
241         // act/assert
242         checkFormat(plain, 9876543210.0, "jihgfedcba.a");
243         checkFormat(sci, 9876543210.0, "j.ihgfedcbEj");
244         checkFormat(eng, 9876543210.0, "j.ihgfedcbEj");
245         checkFormat(mixed, 9876543210.0, "j.ihgfedcbEj");
246     }
247 
248     @Test
249     void testEngineering_custom() {
250         // act
251         final DoubleFunction<String> fmt = DoubleFormat.ENGINEERING.builder()
252                 .maxPrecision(3)
253                 .minDecimalExponent(-3)
254                 .allowSignedZero(false)
255                 .includeFractionPlaceholder(false)
256                 .decimalSeparator(',')
257                 .exponentSeparator("e")
258                 .infinity("inf")
259                 .nan("nan")
260                 .minusSign('!')
261                 .get();
262 
263         // act/assert
264         checkFormat(fmt, Double.NaN, "nan");
265         checkFormat(fmt, Double.POSITIVE_INFINITY, "inf");
266         checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf");
267 
268         checkFormat(fmt, 0.00001, "0");
269         checkFormat(fmt, -0.0001, "0");
270         checkFormat(fmt, 0.001, "1e!3");
271         checkFormat(fmt, -0.01, "!10e!3");
272         checkFormat(fmt, 0.1, "100e!3");
273         checkFormat(fmt, -0.0, "0");
274         checkFormat(fmt, 0.0, "0");
275         checkFormat(fmt, -1.0, "!1");
276         checkFormat(fmt, 10.0, "10");
277         checkFormat(fmt, -100.0, "!100");
278         checkFormat(fmt, 1000.0, "1e3");
279         checkFormat(fmt, -10000.0, "!10e3");
280         checkFormat(fmt, 100000.0, "100e3");
281         checkFormat(fmt, -1000000.0, "!1e6");
282         checkFormat(fmt, 10000000.0, "10e6");
283         checkFormat(fmt, -100000000.0, "!100e6");
284 
285         checkFormat(fmt, 1.25e-3, "1e!3");
286         checkFormat(fmt, -9.975e-4, "!1e!3");
287         checkFormat(fmt, 12345, "12,3e3");
288         checkFormat(fmt, -9_999_999, "!10e6");
289         checkFormat(fmt, 1.00001e7, "10e6");
290 
291         checkFormat(fmt, Double.MAX_VALUE, "180e306");
292         checkFormat(fmt, Double.MIN_VALUE, "0");
293         checkFormat(fmt, Double.MIN_NORMAL, "0");
294         checkFormat(fmt, Math.PI, "3,14");
295         checkFormat(fmt, Math.E, "2,72");
296     }
297 
298     @Test
299     void testEngineering_defaults() {
300         // act
301         final DoubleFunction<String> fmt = DoubleFormat.ENGINEERING.builder()
302                 .get();
303 
304         // act/assert
305         checkDefaultFormatSpecial(fmt);
306 
307         checkFormat(fmt, 0.00001, "10.0E-6");
308         checkFormat(fmt, -0.0001, "-100.0E-6");
309         checkFormat(fmt, 0.001, "1.0E-3");
310         checkFormat(fmt, -0.01, "-10.0E-3");
311         checkFormat(fmt, 0.1, "100.0E-3");
312         checkFormat(fmt, -0.0, "-0.0");
313         checkFormat(fmt, 0.0, "0.0");
314         checkFormat(fmt, -1.0, "-1.0");
315         checkFormat(fmt, 10.0, "10.0");
316         checkFormat(fmt, -100.0, "-100.0");
317         checkFormat(fmt, 1000.0, "1.0E3");
318         checkFormat(fmt, -10000.0, "-10.0E3");
319         checkFormat(fmt, 100000.0, "100.0E3");
320         checkFormat(fmt, -1000000.0, "-1.0E6");
321         checkFormat(fmt, 10000000.0, "10.0E6");
322         checkFormat(fmt, -100000000.0, "-100.0E6");
323 
324         checkFormat(fmt, 1.25e-3, "1.25E-3");
325         checkFormat(fmt, -9.975e-4, "-997.5E-6");
326         checkFormat(fmt, 12345, "12.345E3");
327         checkFormat(fmt, -9_999_999, "-9.999999E6");
328         checkFormat(fmt, 1.00001e7, "10.0001E6");
329 
330         checkFormat(fmt, Double.MAX_VALUE, "179.76931348623157E306");
331         checkFormat(fmt, Double.MIN_VALUE, "4.9E-324");
332         checkFormat(fmt, Double.MIN_NORMAL, "22.250738585072014E-309");
333         checkFormat(fmt, Math.PI, "3.141592653589793");
334         checkFormat(fmt, Math.E, "2.718281828459045");
335     }
336 
337     @Test
338     void testEngineering_localeFormatComparison() {
339         // act/assert
340         checkLocalizedFormats("##0.0##E0", loc -> DoubleFormat.ENGINEERING.builder()
341                 .maxPrecision(6)
342                 .alwaysIncludeExponent(true)
343                 .formatSymbols(DecimalFormatSymbols.getInstance(loc))
344                 .get());
345     }
346 
347     @Test
348     void testFormatAccuracy() {
349         // act/assert
350         checkFormatAccuracyWithDefaults(DoubleFormat.PLAIN);
351         checkFormatAccuracyWithDefaults(DoubleFormat.MIXED);
352         checkFormatAccuracyWithDefaults(DoubleFormat.SCIENTIFIC);
353         checkFormatAccuracyWithDefaults(DoubleFormat.ENGINEERING);
354     }
355 
356     /**
357      * Test formatting at the maximum precision. The formatting is based on the output
358      * of {@link Double#toString()}. If cannot create an extended precision text
359      * representation and is limited to 17 significant digits. This test verifies that
360      * formatting does not lose information that would be required to recreate the
361      * same double value.
362      */
363     @ParameterizedTest
364     @MethodSource
365     void testMaximumPrecision(final DoubleFunction<String> fmt, final double value) {
366         final String s = fmt.apply(value);
367         final double d = Double.parseDouble(s);
368         assertEquals(value, d, () -> value + " formatted as " + s);
369     }
370 
371     @Test
372     void testMixed_custom() {
373         // arrange
374         final DoubleFunction<String> fmt = DoubleFormat.MIXED.builder()
375                 .maxPrecision(3)
376                 .minDecimalExponent(-3)
377                 .allowSignedZero(false)
378                 .includeFractionPlaceholder(false)
379                 .decimalSeparator(',')
380                 .plainFormatMaxDecimalExponent(4)
381                 .plainFormatMinDecimalExponent(-1)
382                 .exponentSeparator("e")
383                 .infinity("inf")
384                 .nan("nan")
385                 .minusSign('!')
386                 .get();
387 
388         // act/assert
389         checkFormat(fmt, Double.NaN, "nan");
390         checkFormat(fmt, Double.POSITIVE_INFINITY, "inf");
391         checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf");
392 
393         checkFormat(fmt, 0.00001, "0");
394         checkFormat(fmt, -0.0001, "0");
395         checkFormat(fmt, 0.001, "1e!3");
396         checkFormat(fmt, -0.01, "!1e!2");
397         checkFormat(fmt, 0.1, "0,1");
398         checkFormat(fmt, -0.0, "0");
399         checkFormat(fmt, 0.0, "0");
400         checkFormat(fmt, -1.0, "!1");
401         checkFormat(fmt, 10.0, "10");
402         checkFormat(fmt, -100.0, "!100");
403         checkFormat(fmt, 1000.0, "1000");
404         checkFormat(fmt, -10000.0, "!10000");
405         checkFormat(fmt, 100000.0, "1e5");
406         checkFormat(fmt, -1000000.0, "!1e6");
407         checkFormat(fmt, 10000000.0, "1e7");
408         checkFormat(fmt, -100000000.0, "!1e8");
409 
410         checkFormat(fmt, 1.25e-3, "1e!3");
411         checkFormat(fmt, -9.975e-4, "!1e!3");
412         checkFormat(fmt, 12345, "12300");
413         checkFormat(fmt, -9_999_999, "!1e7");
414         checkFormat(fmt, 1.00001e7, "1e7");
415 
416         checkFormat(fmt, Double.MAX_VALUE, "1,8e308");
417         checkFormat(fmt, Double.MIN_VALUE, "0");
418         checkFormat(fmt, Double.MIN_NORMAL, "0");
419         checkFormat(fmt, Math.PI, "3,14");
420         checkFormat(fmt, Math.E, "2,72");
421     }
422 
423     @Test
424     void testMixed_defaults() {
425         // arrange
426         testMixed_defaults(DoubleFormat.MIXED.builder().get());
427     }
428 
429     private void testMixed_defaults(final DoubleFunction<String> fmt) {
430         // act/assert
431         checkDefaultFormatSpecial(fmt);
432 
433         checkFormat(fmt, 0.00001, "1.0E-5");
434         checkFormat(fmt, -0.0001, "-1.0E-4");
435         checkFormat(fmt, 0.001, "0.001");
436         checkFormat(fmt, -0.01, "-0.01");
437         checkFormat(fmt, 0.1, "0.1");
438         checkFormat(fmt, -0.0, "-0.0");
439         checkFormat(fmt, 0.0, "0.0");
440         checkFormat(fmt, -1.0, "-1.0");
441         checkFormat(fmt, 10.0, "10.0");
442         checkFormat(fmt, -100.0, "-100.0");
443         checkFormat(fmt, 1000.0, "1000.0");
444         checkFormat(fmt, -10000.0, "-10000.0");
445         checkFormat(fmt, 100000.0, "100000.0");
446         checkFormat(fmt, -1000000.0, "-1000000.0");
447         checkFormat(fmt, 10000000.0, "1.0E7");
448         checkFormat(fmt, -100000000.0, "-1.0E8");
449 
450         checkFormat(fmt, 1.25e-3, "0.00125");
451         checkFormat(fmt, -9.975e-4, "-9.975E-4");
452         checkFormat(fmt, 12345, "12345.0");
453         checkFormat(fmt, -9_999_999, "-9999999.0");
454         checkFormat(fmt, 1.00001e7, "1.00001E7");
455 
456         checkFormat(fmt, Double.MAX_VALUE, "1.7976931348623157E308");
457         checkFormat(fmt, Double.MIN_VALUE, "4.9E-324");
458         checkFormat(fmt, Double.MIN_NORMAL, "2.2250738585072014E-308");
459         checkFormat(fmt, Math.PI, "3.141592653589793");
460         checkFormat(fmt, Math.E, "2.718281828459045");
461     }
462 
463     @Test
464     void testMixed_defaultsDeprecated() {
465         testMixed_defaults(DoubleFormat.MIXED.builder().build());
466     }
467 
468     @Test
469     void testPlain_custom() {
470         // arrange
471         final DoubleFunction<String> fmt = DoubleFormat.PLAIN.builder()
472             .maxPrecision(3)
473             .minDecimalExponent(-3)
474             .allowSignedZero(false)
475             .includeFractionPlaceholder(false)
476             .decimalSeparator(',')
477             .exponentSeparator("e")
478             .infinity("inf")
479             .nan("nan")
480             .minusSign('!')
481             .get();
482 
483         // act/assert
484         checkFormat(fmt, Double.NaN, "nan");
485         checkFormat(fmt, Double.POSITIVE_INFINITY, "inf");
486         checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf");
487 
488         checkFormat(fmt, 0.00001, "0");
489         checkFormat(fmt, -0.0001, "0");
490         checkFormat(fmt, 0.001, "0,001");
491         checkFormat(fmt, -0.01, "!0,01");
492         checkFormat(fmt, 0.1, "0,1");
493         checkFormat(fmt, -0.0, "0");
494         checkFormat(fmt, 0.0, "0");
495         checkFormat(fmt, -1.0, "!1");
496         checkFormat(fmt, 10.0, "10");
497         checkFormat(fmt, -100.0, "!100");
498         checkFormat(fmt, 1000.0, "1000");
499         checkFormat(fmt, -10000.0, "!10000");
500         checkFormat(fmt, 100000.0, "100000");
501         checkFormat(fmt, -1000000.0, "!1000000");
502         checkFormat(fmt, 10000000.0, "10000000");
503         checkFormat(fmt, -100000000.0, "!100000000");
504 
505         checkFormat(fmt, 1.25e-3, "0,001");
506         checkFormat(fmt, -9.975e-4, "!0,001");
507         checkFormat(fmt, 12345, "12300");
508         checkFormat(fmt, -9_999_999, "!10000000");
509         checkFormat(fmt, 1.00001e7, "10000000");
510 
511         checkFormat(fmt, Float.MAX_VALUE, "340000000000000000000000000000000000000");
512         checkFormat(fmt, -Float.MIN_VALUE, "0");
513         checkFormat(fmt, Float.MIN_NORMAL, "0");
514         checkFormat(fmt, Math.PI, "3,14");
515         checkFormat(fmt, Math.E, "2,72");
516     }
517 
518     @Test
519     void testPlain_defaults() {
520         // arrange
521         final DoubleFunction<String> fmt = DoubleFormat.PLAIN.builder()
522             .get();
523 
524         // act/assert
525         checkFormat(fmt, 0.00001, "0.00001");
526         checkFormat(fmt, -0.0001, "-0.0001");
527         checkFormat(fmt, 0.001, "0.001");
528         checkFormat(fmt, -0.01, "-0.01");
529         checkFormat(fmt, 0.1, "0.1");
530         checkFormat(fmt, -0.0, "-0.0");
531         checkFormat(fmt, 0.0, "0.0");
532         checkFormat(fmt, -1.0, "-1.0");
533         checkFormat(fmt, 10.0, "10.0");
534         checkFormat(fmt, -100.0, "-100.0");
535         checkFormat(fmt, 1000.0, "1000.0");
536         checkFormat(fmt, -10000.0, "-10000.0");
537         checkFormat(fmt, 100000.0, "100000.0");
538         checkFormat(fmt, -1000000.0, "-1000000.0");
539         checkFormat(fmt, 10000000.0, "10000000.0");
540         checkFormat(fmt, -100000000.0, "-100000000.0");
541 
542         checkFormat(fmt, 1.25e-3, "0.00125");
543         checkFormat(fmt, -9.975e-4, "-0.0009975");
544         checkFormat(fmt, 12345, "12345.0");
545         checkFormat(fmt, -9_999_999, "-9999999.0");
546         checkFormat(fmt, 1.00001e7, "10000100.0");
547 
548         checkFormat(fmt, Float.MAX_VALUE, "340282346638528860000000000000000000000.0");
549         checkFormat(fmt, -Float.MIN_VALUE, "-0.000000000000000000000000000000000000000000001401298464324817");
550         checkFormat(fmt, Float.MIN_NORMAL, "0.000000000000000000000000000000000000011754943508222875");
551         checkFormat(fmt, Math.PI, "3.141592653589793");
552         checkFormat(fmt, Math.E, "2.718281828459045");
553     }
554 
555     @Test
556     void testPlain_localeFormatComparison() {
557         // act/assert
558         checkLocalizedFormats("0.0##", loc -> DoubleFormat.PLAIN.builder()
559                 .minDecimalExponent(-3)
560                 .formatSymbols(DecimalFormatSymbols.getInstance(loc))
561                 .get());
562         checkLocalizedFormats("#,##0.0##", loc -> DoubleFormat.PLAIN.builder()
563                 .minDecimalExponent(-3)
564                 .groupThousands(true)
565                 .formatSymbols(DecimalFormatSymbols.getInstance(loc))
566                 .get());
567     }
568 
569     @Test
570     void testScientific_custom() {
571         // arrange
572         final DoubleFunction<String> fmt = DoubleFormat.SCIENTIFIC.builder()
573                 .maxPrecision(3)
574                 .minDecimalExponent(-3)
575                 .allowSignedZero(false)
576                 .includeFractionPlaceholder(false)
577                 .decimalSeparator(',')
578                 .exponentSeparator("e")
579                 .infinity("inf")
580                 .nan("nan")
581                 .minusSign('!')
582                 .get();
583 
584         // act/assert
585         checkFormat(fmt, Double.NaN, "nan");
586         checkFormat(fmt, Double.POSITIVE_INFINITY, "inf");
587         checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf");
588 
589         checkFormat(fmt, 0.00001, "0");
590         checkFormat(fmt, -0.0001, "0");
591         checkFormat(fmt, 0.001, "1e!3");
592         checkFormat(fmt, -0.01, "!1e!2");
593         checkFormat(fmt, 0.1, "1e!1");
594         checkFormat(fmt, -0.0, "0");
595         checkFormat(fmt, 0.0, "0");
596         checkFormat(fmt, -1.0, "!1");
597         checkFormat(fmt, 10.0, "1e1");
598         checkFormat(fmt, -100.0, "!1e2");
599         checkFormat(fmt, 1000.0, "1e3");
600         checkFormat(fmt, -10000.0, "!1e4");
601         checkFormat(fmt, 100000.0, "1e5");
602         checkFormat(fmt, -1000000.0, "!1e6");
603         checkFormat(fmt, 10000000.0, "1e7");
604         checkFormat(fmt, -100000000.0, "!1e8");
605 
606         checkFormat(fmt, 1.25e-3, "1e!3");
607         checkFormat(fmt, -9.975e-4, "!1e!3");
608         checkFormat(fmt, 12345, "1,23e4");
609         checkFormat(fmt, -9_999_999, "!1e7");
610         checkFormat(fmt, 1.00001e7, "1e7");
611 
612         checkFormat(fmt, Double.MAX_VALUE, "1,8e308");
613         checkFormat(fmt, Double.MIN_VALUE, "0");
614         checkFormat(fmt, Double.MIN_NORMAL, "0");
615         checkFormat(fmt, Math.PI, "3,14");
616         checkFormat(fmt, Math.E, "2,72");
617     }
618 
619     @Test
620     void testScientific_defaults() {
621         // arrange
622         final DoubleFunction<String> fmt = DoubleFormat.SCIENTIFIC.builder().get();
623 
624         // act/assert
625         checkDefaultFormatSpecial(fmt);
626 
627         checkFormat(fmt, 0.00001, "1.0E-5");
628         checkFormat(fmt, -0.0001, "-1.0E-4");
629         checkFormat(fmt, 0.001, "1.0E-3");
630         checkFormat(fmt, -0.01, "-1.0E-2");
631         checkFormat(fmt, 0.1, "1.0E-1");
632         checkFormat(fmt, -0.0, "-0.0");
633         checkFormat(fmt, 0.0, "0.0");
634         checkFormat(fmt, -1.0, "-1.0");
635         checkFormat(fmt, 10.0, "1.0E1");
636         checkFormat(fmt, -100.0, "-1.0E2");
637         checkFormat(fmt, 1000.0, "1.0E3");
638         checkFormat(fmt, -10000.0, "-1.0E4");
639         checkFormat(fmt, 100000.0, "1.0E5");
640         checkFormat(fmt, -1000000.0, "-1.0E6");
641         checkFormat(fmt, 10000000.0, "1.0E7");
642         checkFormat(fmt, -100000000.0, "-1.0E8");
643 
644         checkFormat(fmt, 1.25e-3, "1.25E-3");
645         checkFormat(fmt, -9.975e-4, "-9.975E-4");
646         checkFormat(fmt, 12345, "1.2345E4");
647         checkFormat(fmt, -9_999_999, "-9.999999E6");
648         checkFormat(fmt, 1.00001e7, "1.00001E7");
649 
650         checkFormat(fmt, Double.MAX_VALUE, "1.7976931348623157E308");
651         checkFormat(fmt, Double.MIN_VALUE, "4.9E-324");
652         checkFormat(fmt, Double.MIN_NORMAL, "2.2250738585072014E-308");
653         checkFormat(fmt, Math.PI, "3.141592653589793");
654         checkFormat(fmt, Math.E, "2.718281828459045");
655     }
656 
657     @Test
658     void testScientific_localeFormatComparison() {
659         // act/assert
660         checkLocalizedFormats("0.0##E0", loc -> DoubleFormat.SCIENTIFIC.builder()
661                 .maxPrecision(4)
662                 .alwaysIncludeExponent(true)
663                 .formatSymbols(DecimalFormatSymbols.getInstance(loc))
664                 .get());
665     }
666 }