Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 94 additions & 171 deletions test/jdk/java/text/Format/NumberFormat/NumberRoundTrip.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand All @@ -23,19 +23,24 @@

/*
* @test
* @summary round trip test NumberFormat
* @bug 4266589 8031145 8164791 8316696 8368001
* @summary NumberFormat round trip testing of parsing and formatting.
* This test checks 4 factory instances per locale against ~20 numeric inputs.
* Samples ~1/4 of the available locales from NumberFormat SPI.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it is not using SPI

* @key randomness
* @run junit NumberRoundTrip
*/

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
* This class tests the round-trip behavior of NumberFormat, DecimalFormat, and DigitList.
Expand All @@ -44,187 +49,105 @@
* Two tests are applied: String preservation, and numeric preservation. String
* preservation is exact; numeric preservation is not. However, numeric preservation
* should extend to the few least-significant bits.
* //bug472
*/
public class NumberRoundTrip {
static final boolean STRING_COMPARE = true;
static final boolean EXACT_NUMERIC_COMPARE = false;
static final double MAX_ERROR = 1e-14;
static double max_numeric_error = 0;
static double min_numeric_error = 1;

String localeName, formatName;

@Test
public void TestNumberFormatRoundTrip() {
System.out.println("Default Locale");
localeName = "Default Locale";
formatName = "getInstance";
doTest(NumberFormat.getInstance());
formatName = "getNumberInstance";
doTest(NumberFormat.getNumberInstance());
formatName = "getCurrencyInstance";
doTest(NumberFormat.getCurrencyInstance());
formatName = "getPercentInstance";
doTest(NumberFormat.getPercentInstance());

Locale[] loc = NumberFormat.getAvailableLocales();
for (int i=0; i<loc.length; ++i) {
System.out.println(loc[i].getDisplayName());
localeName = loc[i].toString();
formatName = "getInstance";
doTest(NumberFormat.getInstance(loc[i]));
formatName = "getNumberInstance";
doTest(NumberFormat.getNumberInstance(loc[i]));
formatName = "getCurrencyInstance";
doTest(NumberFormat.getCurrencyInstance(loc[i]));
formatName = "getPercentInstance";
doTest(NumberFormat.getPercentInstance(loc[i]));
}

System.out.println("Numeric error " +
min_numeric_error + " to " +
max_numeric_error);
@ParameterizedTest
@MethodSource
void testNumberFormatRoundTrip(NumberFormat fmt) {
fmt.setMaximumFractionDigits(Integer.MAX_VALUE);
Stream.concat(numbers.stream(), randomNumbers())
.forEach(num -> test(fmt, num));
}

public void doTest(NumberFormat fmt) {
doTest(fmt, Double.NaN);
doTest(fmt, Double.POSITIVE_INFINITY);
doTest(fmt, Double.NEGATIVE_INFINITY);

doTest(fmt, 500);
doTest(fmt, 0);
doTest(fmt, 5555555555555555L);
doTest(fmt, 55555555555555555L);
doTest(fmt, 9223372036854775807L);
doTest(fmt, 9223372036854775808.0);
doTest(fmt, -9223372036854775808L);
doTest(fmt, -9223372036854775809.0);

for (int i=0; i<2; ++i) {
doTest(fmt, randomDouble(1));
doTest(fmt, randomDouble(10000));
doTest(fmt, Math.floor(randomDouble(10000)));
doTest(fmt, randomDouble(1e50));
doTest(fmt, randomDouble(1e-50));
doTest(fmt, randomDouble(1e100));
// The use of double d such that isInfinite(100d) causes the
// numeric test to fail with percent formats (bug 4266589).
// Largest double s.t. 100d < Inf: d=1.7976931348623156E306
doTest(fmt, randomDouble(1e306));
doTest(fmt, randomDouble(1e-323));
doTest(fmt, randomDouble(1e-100));
private void test(NumberFormat fmt, Number num) {
String originalFormatted = fmt.format(num);
Number parsedNum = Assertions.assertDoesNotThrow(() -> fmt.parse(originalFormatted),
"Failed parse(format(%s))".formatted(num));
String parsedFormatted = fmt.format(parsedNum);
var equal = originalFormatted.equals(parsedFormatted);
// Try BigDecimal parsing, if not equal
if (!equal) {
var df = Assertions.assertInstanceOf(DecimalFormat.class, fmt);
df.setParseBigDecimal(true);
parsedNum = Assertions.assertDoesNotThrow(() -> fmt.parse(originalFormatted),
"Failed BigDecimal parse(format(%s))".formatted(num));
parsedFormatted = fmt.format(parsedNum);
df.setParseBigDecimal(false);
Assertions.assertEquals(originalFormatted, parsedFormatted,
"Failed to round-trip format(parse(format(%s)))".formatted(num));
}
// Numeric mismatch to the amount of 1e-14 is tolerable
var error = proportionalError(num, parsedNum);
Assertions.assertFalse(error > 1e-14,
"Round tripping %s caused numeric error: %s".formatted(num, error));
}

/**
* Return a random value from -range..+range.
*/
public double randomDouble(double range) {
double a = Math.random();
return (2.0 * range * a) - range;
// Regular, number, currency, and percent instance per locale
private static Stream<Arguments> testNumberFormatRoundTrip() {
return Stream.concat(
// Default Locale
Stream.of(
Arguments.of(NumberFormat.getInstance()),
Arguments.of(NumberFormat.getNumberInstance()),
Arguments.of(NumberFormat.getCurrencyInstance()),
Arguments.of(NumberFormat.getPercentInstance())),
// ~1000 locales returned from provider.
// Too expensive to test all locales, so sample a reasonable amount
Arrays.stream(NumberFormat.getAvailableLocales())
.filter(_ -> Math.random() < .25)
.flatMap(loc -> Stream.of(
Arguments.of(NumberFormat.getInstance(loc)),
Arguments.of(NumberFormat.getNumberInstance(loc)),
Arguments.of(NumberFormat.getCurrencyInstance(loc)),
Arguments.of(NumberFormat.getPercentInstance(loc)))
)
);
}

public void doTest(NumberFormat fmt, double value) {
doTest(fmt, Double.valueOf(value));
// Fixed set of numbers to test each locale against
private static final List<Number> numbers = List.of(
Double.NaN,
Double.POSITIVE_INFINITY,
Double.NEGATIVE_INFINITY,
500,
0,
5555555555555555L,
55555555555555555L,
9223372036854775807L,
9223372036854775808.0,
-9223372036854775808L,
-9223372036854775809.0
);

// Compute fresh batch of random numbers per locale
private Stream<Number> randomNumbers() {
return Stream.of(
randomDouble(1),
randomDouble(10000),
Math.floor(randomDouble(10000)),
randomDouble(1e50),
randomDouble(1e-50),
randomDouble(1e100),
// The use of double d such that isInfinite(100d) causes the
// numeric test to fail with percent formats (bug 4266589).
// Largest double s.t. 100d < Inf: d=1.7976931348623156E306
randomDouble(1e306),
randomDouble(1e-323),
randomDouble(1e-100)
);
}

public void doTest(NumberFormat fmt, long value) {
doTest(fmt, Long.valueOf(value));
// Return a random value from -range..+range.
private static double randomDouble(double range) {
double a = Math.random();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest using jdk.test.lib.RandomFactory, as this supports reproducible values in case of a randomised test failure.

import jdk.test.lib.RandomFactory;
...
 * @library /test/lib
 * @build jdk.test.lib.RandomFactory
...
    private static final Random RANDOM = RandomFactory.getRandom();
...
    private static double randomDouble(double range) {
        double a = RANDOM.nextDouble();
...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the improvement, updated in dca988e.

return (2.0 * range * a) - range;
}

static double proportionalError(Number a, Number b) {
private static double proportionalError(Number a, Number b) {
double aa = a.doubleValue(), bb = b.doubleValue();
double error = aa - bb;
if (aa != 0 && bb != 0) error /= aa;
return Math.abs(error);
}

public void doTest(NumberFormat fmt, Number value) {
fmt.setMaximumFractionDigits(Integer.MAX_VALUE);
String s = fmt.format(value), s2 = null;
Number n = null;
String err = "";
try {
System.out.println(" " + value + " F> " + escape(s));
n = fmt.parse(s);
System.out.println(" " + escape(s) + " P> " + n);
s2 = fmt.format(n);
System.out.println(" " + n + " F> " + escape(s2));

if (STRING_COMPARE) {
if (!s.equals(s2)) {
if (fmt instanceof DecimalFormat) {
System.out.println("Text mismatch: expected: " + s + ", got: " + s2 + " --- Try BigDecimal parsing.");
((DecimalFormat)fmt).setParseBigDecimal(true);
n = fmt.parse(s);
System.out.println(" " + escape(s) + " P> " + n);
s2 = fmt.format(n);
System.out.println(" " + n + " F> " + escape(s2));
((DecimalFormat)fmt).setParseBigDecimal(false);

if (!s.equals(s2)) {
err = "STRING ERROR(DecimalFormat): ";
}
} else {
err = "STRING ERROR(NumberFormat): ";
}
}
}

if (EXACT_NUMERIC_COMPARE) {
if (value.doubleValue() != n.doubleValue()) {
err += "NUMERIC ERROR: ";
}
} else {
// Compute proportional error
double error = proportionalError(value, n);

if (error > MAX_ERROR) {
err += "NUMERIC ERROR " + error + ": ";
}

if (error > max_numeric_error) max_numeric_error = error;
if (error < min_numeric_error) min_numeric_error = error;
}

String message = value + typeOf(value) + " F> " +
escape(s) + " P> " +
n + typeOf(n) + " F> " +
escape(s2);
if (err.length() > 0) {
fail("*** " + err + " with " +
formatName + " in " + localeName +
" " + message);
} else {
System.out.println(message);
}
} catch (ParseException e) {
fail("*** " + e.toString() + " with " +
formatName + " in " + localeName);
}
}

static String typeOf(Number n) {
if (n instanceof Long) return " Long";
if (n instanceof Double) return " Double";
return " Number";
}

static String escape(String s) {
StringBuffer buf = new StringBuffer();
for (int i=0; i<s.length(); ++i) {
char c = s.charAt(i);
if (c < (char)0xFF) {
buf.append(c);
} else {
buf.append("\\U");
buf.append(Integer.toHexString((c & 0xF000) >> 12));
buf.append(Integer.toHexString((c & 0x0F00) >> 8));
buf.append(Integer.toHexString((c & 0x00F0) >> 4));
buf.append(Integer.toHexString(c & 0x000F));
}
}
return buf.toString();
}
}