This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 0c324809113af5b1edf538f925736214f36314ec Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Nov 3 14:25:16 2018 +0100 Fix a confusion in the NumberFormat settings performed by StatisticsFormat, in particular when values are percentages. --- .../gazetteer/MilitaryGridReferenceSystem.java | 11 +- .../sis/referencing/operation/matrix/Matrices.java | 3 +- .../apache/sis/geometry/CoordinateFormatTest.java | 4 +- .../java/org/apache/sis/io/CompoundFormat.java | 3 + .../main/java/org/apache/sis/io/DefaultFormat.java | 2 +- .../java/org/apache/sis/math/DecimalFunctions.java | 25 ++++ .../java/org/apache/sis/math/StatisticsFormat.java | 135 +++++++++++---------- .../org/apache/sis/math/DecimalFunctionsTest.java | 27 +++++ .../org/apache/sis/math/StatisticsFormatTest.java | 40 +++++- 9 files changed, 179 insertions(+), 71 deletions(-) diff --git a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java index e8c4426..269f3f5 100644 --- a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java +++ b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java @@ -66,6 +66,7 @@ import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.Envelope2D; import org.apache.sis.geometry.DirectPosition2D; import org.apache.sis.internal.system.Modules; +import org.apache.sis.math.DecimalFunctions; import org.apache.sis.measure.Longitude; import org.apache.sis.measure.Latitude; @@ -464,12 +465,14 @@ public class MilitaryGridReferenceSystem extends ReferencingByIdentifiers { * @param precision the desired precision in metres. */ public void setPrecision(final double precision) { - final double p = Math.floor(Math.log10(precision)); - if (!Double.isFinite(p)) { - throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "precision", precision)); + final int p; + try { + p = DecimalFunctions.floorLog10(precision); + } catch (ArithmeticException e) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "precision", precision), e); } // The -3 is an arbitrary limit to millimetre precision. - int n = Math.max(-3, Math.min(METRE_PRECISION_DIGITS + 1, (int) p)); + int n = Math.max(-3, Math.min(METRE_PRECISION_DIGITS + 1, p)); digits = (byte) (METRE_PRECISION_DIGITS - n); } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java index 5fb909b..d931d8c 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java @@ -29,6 +29,7 @@ import org.apache.sis.util.CharSequences; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.resources.Errors; +import org.apache.sis.math.DecimalFunctions; import org.apache.sis.internal.util.Numerics; import org.apache.sis.internal.util.DoubleDouble; import org.apache.sis.internal.metadata.AxisDirections; @@ -1134,7 +1135,7 @@ public final class Matrices extends Static { * IEEE 754 'double' accuracy for not giving a false sense of precision. */ if (element.indexOf('E') < 0) { - final int accuracy = (int) Math.ceil(-Math.log10(Math.ulp(value))); + final int accuracy = -DecimalFunctions.floorLog10(Math.ulp(value)); maximumPaddingZeros[flatIndex] = (byte) (accuracy - numFractionDigits); } } diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java index 5f45650..ff58b15 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java @@ -39,7 +39,7 @@ import static org.junit.Assert.*; * @author Martin Desruisseaux (IRD, Geomatys) * @author Michael Hausegger * - * @version 0.8 + * @version 1.0 * * @see org.apache.sis.measure.AngleFormatTest * @@ -239,7 +239,7 @@ public final strictfp class CoordinateFormatTest extends TestCase { @Test public void testGetPattern() { CoordinateFormat coordinateFormat = new CoordinateFormat(Locale.UK, null); - assertEquals("#,##0.###", coordinateFormat.getPattern(Byte.class)); + assertEquals("#,##0.###", coordinateFormat.getPattern(Float.class)); assertNull(coordinateFormat.getPattern(Object.class)); assertNull(coordinateFormat.getPattern(Class.class)); } diff --git a/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java b/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java index 4ecf3fd..59ea33b 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java +++ b/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java @@ -39,6 +39,7 @@ import org.apache.sis.measure.AngleFormat; import org.apache.sis.measure.Range; import org.apache.sis.measure.RangeFormat; import org.apache.sis.measure.UnitFormat; +import org.apache.sis.util.Numbers; import org.apache.sis.util.Classes; import org.apache.sis.util.Localized; import org.apache.sis.util.ArraysExt; @@ -466,6 +467,8 @@ public abstract class CompoundFormat<T> extends Format implements Localized { return DefaultFormat.getInstance(valueType); } else if (valueType == Number.class) { return NumberFormat.getInstance(locale); + } else if (Numbers.isInteger(valueType)) { + return NumberFormat.getIntegerInstance(locale); } } else if (valueType == Date.class) { final DateFormat format; diff --git a/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java b/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java index 00fe190..15fe160 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java +++ b/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java @@ -161,7 +161,7 @@ final class DefaultFormat extends Format { /** * Unconditionally returns {@code this} since this format does not contain any modifiable field. - * This same {@code DefaultFormat} instances can be shared. + * The same {@code DefaultFormat} instances can be shared. */ @Override @SuppressWarnings("CloneDoesntCallSuperClone") diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java b/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java index 58f79b6..15a3b3c 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java +++ b/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java @@ -478,6 +478,31 @@ public final class DecimalFunctions extends Static { } /** + * Computes {@code (int) floor(log10(x))}. For values greater than one, this is the number of digits - 1 + * in the decimal representation of the given number. For values smaller than one, this is the number of + * fraction digits required for showing the first non-zero decimal digit. + * + * @param x the value for which to compute the logarithm. Must be greater than zero. + * @return logarithm of the given value, rounded toward zero. + * @throws ArithmeticException if the given value is zero, negative, infinity or NaN. + * + * @see MathFunctions#pow10(int) + * + * @since 1.0 + */ + public static int floorLog10(final double x) { + if (x > 0) { + int p = Numerics.toExp10(MathFunctions.getExponent(x)); // Rounded twice toward floor (may be too low). + final int i = p - EXPONENT_FOR_ZERO; // Convert to index in POW10 array + 1. + if (i >= 0 && i < POW10.length) { + if (POW10[i] <= x) p++; // If p is too low, adjust. + return p; + } + } + throw new ArithmeticException(String.valueOf(x)); + } + + /** * Returns {@code true} if the given numbers or equal or differ only by {@code accurate} * having more non-zero trailing decimal fraction digits than {@code approximate}. * diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/StatisticsFormat.java b/core/sis-utility/src/main/java/org/apache/sis/math/StatisticsFormat.java index f37c8cb..b96cd6b 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/math/StatisticsFormat.java +++ b/core/sis-utility/src/main/java/org/apache/sis/math/StatisticsFormat.java @@ -61,7 +61,7 @@ import static java.lang.Math.*; * </ul> * * @author Martin Desruisseaux (MPO, IRD, Geomatys) - * @version 0.8 + * @version 1.0 * * @see Statistics#toString() * @@ -338,37 +338,26 @@ public class StatisticsFormat extends TabularFormat<Statistics> { } } /* - * Initialize the NumberFormat for formatting integers without scientific notation. - * This is necessary since the format may have been modified by a previous execution - * of this method. - */ - final Format format = getFormat(Double.class); - if (format instanceof DecimalFormat) { - ((DecimalFormat) format).applyPattern("#0"); // Also disable scientific notation. - } else if (format instanceof NumberFormat) { - setFractionDigits((NumberFormat) format, 0); - } - /* * Iterates over the rows to format (count, minimum, maximum, mean, RMS, standard deviation), - * then iterate over columns (statistics on sample values, on the first derivatives, etc.) - * The NumberFormat configuration may be different for each column, but we can skip many - * reconfiguration in the common case where there is only one column. + * then iterate over columns (statistics on first set of sample values, on second set, etc.) + * The NumberFormat configuration may be different for each column. */ - boolean needsConfigure = false; - for (int i=0; i<KEYS.length; i++) { - switch (i) { - case 1: if (!showNaNCount) continue; else break; - // Case 0 and 1 use the above configuration for integers. - // Case 2 unconditionally needs a reconfiguration for floating point values. - // Case 3 and others need reconfiguration only if there is more than one column. - case 2: needsConfigure = true; break; - case 3: needsConfigure = (stats[0].differences() != null); break; + final Format countFormat = getFormat(Integer.class); + final Format valueFormat = getFormat(Double.class); + final Format[] formats = new Format[stats.length]; + for (int i=0; i<formats.length; i++) { + formats[i] = configure(valueFormat, stats[i], i != 0); + } + for (int line=0; line < KEYS.length; line++) { + if (line == 1 & !showNaNCount) { + continue; } table.setCellAlignment(TableAppender.ALIGN_LEFT); - table.append(resources.getString(KEYS[i])).append(':'); - for (final Statistics s : stats) { + table.append(resources.getString(KEYS[line])).append(':'); + for (int i=0; i<stats.length; i++) { + final Statistics s = stats[i]; final Number value; - switch (i) { + switch (line) { case 0: value = s.count(); break; case 1: value = s.countNaN(); break; case 2: value = s.minimum(); break; @@ -376,14 +365,11 @@ public class StatisticsFormat extends TabularFormat<Statistics> { case 4: value = s.mean(); break; case 5: value = s.rms(); break; case 6: value = s.standardDeviation(allPopulation); break; - default: throw new AssertionError(i); - } - if (needsConfigure) { - configure(format, s); + default: throw new AssertionError(line); } table.append(beforeFill); table.nextColumn(fillCharacter); - table.append(format.format(value)); + table.append((line >= 2 ? formats[i] : countFormat).format(value)); table.setCellAlignment(TableAppender.ALIGN_RIGHT); } table.append(lineSeparator); @@ -420,44 +406,69 @@ public class StatisticsFormat extends TabularFormat<Statistics> { * * @param format the formatter to configure. * @param stats the statistics for which to configure the formatter. + * @param clone whether to clone the given format before to modify it. + * @return the formatter to use. May be a clone of the given formatter. */ - private void configure(final Format format, final Statistics stats) { + private static Format configure(final Format format, final Statistics stats, final boolean clone) { final double minimum = stats.minimum(); final double maximum = stats.maximum(); final double extremum = max(abs(minimum), abs(maximum)); - if ((extremum >= 1E+10 || extremum <= 1E-4) && format instanceof DecimalFormat) { - /* - * The above threshold is high so that geocentric and projected coordinates in metres - * are not formatted with scientific notation (a threshold of 1E+7 is not enough). - * The number of decimal digits in the pattern is arbitrary. - */ - ((DecimalFormat) format).applyPattern("0.00000E00"); - } else { + int multiplier = 1; + if (format instanceof DecimalFormat) { + DecimalFormat df = (DecimalFormat) format; + multiplier = df.getMultiplier(); /* - * Computes a representative range of values. We take 2 standard deviations away - * from the mean. Assuming that data have a gaussian distribution, this is 97.7% - * of data. If the data have a uniform distribution, then this is 100% of data. + * Check for scientific notation: the threshold below is high so that geocentric and projected + * coordinates in metres are not formatted with scientific notation (a 1E+7 threshold is not + * enough). If the numbers seem to require scientific notation, switch to that notation only + * if the user has not already set a different number pattern. */ - double delta; - final double mean = stats.mean(); - delta = 2 * stats.standardDeviation(true); // 'true' is for avoiding NaN when count == 1. - delta = min(maximum, mean+delta) - max(minimum, mean-delta); // Range of 97.7% of values. - delta = max(delta/stats.count(), ulp(extremum)); // Mean delta for uniform distribution, not finer than 'double' accuracy. - if (format instanceof NumberFormat) { - setFractionDigits((NumberFormat) format, max(0, ADDITIONAL_DIGITS - + DecimalFunctions.fractionDigitsForDelta(delta, false))); - } else { - // A future version could configure DateFormat here. + if (multiplier == 1 && (extremum >= 1E+10 || extremum <= 1E-4)) { + final String pattern = df.toPattern(); + for (int i = pattern.length(); --i >= 0;) { + switch (pattern.charAt(i)) { + case '\'': // Quote character: if present, user probably personalized the pattern. + case '¤': // Currency sign: not asked by super.createFormat(…), so assumed user format. + case 'E': return format; // Scientific notation: not asked by super.createFormat(…), so assumed user format. + } + } + /* + * Apply the scientific notation on a clone in order to avoid misleading + * this 'configure' method next time we will format a Statistics object. + * The number of decimal digits in the pattern is arbitrary. + */ + df = (DecimalFormat) df.clone(); + df.applyPattern("0.00000E00"); + return df; } } - } - - /** - * Convenience method for setting the minimum and maximum fraction digits of the given format. - */ - private static void setFractionDigits(final NumberFormat format, final int digits) { - format.setMinimumFractionDigits(digits); - format.setMaximumFractionDigits(digits); + /* + * Computes a representative range of values. We take 2 standard deviations away + * from the mean. Assuming that data have a gaussian distribution, this is 97.7% + * of data. If the data have a uniform distribution, then this is 100% of data. + */ + double delta; + final double mean = stats.mean(); + delta = 2 * stats.standardDeviation(true); // 'true' is for avoiding NaN when count == 1. + delta = min(maximum, mean+delta) - max(minimum, mean-delta); // Range of 97.7% of values. + delta = max(delta/stats.count(), ulp(extremum)); // Mean delta for uniform distribution, not finer than 'double' accuracy. + if (format instanceof NumberFormat) { + int digits = DecimalFunctions.fractionDigitsForDelta(delta, false); + digits -= DecimalFunctions.floorLog10(multiplier); + digits = max(0, digits + ADDITIONAL_DIGITS); + NumberFormat nf = (NumberFormat) format; + if (digits != nf.getMinimumFractionDigits() || + digits != nf.getMaximumFractionDigits()) + { + if (clone) nf = (NumberFormat) nf.clone(); + nf.setMinimumFractionDigits(digits); + nf.setMaximumFractionDigits(digits); + } + return nf; + } else { + // A future version could configure DateFormat here. + } + return format; } /** diff --git a/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java b/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java index 1eb8ea6..bf6c53c 100644 --- a/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java +++ b/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java @@ -269,6 +269,33 @@ public final strictfp class DecimalFunctionsTest extends TestCase { } /** + * Tests {@link DecimalFunctions#floorLog10(double)} method. + */ + @Test + public void testFloorLog10() { + assertEquals( 0, floorLog10( 1)); + assertEquals( 0, floorLog10( 9)); + assertEquals( 1, floorLog10( 10)); + assertEquals( 1, floorLog10( 11)); + assertEquals( 1, floorLog10( 99)); + assertEquals( 2, floorLog10( 100)); + assertEquals( 2, floorLog10( 999)); + assertEquals( 3, floorLog10(1000)); + assertEquals( -1, floorLog10(0.100)); + assertEquals( -2, floorLog10(0.099)); + assertEquals( -2, floorLog10(0.010)); + assertEquals( -3, floorLog10(0.009)); + assertEquals( -3, floorLog10(0.001)); + assertEquals( 308, floorLog10(MAX_VALUE)); + assertEquals(-324, floorLog10(MIN_VALUE)); + try {floorLog10( 0); fail("Expected ArithmeticException.");} catch (ArithmeticException e) {} + try {floorLog10(-1); fail("Expected ArithmeticException.");} catch (ArithmeticException e) {} + try {floorLog10(NaN); fail("Expected ArithmeticException.");} catch (ArithmeticException e) {} + try {floorLog10(NEGATIVE_INFINITY); fail("Expected ArithmeticException.");} catch (ArithmeticException e) {} + try {floorLog10(POSITIVE_INFINITY); fail("Expected ArithmeticException.");} catch (ArithmeticException e) {} + } + + /** * Tests {@link DecimalFunctions#equalsIgnoreMissingFractionDigits(double, double)}. * This test uses the conversion factor from degrees to radians as a use case. * This factor is written as {@code ANGLEUNIT["degree", 0.01745329252]} in some diff --git a/core/sis-utility/src/test/java/org/apache/sis/math/StatisticsFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/math/StatisticsFormatTest.java index 32a956e..b8eed85 100644 --- a/core/sis-utility/src/test/java/org/apache/sis/math/StatisticsFormatTest.java +++ b/core/sis-utility/src/test/java/org/apache/sis/math/StatisticsFormatTest.java @@ -16,6 +16,8 @@ */ package org.apache.sis.math; +import java.text.Format; +import java.text.NumberFormat; import java.util.Locale; import org.junit.Test; import org.apache.sis.test.TestCase; @@ -28,7 +30,7 @@ import static org.apache.sis.test.Assert.*; * Tests the {@link StatisticsFormat} class. * * @author Martin Desruisseaux (Geomatys) - * @version 0.3 + * @version 1.0 * @since 0.3 * @module */ @@ -90,4 +92,40 @@ public final strictfp class StatisticsFormatTest extends TestCase { "│ Standard deviation: │ 6.49 │ 6.99 │ 6.19 │\n" + "└─────────────────────┴─────────────┴───────┴─────────┘\n", text); } + + /** + * Tests the formatting of {@code Statistics} with customized number format. + * + * @since 1.0 + */ + @Test + @DependsOnMethod("testFormattingWithoutHeader") + public void testFormattingPercent() { + final Statistics statistics = new Statistics("Percent"); + statistics.accept(0.1); + statistics.accept(0.8); + statistics.accept(0.6); + statistics.accept(0.3); + statistics.accept(0.1); + statistics.accept(0.7); + + final StatisticsFormat format = new StatisticsFormat(Locale.US, null, null) { + @Override protected Format createFormat(final Class<?> valueType) { + if (Number.class == valueType) { + return NumberFormat.getPercentInstance(getLocale()); + } else { + return super.createFormat(valueType); + } + } + }; + final String text = format.format(statistics); + assertMultilinesEquals( + " Percent\n" + + "Number of values: 6\n" + + "Minimum value: 10.0%\n" + + "Maximum value: 80.0%\n" + + "Mean value: 43.3%\n" + + "Root Mean Square: 51.6%\n" + + "Standard deviation: 30.8%\n", text); + } }
