sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: More effort to prepend a SI prefix when possible at formatting time.
Date Sun, 05 Aug 2018 17:39:34 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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 91ea384  More effort to prepend a SI prefix when possible at formatting time.
91ea384 is described below

commit 91ea3843aa47bff39aabade7d0ef6507dfc48623
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Aug 5 19:39:12 2018 +0200

    More effort to prepend a SI prefix when possible at formatting time.
---
 .../main/java/org/apache/sis/math/Fraction.java    |  11 +-
 .../org/apache/sis/measure/AbstractConverter.java  |  22 ++-
 .../java/org/apache/sis/measure/AbstractUnit.java  |   9 +-
 .../org/apache/sis/measure/ConventionalUnit.java   |  15 +-
 .../org/apache/sis/measure/LinearConverter.java    |   8 -
 .../main/java/org/apache/sis/measure/Prefixes.java |  64 +++++--
 .../java/org/apache/sis/measure/SystemUnit.java    |   9 +-
 .../java/org/apache/sis/measure/UnitFormat.java    | 191 +++++++++++----------
 .../main/java/org/apache/sis/measure/Units.java    |  12 +-
 .../apache/sis/measure/ConventionalUnitTest.java   |   2 +-
 .../java/org/apache/sis/measure/PrefixesTest.java  |  61 ++++---
 .../org/apache/sis/measure/UnitFormatTest.java     |  13 +-
 12 files changed, 251 insertions(+), 166 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java b/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java
index 2862f94..92846f5 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java
@@ -162,7 +162,16 @@ public final class Fraction extends Number implements Comparable<Fraction>,
Seri
      * @throws ArithmeticException if the result overflows.
      */
     public Fraction negate() {
-        return (numerator == 0) ? this : new Fraction(Math.negateExact(numerator), denominator);
+        int n = numerator;
+        int d = denominator;
+        if (n != 0) {
+            n = Math.negateExact(n);
+        } else if (d != 0) {
+            d = Math.negateExact(d);
+        } else {
+            return this;
+        }
+        return new Fraction(n, d);
     }
 
     /**
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
index 737cea0..b823239 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractConverter.java
@@ -25,7 +25,7 @@ import org.apache.sis.math.DecimalFunctions;
  * Base class of unit converters.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -70,10 +70,30 @@ abstract class AbstractConverter implements UnitConverter, Serializable
{
     }
 
     /**
+     * Returns the scale factor of the given converter if the conversion is linear, or NaN
otherwise.
+     */
+    static double scale(final UnitConverter converter) {
+        if (converter != null && converter.isLinear() && converter.convert(0)
== 0) {
+            // Above check for converter(0) is a paranoiac check since
+            // JSR-363 said that a "linear" converter has no offset.
+            return converter.convert(1);
+        }
+        return Double.NaN;
+    }
+
+    /**
      * Returns the value of the given number, with special handling for {@link Float} value
on the assumption
      * that the original value was written in base 10. This is usually the case for unit
conversion factors.
      */
     static double doubleValue(final Number n) {
         return (n instanceof Float) ? DecimalFunctions.floatToDouble(n.floatValue()) : n.doubleValue();
     }
+
+    /**
+     * Returns {@code true} if the given floating point numbers are considered equal.
+     * The tolerance factor used in this method is arbitrary and may change in any future
version.
+     */
+    static boolean epsilonEquals(final double expected, final double actual) {
+        return Math.abs(expected - actual) <= Math.scalb(Math.ulp(expected), 4);
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java
index 202f044..66b7530 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java
@@ -131,6 +131,13 @@ abstract class AbstractUnit<Q extends Quantity<Q>> implements
Unit<Q>, LenientCo
     }
 
     /**
+     * Returns {@code true} if the use of SI prefixes is allowed for the given unit.
+     */
+    static boolean isPrefixable(final Unit<?> unit) {
+        return (unit instanceof AbstractUnit<?>) && ((AbstractUnit<?>)
unit).isPrefixable();
+    }
+
+    /**
      * Returns {@code true} if the use of SI prefixes is allowed for this unit.
      */
     final boolean isPrefixable() {
@@ -274,7 +281,7 @@ abstract class AbstractUnit<Q extends Quantity<Q>> implements
Unit<Q>, LenientCo
         if (Math.abs(multiplier) < 1) {
             final double inverse = 1 / multiplier;
             final double r = Math.rint(inverse);
-            if (LinearConverter.epsilonEquals(inverse, r)) {
+            if (AbstractConverter.epsilonEquals(inverse, r)) {
                 return r;
             }
         }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java b/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java
index 2154edd..68678a8 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/ConventionalUnit.java
@@ -106,17 +106,10 @@ final class ConventionalUnit<Q extends Quantity<Q>> extends
AbstractUnit<Q> {
         String symbol = null;
         if (target.isPrefixable()) {
             final String ts = target.getSymbol();
-            if (ts != null && !ts.isEmpty() && toTarget.isLinear()) {
-                final int power = power(ts);
-                if (power != 0) {
-                    double scale = toTarget.convert(1);
-                    switch (power) {
-                        case 1:  break;
-                        case 2:  scale = Math.sqrt(scale); break;
-                        case 3:  scale = Math.cbrt(scale); break;
-                        default: scale = Math.pow(scale, 1.0/power);
-                    }
-                    final char prefix = Prefixes.symbol(scale);
+            if (ts != null && !ts.isEmpty()) {
+                double scale = AbstractConverter.scale(toTarget);
+                if (!Double.isNaN(scale)) {
+                    final char prefix = Prefixes.symbol(scale, power(ts));
                     if (prefix != 0) {
                         symbol = Prefixes.concat(prefix, ts);
                     }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java b/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java
index 92cc178..1277864 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java
@@ -416,14 +416,6 @@ final class LinearConverter extends AbstractConverter implements LenientComparab
     }
 
     /**
-     * Returns {@code true} if the given floating point numbers are considered equal.
-     * The tolerance factor used in this method is arbitrary and may change in any future
version.
-     */
-    static boolean epsilonEquals(final double expected, final double actual) {
-        return Math.abs(expected - actual) <= Math.scalb(Math.ulp(expected), 4);
-    }
-
-    /**
      * Returns a string representation of this converter for debugging purpose.
      * This string representation may change in any future SIS release.
      * Current format is of the form "y = scale⋅x + offset".
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Prefixes.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Prefixes.java
index 5d85918..353023f 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Prefixes.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Prefixes.java
@@ -17,7 +17,9 @@
 package org.apache.sis.measure;
 
 import java.util.Arrays;
+import javax.measure.Unit;
 import javax.measure.Quantity;
+import javax.measure.UnitConverter;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.math.MathFunctions;
 
@@ -56,7 +58,7 @@ final class Prefixes {
      * The SI prefixes from smallest to largest. Power of tens go from -24 to +24 inclusive
with a step of 3,
      * except for the addition of -2, -1, +1, +2 and the omission of 0.
      *
-     * @see #symbol(double)
+     * @see #symbol(double, int)
      */
     private static final char[] ENUM = {'y','z','a','f','p','n','µ','m','c','d','㍲','h','k','M','G','T','P','E','Z','Y'};
 
@@ -108,10 +110,21 @@ final class Prefixes {
 
     /**
      * Returns the SI prefix symbol for the given scale factor, or 0 if none.
+     *
+     * @param  scale  the scale factor.
+     * @param  power  the unit power. For example if we are scaling m², then this is 2.
+     * @return the prefix, or 0 if none.
      */
-    static char symbol(final double scale) {
+    static char symbol(double scale, final int power) {
+        switch (power) {
+            case 0:  return 0;
+            case 1:  break;
+            case 2:  scale = Math.sqrt(scale); break;
+            case 3:  scale = Math.cbrt(scale); break;
+            default: scale = Math.pow(scale, 1.0/power);
+        }
         final int n = Numerics.toExp10(Math.getExponent(scale)) + 1;
-        if (LinearConverter.epsilonEquals(MathFunctions.pow10(n), scale)) {
+        if (AbstractConverter.epsilonEquals(MathFunctions.pow10(n), scale)) {
             int i = Math.abs(n);
             switch (i) {
                 case 0:  return 0;
@@ -138,15 +151,41 @@ final class Prefixes {
     }
 
     /**
-     * If the given string begins with a two-letter prefix, returns the one-character representation
of that prefix.
-     * Otherwise returns the given prefix unchanged. This method is the converse of {@link
#concat(char, String)}.
+     * Returns the unit for the given symbol, taking the SI prefix in account. The given
string is usually a single symbol
+     * like "km", but may be an expression like "m³" or "m/s" if the given symbol is explicitly
registered as an item that
+     * {@link Units#get(String)} recognizes. This method does not perform any arithmetic
operation on {@code Unit},
+     * except a check for the exponent.
      *
-     * @param  prefix  value of {@code uom.charAt(0)}.
-     * @param  uom     the unit symbol. Length shall be at least 2 characters long.
-     * @return the one-character prefix, or the given {@code prefix} unchanged.
+     * @param  uom  a symbol compliant with the rules documented in {@link AbstractUnit#symbol}.
+     * @return the unit for the given symbol, or {@code null} if no unit is found.
      */
-    static char twoLetters(final char prefix, final String uom) {
-        return (prefix == 'd' && uom.charAt(1) == 'a') ? '㍲' : prefix;
+    static Unit<?> getUnit(final String uom) {
+        Unit<?> unit = Units.get(uom);
+        if (unit == null && uom.length() >= 2) {
+            int s = 1;
+            char prefix = uom.charAt(0);
+            if (prefix == 'd' && uom.charAt(1) == 'a') {
+                prefix = '㍲';      // Converse of above 'concat(char, String)' method.
+                s = 2;              // Skip "da", which we represent by '㍲'.
+            }
+            unit = Units.get(uom.substring(s));
+            if (AbstractUnit.isPrefixable(unit)) {
+                LinearConverter c = Prefixes.converter(prefix);
+                if (c != null) {
+                    String symbol = unit.getSymbol();
+                    final int power = ConventionalUnit.power(symbol);
+                    if (power != 0) {
+                        if (power != 1) {
+                            c = LinearConverter.pow(c, power, false);
+                        }
+                        symbol = Prefixes.concat(prefix, symbol);
+                        return new ConventionalUnit<>((AbstractUnit<?>) unit,
c, symbol.intern(), (byte) 0, (short) 0);
+                    }
+                }
+            }
+            unit = null;
+        }
+        return unit;
     }
 
     /**
@@ -160,14 +199,15 @@ final class Prefixes {
     static <Q extends Quantity<Q>> ConventionalUnit<Q> pseudoSystemUnit(final
SystemUnit<Q> unit) {
         if ((unit.scope & ~UnitRegistry.SI) == 0 && unit.dimension.numeratorIs('M'))
{
             if (unit == Units.KILOGRAM) {
-                return (ConventionalUnit<Q>) Units.GRAM;                    // Optimization
for a common case.
+                return (ConventionalUnit<Q>) Units.GRAM;            // Optimization
for a common case.
             } else {
                 String symbol = unit.getSymbol();
                 if (symbol != null && symbol.length() >= 3 && symbol.startsWith("kg")
                         && !AbstractUnit.isSymbolChar(symbol.codePointAt(2)))
                 {
                     symbol = symbol.substring(1);
-                    return new ConventionalUnit<>(unit, converter('m'), symbol, UnitRegistry.PREFIXABLE,
(byte) 0);
+                    UnitConverter c = converter('m');
+                    return new ConventionalUnit<>(unit, c, symbol, UnitRegistry.PREFIXABLE,
(byte) 0).unique(symbol);
                 }
             }
         }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
index d186a3c..1b92853 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
@@ -525,11 +525,10 @@ final class SystemUnit<Q extends Quantity<Q>> extends AbstractUnit<Q>
implements
              * Special case for Units.KILOGRAM, to be replaced by Units.GRAM so a prefix
can be computed.
              * The kilogram may appear in an expression like "kg/m", which we want to replace
by "g/m".
              *
-             * Note: we could argue that this block should be UnitFormat work rather than
SystemUnit.
-             * For now we perform this work here because this unit symbol may be different
than what
-             * UnitFormat would infer, for example because of the symbols recorded by the
sequence of
-             * Unit.multiply(Unit) and Unit.divide(Unit) operations. However we may revisit
this policy
-             * in a future version.
+             * Note: we could argue that this block should be UnitFormat's work rather than
SystemUnit.
+             * We perform this work here because this unit symbol may be different than what
UnitFormat
+             * would infer, for example because of symbols recorded by sequence of Unit.multiply(Unit)
+             * and Unit.divide(Unit) operations.
              */
             operation = operation.concatenate(pseudo.toTarget.inverse());
             base = pseudo;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
index 7ccf82a..ee7cc59 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
@@ -588,9 +588,11 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
         /*
          * Choice 1: label specified by a call to label(Unit, String).
          */
-        String label = unitToLabel.get(unit);
-        if (label != null) {
-            return toAppendTo.append(label);
+        {
+            final String label = unitToLabel.get(unit);
+            if (label != null) {
+                return toAppendTo.append(label);
+            }
         }
         /*
          * Choice 2: value specified by Unit.getName(). We skip this check if the given Unit
is an instance
@@ -600,12 +602,12 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
          */
         if (style == Style.NAME) {
             if (!(unit instanceof AbstractUnit)) {
-                label = unit.getName();
+                final String label = unit.getName();
                 if (label != null) {
                     return toAppendTo.append(label);
                 }
             } else {
-                label = unit.getSymbol();
+                String label = unit.getSymbol();
                 if (label != null) {
                     if (label.isEmpty()) {
                         label = UNITY;
@@ -626,78 +628,114 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
          * Apache SIS implementation use Unicode characters in the symbol, which are not
valid for UCUM.
          * But Styme.UCUM.appendSymbol(…) performs required replacements.
          */
-        label = unit.getSymbol();
-        if (label != null) {
-            return style.appendSymbol(toAppendTo, label);
+        {
+            final String symbol = unit.getSymbol();
+            if (symbol != null) {
+                return style.appendSymbol(toAppendTo, symbol);
+            }
         }
         /*
          * Choice 4: if all the above failed, fallback on a symbol created from the base
units and their power.
-         * Note that this may produce more verbose symbols than needed since derived units
like Volt or Watt are
-         * decomposed into their base SI units.
+         * Note that this may produce more verbose symbols than needed because derived units
like Volt or Watt
+         * are decomposed into their base SI units. The scale factor will be inserted before
the unit components,
+         * e.g. "30⋅m∕s". Note that a scale factor relative to system unit may not be
what we want if the unit
+         * contains "kg", since it block us from using SI prefixes. But in many cases (not
all), a symbol will
+         * have been created by SystemUnit.transform(…), in which case "Choice 3" above
would have been executed.
          */
-        boolean hasPositivePower = false;
+        final Unit<?> unscaled = unit.getSystemUnit();
+        @SuppressWarnings("unchecked")          // Both 'unit' and 'unscaled' are 'Unit<Q>'.
+        final double scale = AbstractConverter.scale(unit.getConverterTo((Unit) unscaled));
+        if (Double.isNaN(scale)) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.NonRatioUnit_1,
+                    "?⋅" + Style.OPEN + unscaled + Style.CLOSE));
+        }
+        /*
+         * In addition of the scale, we will need to know:
+         *
+         *   - The components (for example "m" and "s" in "m∕s").
+         *   - Whether we have at least one component on the left side of "∕" operation.
+         *     Used for determining if we should prepend "1" before the "∕" symbol.
+         *   - If there is exactly one component on the left side of "∕" and that component
+         *     is prefixable, the power raising that component. Used for choosing a prefix.
+         */
+        int prefixPower = 0;
+        boolean hasNumerator = false;
         final Map<? extends Unit<?>, ? extends Number> components;
-        if (unit instanceof AbstractUnit<?>) {
+        if (unscaled instanceof AbstractUnit<?>) {
             // In Apache SIS implementation, power may be fractional.
-            final Map<SystemUnit<?>, Fraction> c = ((AbstractUnit<?>) unit).getBaseSystemUnits();
-            for (final Fraction power : c.values()) {
-                hasPositivePower = (power.signum() > 0);
-                if (hasPositivePower) break;
-            }
+            final Map<SystemUnit<?>, Fraction> c = ((AbstractUnit<?>) unscaled).getBaseSystemUnits();
             components = c;
+            for (final Map.Entry<SystemUnit<?>, Fraction> e : c.entrySet()) {
+                final Fraction power = e.getValue();
+                if (power.signum() > 0) {
+                    hasNumerator = true;
+                    if (prefixPower == 0 && power.denominator == 1 && e.getKey().isPrefixable())
{
+                        prefixPower = power.numerator;
+                    } else {
+                        prefixPower = 0;
+                        break;
+                    }
+                }
+            }
         } else {
             // Fallback for foreigner implementations (power restricted to integer).
-            Map<? extends Unit<?>, Integer> c = unit.getBaseUnits();
+            Map<? extends Unit<?>, Integer> c = unscaled.getBaseUnits();
             if (c == null) c = Collections.singletonMap(unit, 1);
-            for (final Integer power : c.values()) {
-                hasPositivePower = (power > 0);
-                if (hasPositivePower) break;
-            }
             components = c;
+            for (final Map.Entry<? extends Unit<?>, Integer> e : c.entrySet())
{
+                final int power = e.getValue();
+                if (power > 0) {
+                    hasNumerator = true;
+                    if (prefixPower == 0 && AbstractUnit.isPrefixable(e.getKey()))
{
+                        prefixPower = power;
+                    } else {
+                        prefixPower = 0;
+                        break;
+                    }
+                }
+            }
         }
-        final double scale = Units.toStandardUnit(unit);
+        /*
+         * Append the scale factor. If we can use a prefix (e.g. "km" instead of "1000⋅m"),
we will do that.
+         * Otherwise if the scale is a power of 10 and we are allowed to use Unicode symbols,
we will write
+         * for example 10⁵⋅m instead of 100000⋅m. If the scale is not a power of 10,
or if we are requested
+         * to format UCUM symbol, then we fallback on the usual 'Double.toString(double)'
representation.
+         */
         if (scale != 1) {
-            if (Double.isNaN(scale)) {
-                throw new IllegalArgumentException(Errors.format(Errors.Keys.NonRatioUnit_1,
-                        "?⋅" + Style.OPEN + unit.getSystemUnit() + Style.CLOSE));
-            }
-            boolean asFloatingPoint = (style == Style.UCUM);
-            if (!asFloatingPoint) {
-                /*
-                 * Try to format the scale using exponent, for example 10⁵⋅m instead
of 100000⋅m.
-                 * If the scale is not a power of 10, or if we have been requested to format
UCUM
-                 * symbol, then we fallback on the usual 'Double.toString(double)' representation.
-                 */
-                double power = Math.log10(scale);
-                if (2*Math.ulp(power) >= Math.abs(power - (power = Math.round(power))))
{
-                    toAppendTo.append("10");
-                    final String text = Integer.toString((int) power);
-                    for (int i=0; i<text.length(); i++) {
-                        toAppendTo.append(Characters.toSuperScript(text.charAt(i)));
+            final char prefix = Prefixes.symbol(scale, prefixPower);
+            if (prefix != 0) {
+                toAppendTo.append(Prefixes.concat(prefix, ""));
+            } else {
+                boolean asPowerOf10 = (style != Style.UCUM);
+                if (asPowerOf10) {
+                    double power = Math.log10(scale);
+                    asPowerOf10 = AbstractConverter.epsilonEquals(power, power = Math.round(power));
+                    if (asPowerOf10) {
+                        toAppendTo.append("10");
+                        final String text = Integer.toString((int) power);
+                        for (int i=0; i<text.length(); i++) {
+                            toAppendTo.append(Characters.toSuperScript(text.charAt(i)));
+                        }
                     }
-                } else {
-                    asFloatingPoint = true;                         // Scale is not a power
of 10.
                 }
-            }
-            if (asFloatingPoint) {
-                final String text = Double.toString(scale);
-                int length = text.length();
-                if (text.endsWith(".0")) {
-                    length -= 2;
+                if (!asPowerOf10) {
+                    final String text = Double.toString(scale);
+                    int length = text.length();
+                    if (text.endsWith(".0")) length -= 2;
+                    toAppendTo.append(text, 0, length);
+                }
+                /*
+                 * The 'formatComponents' method appends division symbol only, no multiplication
symbol.
+                 * If we have formatted a scale factor and there is at least one component
to multiply,
+                 * we need to append the multiplication symbol ourselves. Note that 'formatComponents'
+                 * put numerators before denominators, so we are sure that the first term
after the
+                 * multiplication symbol is a numerator.
+                 */
+                if (hasNumerator) {
+                    toAppendTo.append(style.multiply);
                 }
-                toAppendTo.append(text, 0, length);
-            }
-            /*
-             * The 'formatComponents' method appends division symbol only, no multiplication
symbol.
-             * If we have formatted a scale factor and there is at least one component to
multiply,
-             * we need to append the multiplication symbol ourselves. Note that 'formatComponents'
-             * put numerators before denominators, so we are sure that the first term after
the
-             * multiplication symbol is a numerator.
-             */
-            if (hasPositivePower) {
-                toAppendTo.append(style.multiply);
             }
-        } else if (!hasPositivePower) {
+        } else if (!hasNumerator) {
             toAppendTo.append('1');
         }
         formatComponents(components, style, toAppendTo);
@@ -1256,7 +1294,8 @@ search:     while ((i = CharSequences.skipTrailingWhitespaces(symbols,
start, i)
     /**
      * Parses a single unit symbol with its exponent.
      * The given symbol shall not contain multiplication or division operator except in exponent.
-     * Parsing of fractional exponent as in "m2/3" is not yet supported.
+     * Parsing of fractional exponent as in "m2/3" is supported; other operations in the
exponent
+     * will cause an exception to be thrown.
      *
      * @param  symbols    the complete string specified by the user.
      * @param  lower      index where to begin parsing in the {@code symbols} string.
@@ -1277,7 +1316,7 @@ search:     while ((i = CharSequences.skipTrailingWhitespaces(symbols,
start, i)
          */
         Unit<?> unit = labelToUnit.get(uom);
         if (unit == null) {
-            unit = getPrefixed(uom);
+            unit = Prefixes.getUnit(uom);
             if (unit == null) {
                 final int length = uom.length();
                 if (length == 0) {
@@ -1362,7 +1401,7 @@ search:     while ((i = CharSequences.skipTrailingWhitespaces(symbols,
start, i)
                                 }
                             }
                         }
-                        unit = getPrefixed(uom.substring(CharSequences.skipLeadingWhitespaces(uom,
0, i), i));
+                        unit = Prefixes.getUnit(uom.substring(CharSequences.skipLeadingWhitespaces(uom,
0, i), i));
                         if (unit != null) {
                             int numerator   = power.numerator;
                             int denominator = power.denominator;
@@ -1421,32 +1460,6 @@ search:     while ((i = CharSequences.skipTrailingWhitespaces(symbols,
start, i)
     }
 
     /**
-     * Returns the unit for the given symbol, taking the SI prefix in account.
-     * This method does not perform any arithmetic operation on {@code Unit}.
-     * Returns {@code null} if no unit is found.
-     */
-    private static Unit<?> getPrefixed(final String uom) {
-        Unit<?> unit = Units.get(uom);
-        if (unit == null && uom.length() >= 2) {
-            int s = 1;
-            char prefix = uom.charAt(0);
-            if (prefix != (prefix = Prefixes.twoLetters(prefix, uom))) {
-                s = 2;          // Skip "da", which we represent by '㍲'.
-            }
-            unit = Units.get(uom.substring(s));
-            if (unit instanceof AbstractUnit<?> && ((AbstractUnit<?>)
unit).isPrefixable()) {
-                final LinearConverter c = Prefixes.converter(prefix);
-                if (c != null) {
-                    final String symbol = Prefixes.concat(prefix, unit.getSymbol());
-                    return new ConventionalUnit<>((AbstractUnit<?>) unit, c,
symbol.intern(), (byte) 0, (short) 0);
-                }
-            }
-            unit = null;
-        }
-        return unit;
-    }
-
-    /**
      * Parses text from a string to produce a unit. The default implementation delegates
to {@link #parse(CharSequence)}
      * and wraps the {@link ParserException} into a {@link ParseException} for compatibility
with {@code java.text} API.
      *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
index 596990d..1b7335d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
@@ -1306,6 +1306,8 @@ public final class Units extends Static {
      *
      * <p><b>Implementation note:</b> this method must be defined in this
{@code Units} class
      * in order to force a class initialization before use.</p>
+     *
+     * @see Prefixes#getUnit(String)
      */
     @SuppressWarnings("unchecked")
     static Unit<?> get(final String symbol) {
@@ -1497,15 +1499,7 @@ public final class Units extends Static {
      *         measurement in the standard unit, or NaN if the conversion can not be expressed
by a scale factor.
      */
     public static <Q extends Quantity<Q>> double toStandardUnit(final Unit<Q>
unit) {
-        if (unit != null) {
-            final UnitConverter converter = unit.getConverterTo(unit.getSystemUnit());
-            if (converter.isLinear() && converter.convert(0) == 0) {
-                // Above check for converter(0) is a paranoiac check since
-                // JSR-363 said that a "linear" converter has no offset.
-                return converter.convert(1);
-            }
-        }
-        return Double.NaN;
+        return AbstractConverter.scale(unit == null ? null : unit.getConverterTo(unit.getSystemUnit()));
     }
 
     /**
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/ConventionalUnitTest.java
b/core/sis-utility/src/test/java/org/apache/sis/measure/ConventionalUnitTest.java
index 815e0bd..c96fa42 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/ConventionalUnitTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/ConventionalUnitTest.java
@@ -41,7 +41,7 @@ import static org.apache.sis.test.Assert.*;
 @DependsOn({SystemUnitTest.class, LinearConverterTest.class, PrefixesTest.class})
 public final strictfp class ConventionalUnitTest extends TestCase {
     /**
-     * Verifies the properties if the given unit.
+     * Verifies the properties in the given unit.
      *
      * @param  system  the expected system unit.
      * @param  unit    the conventional unit to verify.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/PrefixesTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/PrefixesTest.java
index 2394e5e..dcfb206 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/PrefixesTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/PrefixesTest.java
@@ -23,7 +23,7 @@ import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.util.ArraysExt;
 import org.junit.Test;
 
-import static org.apache.sis.test.Assert.*;
+import static org.junit.Assert.*;
 
 
 /**
@@ -83,7 +83,7 @@ public final strictfp class PrefixesTest extends TestCase {
              * At this point we got the LinearConverter to use for the test,
              * and we know the expected prefix. Verify that we get that value.
              */
-            assertEquals("Prefixes.converter(double)", asString, String.valueOf(Prefixes.symbol(scale)));
+            assertEquals("Prefixes.converter(double)", asString, String.valueOf(Prefixes.symbol(scale,
1)));
             previousScale = scale;
         }
     }
@@ -102,31 +102,42 @@ public final strictfp class PrefixesTest extends TestCase {
     }
 
     /**
-     * Tests {@link Prefixes#symbol(double)}.
+     * Tests {@link Prefixes#symbol(double, int)}.
      */
     @Test
     public void testSymbol() {
-        assertEquals( 0 , Prefixes.symbol(1E-27));
-        assertEquals( 0 , Prefixes.symbol(1E-25));
-        assertEquals('y', Prefixes.symbol(1E-24));
-        assertEquals( 0 , Prefixes.symbol(1E-23));
-        assertEquals('n', Prefixes.symbol(1E-09));
-        assertEquals( 0 , Prefixes.symbol(1E-08));
-        assertEquals( 0 , Prefixes.symbol(1E-04));
-        assertEquals('m', Prefixes.symbol(1E-03));
-        assertEquals('c', Prefixes.symbol(1E-02));
-        assertEquals('d', Prefixes.symbol(1E-01));
-        assertEquals( 0 , Prefixes.symbol(    1));
-        assertEquals( 0 , Prefixes.symbol(    0));
-        assertEquals( 0 , Prefixes.symbol(  -10));
-        assertEquals('㍲', Prefixes.symbol(   10));
-        assertEquals('h', Prefixes.symbol(  100));
-        assertEquals('k', Prefixes.symbol( 1000));
-        assertEquals( 0 , Prefixes.symbol(1E+04));
-        assertEquals('G', Prefixes.symbol(1E+09));
-        assertEquals('Y', Prefixes.symbol(1E+24));
-        assertEquals( 0 , Prefixes.symbol(1E+25));
-        assertEquals( 0 , Prefixes.symbol(1E+27));
-        assertEquals( 0 , Prefixes.symbol(1E+25));
+        assertEquals( 0 , Prefixes.symbol(1E-27, 1));
+        assertEquals( 0 , Prefixes.symbol(1E-25, 1));
+        assertEquals('y', Prefixes.symbol(1E-24, 1));
+        assertEquals( 0 , Prefixes.symbol(1E-23, 1));
+        assertEquals('n', Prefixes.symbol(1E-09, 1));
+        assertEquals( 0 , Prefixes.symbol(1E-08, 1));
+        assertEquals( 0 , Prefixes.symbol(1E-04, 1));
+        assertEquals('m', Prefixes.symbol(1E-03, 1));
+        assertEquals('c', Prefixes.symbol(1E-02, 1));
+        assertEquals('d', Prefixes.symbol(1E-01, 1));
+        assertEquals( 0 , Prefixes.symbol(    1, 1));
+        assertEquals( 0 , Prefixes.symbol(    0, 1));
+        assertEquals( 0 , Prefixes.symbol(  -10, 1));
+        assertEquals('㍲', Prefixes.symbol(   10, 1));
+        assertEquals('h', Prefixes.symbol(  100, 1));
+        assertEquals('k', Prefixes.symbol( 1000, 1));
+        assertEquals( 0 , Prefixes.symbol(1E+04, 1));
+        assertEquals('G', Prefixes.symbol(1E+09, 1));
+        assertEquals('Y', Prefixes.symbol(1E+24, 1));
+        assertEquals( 0 , Prefixes.symbol(1E+25, 1));
+        assertEquals( 0 , Prefixes.symbol(1E+27, 1));
+        assertEquals( 0 , Prefixes.symbol(1E+25, 1));
+    }
+
+    /**
+     * Tests {@link Prefixes#getUnit(String)}.
+     */
+    @Test
+    public void testGetUnit() {
+        assertEquals( "m" , 1,    Units.toStandardUnit(Prefixes.getUnit( "m" )), STRICT);
+        assertEquals( "m²", 1,    Units.toStandardUnit(Prefixes.getUnit( "m²")), STRICT);
+        assertEquals("km",  1E+3, Units.toStandardUnit(Prefixes.getUnit("km" )), STRICT);
+        assertEquals("km²", 1E+6, Units.toStandardUnit(Prefixes.getUnit("km²")), STRICT);
     }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
index b307211..b03d581 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
@@ -287,9 +287,10 @@ public final strictfp class UnitFormatTest extends TestCase {
     public void testFormatUnusual() {
         final UnitFormat f = new UnitFormat(Locale.UK);
         final Unit<?> u1 = Units.SECOND.pow(-1).multiply(3);
-        assertEquals("3∕s",   f.format(u1));
-        assertEquals("3⋅m∕s", f.format(Units.METRE.multiply(u1)));
-        assertEquals("m^⅔",   f.format(Units.METRE.pow(2).root(3)));
+        assertEquals("3∕s",        f.format(u1));
+        assertEquals("3⋅m∕s",      f.format(Units.METRE.multiply(u1)));
+        assertEquals("m^⅔",        f.format(Units.METRE.pow(2).root(3)));
+        assertEquals("km²∕(s⋅kg)", f.format(Units.SQUARE_METRE.divide(Units.SECOND).divide(Units.KILOGRAM).multiply(1E+6)));
     }
 
     /**
@@ -420,6 +421,12 @@ public final strictfp class UnitFormatTest extends TestCase {
         ConventionalUnitTest.verify(Units.KILOGRAM,    f.parse("g"),    "g",    1E-3);
         ConventionalUnitTest.verify(Units.KILOGRAM,    f.parse("mg"),   "mg",   1E-6);
         /*
+         * When the unit contain an exponent, the conversion factor shall be raised
+         * to that exponent too.
+         */
+        assertEquals("km²", 1E+6, Units.toStandardUnit(f.parse("km²")), STRICT);
+        assertEquals("kJ²", 1E+6, Units.toStandardUnit(f.parse("kJ²")), STRICT);
+        /*
          * Verify that prefix are not accepted for conventional units. It would either be
illegal prefix duplication
          * (for example we should not accept "kkm" as if it was "k" + "km") or confusing
(for example "a" stands for
          * the tropical year, "ha" could be understood as 100 tropical years but is actually
used for hectare).


Mime
View raw message