sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/04: Fix a confusion in the NumberFormat settings performed by StatisticsFormat, in particular when values are percentages.
Date Sat, 03 Nov 2018 16:42:21 GMT
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 <martin.desruisseaux@geomatys.com>
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);
+    }
 }


Mime
View raw message