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: Group methods that are specific to SI in a Prefixes class. All those SI-specific methods are related to the management of prefixes. For now only SI prefixes are supported, but we may evolve the Prefixes class in a future version for supporting other sets of prefixes.
Date Sun, 05 Aug 2018 17:09:20 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 18fb1d0  Group methods that are specific to SI in a Prefixes class. All those SI-specific
methods are related to the management of prefixes. For now only SI prefixes are supported,
but we may evolve the Prefixes class in a future version for supporting other sets of prefixes.
18fb1d0 is described below

commit 18fb1d070f3e92cc83ed1f9fce18a6ec656c13bb
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Aug 5 15:12:37 2018 +0200

    Group methods that are specific to SI in a Prefixes class. All those SI-specific methods
are related to the management of prefixes. For now only SI prefixes are supported, but we
may evolve the Prefixes class in a future version for supporting other sets of prefixes.
---
 .../java/org/apache/sis/measure/AbstractUnit.java  |  12 +-
 .../org/apache/sis/measure/ConventionalUnit.java   |  50 +-----
 .../org/apache/sis/measure/LinearConverter.java    |  62 ++------
 .../main/java/org/apache/sis/measure/Prefixes.java | 176 +++++++++++++++++++++
 .../java/org/apache/sis/measure/SystemUnit.java    |  20 +--
 .../java/org/apache/sis/measure/UnitDimension.java |  13 +-
 .../java/org/apache/sis/measure/UnitFormat.java    |  46 +++---
 .../main/java/org/apache/sis/measure/Units.java    |  12 +-
 .../apache/sis/measure/ConventionalUnitTest.java   |  69 +-------
 .../apache/sis/measure/LinearConverterTest.java    |  39 +----
 .../java/org/apache/sis/measure/PrefixesTest.java  | 132 ++++++++++++++++
 .../apache/sis/test/suite/UtilityTestSuite.java    |   1 +
 12 files changed, 365 insertions(+), 267 deletions(-)

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 871a80d..202f044 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
@@ -97,7 +97,7 @@ abstract class AbstractUnit<Q extends Quantity<Q>> implements
Unit<Q>, LenientCo
      * A code that identifies whether this unit is part of SI system, or outside SI but accepted
for use with SI.
      * Value can be {@link UnitRegistry#SI}, {@link UnitRegistry#ACCEPTED}, other constants
or 0 if unknown.
      *
-     * <p>This information may be approximative since we can not always guess correctly
whether the result of
+     * <p>This information may be approximate since we can not always guess correctly
whether the result of
      * an operation is part of SI or not. Values given to the field may be adjusted in any
future version.</p>
      *
      * <p>This information is not serialized because {@link #readResolve()} will replace
the deserialized instance
@@ -274,7 +274,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 (epsilonEquals(inverse, r)) {
+            if (LinearConverter.epsilonEquals(inverse, r)) {
                 return r;
             }
         }
@@ -282,14 +282,6 @@ abstract class AbstractUnit<Q extends Quantity<Q>> implements
Unit<Q>, LenientCo
     }
 
     /**
-     * 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 the inverse of this unit.
      *
      * @return 1 / {@code this}
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 d9c6051..2154edd 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
@@ -29,15 +29,13 @@ import org.apache.sis.util.Characters;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.math.Fraction;
-import org.apache.sis.math.MathFunctions;
-import org.apache.sis.internal.util.Numerics;
 
 
 /**
  * A unit of measure which is related to a base or derived unit through a conversion formula.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -48,19 +46,6 @@ final class ConventionalUnit<Q extends Quantity<Q>> extends
AbstractUnit<Q> {
     private static final long serialVersionUID = 6963634855104019466L;
 
     /**
-     * The SI prefixes form 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 #prefix(double)
-     */
-    private static final char[] PREFIXES = {'y','z','a','f','p','n','µ','m','c','d','㍲','h','k','M','G','T','P','E','Z','Y'};
-
-    /**
-     * The maximal power of 1000 for the prefixes in the {@link #PREFIXES} array. Note that
1000⁸ = 1E+24.
-     */
-    static final int MAX_POWER = 8;
-
-    /**
      * The base, derived or alternate units to which this {@code ConventionalUnit} is related.
      * This is called "preferred unit" in GML. This is usually an instance of {@link SystemUnit},
      * but may also be another {@link ConventionalUnit} in some rare cases where this conventional
@@ -131,13 +116,9 @@ final class ConventionalUnit<Q extends Quantity<Q>> extends
AbstractUnit<Q> {
                         case 3:  scale = Math.cbrt(scale); break;
                         default: scale = Math.pow(scale, 1.0/power);
                     }
-                    final char prefix = prefix(scale);
+                    final char prefix = Prefixes.symbol(scale);
                     if (prefix != 0) {
-                        if (prefix == '㍲') {
-                            symbol = UnitFormat.DECA + ts;
-                        } else {
-                            symbol = prefix + ts;
-                        }
+                        symbol = Prefixes.concat(prefix, ts);
                     }
                 }
             }
@@ -231,31 +212,6 @@ final class ConventionalUnit<Q extends Quantity<Q>> extends
AbstractUnit<Q> {
     }
 
     /**
-     * Returns the SI prefix for the given scale factor, or 0 if none.
-     */
-    @SuppressWarnings("null")
-    static char prefix(final double scale) {
-        final int n = Numerics.toExp10(Math.getExponent(scale)) + 1;
-        if (epsilonEquals(MathFunctions.pow10(n), scale)) {
-            int i = Math.abs(n);
-            switch (i) {
-                case 0:  return 0;
-                case 1:  // Fallthrough
-                case 2:  break;
-                default: {
-                    if (i > (MAX_POWER*3) || (i % 3) != 0) {
-                        return 0;
-                    }
-                    i = i/3 + 2;
-                    break;
-                }
-            }
-            return PREFIXES[n >= 0 ? (MAX_POWER+1) + i : (MAX_POWER+2) - i];
-        }
-        return 0;
-    }
-
-    /**
      * Returns the dimension of this unit.
      * Two units {@code u1} and {@code u2} are {@linkplain #isCompatible(Unit) compatible}
      * if and only if {@code u1.getDimension().equals(u2.getDimension())}.
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 69b36e8..92cc178 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
@@ -17,7 +17,6 @@
 package org.apache.sis.measure;
 
 import java.util.List;
-import java.util.Arrays;
 import java.util.Collections;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -45,7 +44,7 @@ import org.apache.sis.internal.util.Numerics;
  * and know how to copy the {@code UnitConverter} coefficients into an affine transform matrix.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -56,25 +55,6 @@ final class LinearConverter extends AbstractConverter implements LenientComparab
     private static final long serialVersionUID = -3759983642723729926L;
 
     /**
-     * The SI prefixes in increasing order. The only two-letters prefix – “da” –
is encoded using the JCK compatibility
-     * character “㍲”. The Greek letter μ is repeated twice: the U+00B5 character for
micro sign (this is the character
-     * that Apache SIS uses in unit symbols) and the U+03BC character for the Greek small
letter “mu” (the later is the
-     * character that appears when decomposing JCK compatibility characters with {@link java.text.Normalizer}).
-     * Both characters have same appearance but different values.
-     *
-     * <p>For each prefix at index <var>i</var>, the multiplication factor
is given by 10 raised to power {@code POWERS[i]}.</p>
-     */
-    private static final char[] PREFIXES = {'E','G','M','P','T','Y','Z','a','c','d','f','h','k','m','n','p','y','z','µ','μ','㍲'};
-    private static final byte[] POWERS   = {18,  9,  6, 15, 12, 24, 21,-18, -2, -1,-15, 
2,  3, -3, -9,-12,-24,-21, -6, -6,  1};
-
-    /**
-     * The converters for SI prefixes, created when first needed.
-     *
-     * @see #forPrefix(char)
-     */
-    private static final LinearConverter[] SI = new LinearConverter[POWERS.length];
-
-    /**
      * The identity linear converter.
      */
     static final LinearConverter IDENTITY = new LinearConverter(1, 0, 1);
@@ -148,34 +128,6 @@ final class LinearConverter extends AbstractConverter implements LenientComparab
     }
 
     /**
-     * Returns the converter for the given SI prefix, or {@code null} if none.
-     * Those converters are created when first needed and cached for reuse.
-     */
-    static LinearConverter forPrefix(final char prefix) {
-        final int i = Arrays.binarySearch(PREFIXES, prefix);
-        if (i < 0) {
-            return null;
-        }
-        synchronized (SI) {
-            LinearConverter c = SI[i];
-            if (c == null) {
-                final int p = POWERS[i];
-                final double numerator, denominator;
-                if (p >= 0) {
-                    numerator = MathFunctions.pow10(p);
-                    denominator = 1;
-                } else {
-                    numerator = 1;
-                    denominator = MathFunctions.pow10(-p);
-                }
-                c = scale(numerator, denominator);
-                SI[i] = c;
-            }
-            return c;
-        }
-    }
-
-    /**
      * Raises the given converter to the given power. This method assumes that the given
converter
      * {@linkplain #isLinear() is linear} (this is not verified) and takes only the scale
factor;
      * the offset (if any) is ignored.
@@ -459,8 +411,16 @@ final class LinearConverter extends AbstractConverter implements LenientComparab
      * except for rounding errors.
      */
     boolean equivalent(final LinearConverter other) {
-        return AbstractUnit.epsilonEquals(scale  * other.divisor, other.scale  * divisor)
&&
-               AbstractUnit.epsilonEquals(offset * other.divisor, other.offset * divisor);
+        return epsilonEquals(scale  * other.divisor, other.scale  * divisor) &&
+               epsilonEquals(offset * other.divisor, other.offset * divisor);
+    }
+
+    /**
+     * 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/Prefixes.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Prefixes.java
new file mode 100644
index 0000000..5d85918
--- /dev/null
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Prefixes.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.measure;
+
+import java.util.Arrays;
+import javax.measure.Quantity;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.math.MathFunctions;
+
+
+/**
+ * Utility methods related to the management of prefixes.
+ * Current implementation handles only the International System of Unit (SI),
+ * but this may be improved in future version.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class Prefixes {
+    /**
+     * The SI “deca” prefix. This is the only SI prefix encoded on two letters instead
than one.
+     * It can be represented by the CJK compatibility character “㍲”, but use of those
characters
+     * is generally not recommended outside of Chinese, Japanese or Korean texts.
+     */
+    private static final String DECA = "da";
+
+    /**
+     * The SI prefixes in increasing order. The only two-letters prefix – “da” –
is encoded using the JCK compatibility
+     * character “㍲”. The Greek letter μ is repeated twice: the U+00B5 character for
micro sign (this is the character
+     * that Apache SIS uses in unit symbols) and the U+03BC character for the Greek small
letter “mu” (the later is the
+     * character that appears when decomposing JCK compatibility characters with {@link java.text.Normalizer}).
+     * Both characters have same appearance but different values.
+     *
+     * <p>For each prefix at index <var>i</var>, the multiplication factor
is given by 10 raised to power {@code POWERS[i]}.</p>
+     */
+    private static final char[] PREFIXES = {'E','G','M','P','T','Y','Z','a','c','d','f','h','k','m','n','p','y','z','µ','μ','㍲'};
+    private static final byte[] POWERS   = {18,  9,  6, 15, 12, 24, 21,-18, -2, -1,-15, 
2,  3, -3, -9,-12,-24,-21, -6, -6,  1};
+
+    /**
+     * 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)
+     */
+    private static final char[] ENUM = {'y','z','a','f','p','n','µ','m','c','d','㍲','h','k','M','G','T','P','E','Z','Y'};
+
+    /**
+     * The maximal power of 1000 for the prefixes in the {@link #ENUM} array. Note that 1000⁸
= 1E+24.
+     */
+    static final int MAX_POWER = 8;
+
+    /**
+     * The converters for SI prefixes, created when first needed.
+     *
+     * @see #converter(char)
+     */
+    private static final LinearConverter[] CONVERTERS = new LinearConverter[POWERS.length];
+
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private Prefixes() {
+    }
+
+    /**
+     * Returns the converter for the given SI prefix, or {@code null} if none.
+     * Those converters are created when first needed and cached for reuse.
+     */
+    static LinearConverter converter(final char prefix) {
+        final int i = Arrays.binarySearch(PREFIXES, prefix);
+        if (i < 0) {
+            return null;
+        }
+        synchronized (CONVERTERS) {
+            LinearConverter c = CONVERTERS[i];
+            if (c == null) {
+                final int p = POWERS[i];
+                final double numerator, denominator;
+                if (p >= 0) {
+                    numerator = MathFunctions.pow10(p);
+                    denominator = 1;
+                } else {
+                    numerator = 1;
+                    denominator = MathFunctions.pow10(-p);
+                }
+                c = LinearConverter.scale(numerator, denominator);
+                CONVERTERS[i] = c;
+            }
+            return c;
+        }
+    }
+
+    /**
+     * Returns the SI prefix symbol for the given scale factor, or 0 if none.
+     */
+    static char symbol(final double scale) {
+        final int n = Numerics.toExp10(Math.getExponent(scale)) + 1;
+        if (LinearConverter.epsilonEquals(MathFunctions.pow10(n), scale)) {
+            int i = Math.abs(n);
+            switch (i) {
+                case 0:  return 0;
+                case 1:  // Fallthrough
+                case 2:  break;
+                default: {
+                    if (i > (MAX_POWER*3) || (i % 3) != 0) {
+                        return 0;
+                    }
+                    i = i/3 + 2;
+                    break;
+                }
+            }
+            return ENUM[n >= 0 ? (MAX_POWER+1) + i : (MAX_POWER+2) - i];
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the concatenation of the given prefix with the given unit symbol.
+     */
+    static String concat(final char prefix, final String unit) {
+        return (prefix == '㍲') ? DECA + unit : prefix + unit;
+    }
+
+    /**
+     * 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)}.
+     *
+     * @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.
+     */
+    static char twoLetters(final char prefix, final String uom) {
+        return (prefix == 'd' && uom.charAt(1) == 'a') ? '㍲' : prefix;
+    }
+
+    /**
+     * If the given system unit should be replaced by pseudo-unit for the purpose of formatting,
+     * returns that pseudo-unit. Otherwise returns {@code null}. This method is for handling
the
+     * Special case of {@link Units#KILOGRAM}, to be replaced by {@link 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". We do that by dividing the unit by 1000 (the converter for "milli"
prefix).
+     */
+    @SuppressWarnings("unchecked")
+    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.
+            } 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);
+                }
+            }
+        }
+        return null;
+    }
+}
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 16edc25..d186a3c 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
@@ -516,15 +516,14 @@ final class SystemUnit<Q extends Quantity<Q>> extends AbstractUnit<Q>
implements
      * @return the unit after the specified transformation.
      */
     @Override
-    @SuppressWarnings("unchecked")
     public Unit<Q> transform(UnitConverter operation) {
         ArgumentChecks.ensureNonNull("operation", operation);
         AbstractUnit<Q> base = this;
-        if ((scope & ~UnitRegistry.SI) == 0 && dimension.numeratorIs('M')) {
+        final ConventionalUnit<Q> pseudo = Prefixes.pseudoSystemUnit(this);
+        if (pseudo != null) {
             /*
              * 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".
-             * We do that by dividing the unit by 1000 (the converter for "milli" prefix).
              *
              * 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
@@ -532,19 +531,8 @@ final class SystemUnit<Q extends Quantity<Q>> extends AbstractUnit<Q>
implements
              * Unit.multiply(Unit) and Unit.divide(Unit) operations. However we may revisit
this policy
              * in a future version.
              */
-            if (this == Units.KILOGRAM) {
-                base = (AbstractUnit<Q>) Units.GRAM;                    // Optimization
for a common case.
-            } else {
-                String symbol = getSymbol();
-                if (symbol != null && symbol.length() >= 3 && symbol.startsWith("kg")
&& !isSymbolChar(symbol.codePointAt(2))) {
-                    symbol = symbol.substring(1);
-                    base = new ConventionalUnit<>(this, LinearConverter.forPrefix('m'),
-                            symbol, UnitRegistry.PREFIXABLE, (byte) 0).unique(symbol);
-                }
-            }
-            if (base != this) {
-                operation = operation.concatenate(LinearConverter.forPrefix('k'));
-            }
+            operation = operation.concatenate(pseudo.toTarget.inverse());
+            base = pseudo;
         }
         return ConventionalUnit.create(base, operation);
     }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java
index 45f159b..85f0218 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java
@@ -175,10 +175,13 @@ final class UnitDimension implements Dimension, Serializable {
     /**
      * Returns {@code true} if the numerator is the dimension identified by the given symbol.
      * This method returns {@code true} only if the numerator is not be raised to any exponent
-     * other than 1. All denominator terms are ignored.
+     * other than 1 and there is no other numerator. All denominator terms are ignored.
+     *
+     * <p>This method is used for identifying units like "kg", "kg/s", <i>etc</i>
for handling
+     * the "kg" prefix in a special way.</p>
      */
     final boolean numeratorIs(final char s) {
-        if (symbol == s) {
+        if (symbol == s) {                                  // Optimization for a simple
case.
             assert components.keySet().equals(Collections.singleton(this));
             return true;
         }
@@ -186,12 +189,12 @@ final class UnitDimension implements Dimension, Serializable {
         for (final Map.Entry<UnitDimension,Fraction> e : components.entrySet()) {
             final Fraction value = e.getValue();
             if (e.getKey().symbol == s) {
-                if (value.numerator != 1 || value.denominator != 1) {
-                    return false;
+                if (value.numerator != value.denominator) {
+                    return false;                           // Raised to a power different
than 1.
                 }
                 found = true;
             } else if (value.signum() >= 0) {
-                return false;
+                return false;                               // Found other numerators.
             }
         }
         return found;
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 ea77608..7ccf82a 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
@@ -68,10 +68,8 @@ import org.apache.sis.util.collection.WeakValueHashMap;
  * opposite sign. It is caller responsibility to handle the direction of axes associated
to netCDF units.
  *
  * <div class="section">Multi-threading</div>
- * {@code UnitFormat} is generally not thread-safe.
- * However if there is no call to any setter method or to {@link #label(Unit, String)} after
construction,
- * then the {@link #parse(CharSequence)} and {@link #format(Unit)} methods can be invoked
concurrently in
- * different threads.
+ * {@code UnitFormat} is generally not thread-safe. If units need to be parsed or formatted
in different threads,
+ * each thread should have its own {@code UnitFormat} instance.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
@@ -99,16 +97,9 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
     static final String UNITY = "unity";
 
     /**
-     * The SI “deca” prefix. This is the only SI prefix encoded on two letters instead
than one.
-     * It can be represented by the CJK compatibility character “㍲”, but use of those
characters
-     * is generally not recommended outside of Chinese, Japanese or Korean texts.
-     */
-    static final String DECA = "da";
-
-    /**
      * The default instance used by {@link Units#valueOf(String)} for parsing units of measurement.
      * While {@code UnitFormat} is generally not thread-safe, this particular instance is
safe if
-     * we never invoke any setter method.
+     * we never invoke any setter method and we do not format with {@link Style#NAME}.
      */
     static final UnitFormat INSTANCE = new UnitFormat();
 
@@ -464,6 +455,9 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
      * international and American spelling of unit names in addition of localized names.
      * The intent is to recognize "meter" as well as "metre".
      *
+     * <p>While we said that {@code UnitFormat} is not thread safe, we make an exception
for this method
+     * for allowing the singleton {@link #INSTANCE} to parse symbols in a multi-threads environment.</p>
+     *
      * @param  uom  the unit symbol, without leading or trailing spaces.
      */
     private Unit<?> fromName(String uom) {
@@ -598,13 +592,13 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
         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
+         * implemented by Apache SIS because  AbstractUnit.getName()  delegates to the same
resource bundle
+         * than the one used by this block. We are better to use the resource bundle of the
UnitFormat both
+         * for performance reasons and because the locale may not be the same.
+         */
         if (style == Style.NAME) {
-            /*
-             * Choice 2: value specified by Unit.getName(). We skip this check if the given
Unit is an instance
-             * implemented by Apache SIS because  AbstractUnit.getName()  delegates to the
same resource bundle
-             * than the one used by this block. We are better to use the resource bundle
of the UnitFormat both
-             * for performance reasons and because the locale may not be the same.
-             */
             if (!(unit instanceof AbstractUnit)) {
                 label = unit.getName();
                 if (label != null) {
@@ -629,6 +623,8 @@ public class UnitFormat extends Format implements javax.measure.format.UnitForma
         }
         /*
          * Choice 3: if the unit has a specific symbol, appends that symbol.
+         * 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) {
@@ -1434,20 +1430,14 @@ search:     while ((i = CharSequences.skipTrailingWhitespaces(symbols,
start, i)
         if (unit == null && uom.length() >= 2) {
             int s = 1;
             char prefix = uom.charAt(0);
-            if (prefix == 'd' && uom.charAt(1) == 'a') {
-                prefix = '㍲';
-                s = 2;
+            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 = LinearConverter.forPrefix(prefix);
+                final LinearConverter c = Prefixes.converter(prefix);
                 if (c != null) {
-                    String symbol = unit.getSymbol();
-                    if (prefix == '㍲') {
-                        symbol = DECA + symbol;
-                    } else {
-                        symbol = prefix + symbol;
-                    }
+                    final String symbol = Prefixes.concat(prefix, unit.getSymbol());
                     return new ConventionalUnit<>((AbstractUnit<?>) unit, c,
symbol.intern(), (byte) 0, (short) 0);
                 }
             }
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 372f877..596990d 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
@@ -1104,12 +1104,12 @@ public final class Units extends Static {
         /*
          * All SI prefix to be used below, with additional converters to be used more than
once.
          */
-        final LinearConverter nano  = LinearConverter.forPrefix('n');
-        final LinearConverter micro = LinearConverter.forPrefix('µ');
-        final LinearConverter milli = LinearConverter.forPrefix('m');
-        final LinearConverter centi = LinearConverter.forPrefix('c');
-        final LinearConverter hecto = LinearConverter.forPrefix('h');
-        final LinearConverter kilo  = LinearConverter.forPrefix('k');
+        final LinearConverter nano  = Prefixes.converter('n');
+        final LinearConverter micro = Prefixes.converter('µ');
+        final LinearConverter milli = Prefixes.converter('m');
+        final LinearConverter centi = Prefixes.converter('c');
+        final LinearConverter hecto = Prefixes.converter('h');
+        final LinearConverter kilo  = Prefixes.converter('k');
         final LinearConverter ten4  = LinearConverter.scale(10000, 1);
         /*
          * All Unit<Angle>.
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 1155e4f..815e0bd 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
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.measure;
 
-import java.lang.reflect.Field;
 import javax.measure.IncommensurableException;
 import javax.measure.Unit;
 import javax.measure.UnitConverter;
@@ -39,7 +38,7 @@ import static org.apache.sis.test.Assert.*;
  * @since   0.8
  * @module
  */
-@DependsOn({SystemUnitTest.class, LinearConverterTest.class})
+@DependsOn({SystemUnitTest.class, LinearConverterTest.class, PrefixesTest.class})
 public final strictfp class ConventionalUnitTest extends TestCase {
     /**
      * Verifies the properties if the given unit.
@@ -81,35 +80,6 @@ public final strictfp class ConventionalUnitTest extends TestCase {
     }
 
     /**
-     * Tests {@link ConventionalUnit#prefix(double)}.
-     */
-    @Test
-    public void testPrefix() {
-        assertEquals( 0 , ConventionalUnit.prefix(1E-27));
-        assertEquals( 0 , ConventionalUnit.prefix(1E-25));
-        assertEquals('y', ConventionalUnit.prefix(1E-24));
-        assertEquals( 0 , ConventionalUnit.prefix(1E-23));
-        assertEquals('n', ConventionalUnit.prefix(1E-09));
-        assertEquals( 0 , ConventionalUnit.prefix(1E-08));
-        assertEquals( 0 , ConventionalUnit.prefix(1E-04));
-        assertEquals('m', ConventionalUnit.prefix(1E-03));
-        assertEquals('c', ConventionalUnit.prefix(1E-02));
-        assertEquals('d', ConventionalUnit.prefix(1E-01));
-        assertEquals( 0 , ConventionalUnit.prefix(    1));
-        assertEquals( 0 , ConventionalUnit.prefix(    0));
-        assertEquals( 0 , ConventionalUnit.prefix(  -10));
-        assertEquals('㍲', ConventionalUnit.prefix(   10));
-        assertEquals('h', ConventionalUnit.prefix(  100));
-        assertEquals('k', ConventionalUnit.prefix( 1000));
-        assertEquals( 0 , ConventionalUnit.prefix(1E+04));
-        assertEquals('G', ConventionalUnit.prefix(1E+09));
-        assertEquals('Y', ConventionalUnit.prefix(1E+24));
-        assertEquals( 0 , ConventionalUnit.prefix(1E+25));
-        assertEquals( 0 , ConventionalUnit.prefix(1E+27));
-        assertEquals( 0 , ConventionalUnit.prefix(1E+25));
-    }
-
-    /**
      * Tests {@link ConventionalUnit#power(String)}.
      */
     @Test
@@ -126,45 +96,11 @@ public final strictfp class ConventionalUnitTest extends TestCase {
     }
 
     /**
-     * Ensures that the characters in the {@link ConventionalUnit#PREFIXES} array match
-     * the prefixes recognized by {@link LinearConverter#forPrefix(char)}.
-     *
-     * @throws ReflectiveOperationException if this test can not access the private fields
of {@link LinearConverter}.
-     *
-     * @see LinearConverterTest#verifyPrefixes()
-     */
-    @Test
-    @DependsOnMethod("testPrefix")
-    public void verifyPrefixes() throws ReflectiveOperationException {
-        Field f = ConventionalUnit.class.getDeclaredField("PREFIXES");
-        f.setAccessible(true);
-        double previousScale = StrictMath.pow(1000, -(ConventionalUnit.MAX_POWER + 1));
-        for (final char prefix : (char[]) f.get(null)) {
-            final LinearConverter lc = LinearConverter.forPrefix(prefix);
-            final String asString = String.valueOf(prefix);
-            assertNotNull(asString, lc);
-            /*
-             * Ratio of previous scale with current scale shall be a power of 10.
-             */
-            final double scale = lc.derivative(0);
-            final double power = StrictMath.log10(scale / previousScale);
-            assertTrue  (asString,    power >= 1);
-            assertEquals(asString, 0, power % 1, STRICT);
-            /*
-             * 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("ConventionalUnit.prefix(double)", asString, String.valueOf(ConventionalUnit.prefix(scale)));
-            previousScale = scale;
-        }
-    }
-
-    /**
      * Tests {@link SystemUnit#multiply(double)} and {@link SystemUnit#divide(double)}.
      * Both are implemented by calls to {@link SystemUnit#transform(UnitConverter)}.
      */
     @Test
-    @DependsOnMethod({"verifyPrefixes","testPower"})
+    @DependsOnMethod("testPower")
     public void testTransformSystemUnit() {
         assertSame(Units.METRE,      Units.METRE.multiply(   1));
         assertSame(Units.KILOMETRE,  Units.METRE.multiply(1000));
@@ -286,7 +222,6 @@ public final strictfp class ConventionalUnitTest extends TestCase {
      * @throws IncommensurableException if {@link Unit#getConverterToAny(Unit)} failed.
      */
     @Test
-    @DependsOnMethod("verifyPrefixes")
     public void testVolumeEquivalences() throws IncommensurableException {
         assertEquivalent(  "L", Units.LITRE.divide  (1E+00),  "dm³", Units.CUBIC_METRE.divide
 (1E+03));
         assertEquivalent( "mL", Units.LITRE.divide  (1E+03),  "cm³", Units.CUBIC_METRE.divide
 (1E+06));
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
b/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
index 66900fd..d25e8d5 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/LinearConverterTest.java
@@ -17,10 +17,8 @@
 package org.apache.sis.measure;
 
 import java.math.BigDecimal;
-import java.lang.reflect.Field;
 import javax.measure.UnitConverter;
 import org.apache.sis.math.Fraction;
-import org.apache.sis.util.ArraysExt;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.test.DependsOnMethod;
 import org.junit.Test;
@@ -32,7 +30,7 @@ import static org.apache.sis.test.Assert.*;
  * Tests the {@link LinearConverter} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -45,7 +43,7 @@ public final strictfp class LinearConverterTest extends TestCase {
      * @param  denominator  the expected denominator in the conversion factor.
      * @param  converter    the converter to verify.
      */
-    private static void assertScale(final int numerator, final int denominator, final LinearConverter
converter) {
+    static void assertScale(final int numerator, final int denominator, final LinearConverter
converter) {
         final double derivative = numerator / (double) denominator;
         final Number[] coefficients = converter.coefficients();
         assertEquals("coefficients.length", 2, coefficients.length);
@@ -61,39 +59,6 @@ public final strictfp class LinearConverterTest extends TestCase {
     }
 
     /**
-     * Ensures that the characters in the {@link LinearConverter#PREFIXES} array are in strictly
increasing order,
-     * and that {@link LinearConverter#POWERS} has the same length.
-     *
-     * @throws ReflectiveOperationException if this test can not access the private fields
of {@link LinearConverter}.
-     *
-     * @see ConventionalUnitTest#verifyPrefixes()
-     */
-    @Test
-    public void verifyPrefixes() throws ReflectiveOperationException {
-        Field f = LinearConverter.class.getDeclaredField("PREFIXES");
-        f.setAccessible(true);
-        final char[] prefixes = (char[]) f.get(null);
-        assertTrue(ArraysExt.isSorted(prefixes, true));
-
-        f = LinearConverter.class.getDeclaredField("POWERS");
-        f.setAccessible(true);
-        assertEquals("length", prefixes.length, ((byte[]) f.get(null)).length);
-    }
-
-    /**
-     * Tests the {@link LinearConverter#forPrefix(char)} method. This also indirectly tests
the
-     * {@link LinearConverter#scale(double, double)} and {@link LinearConverter#coefficients()}
-     * methods.
-     */
-    @Test
-    public void testForPrefix() {
-        assertScale(1000000,    1, LinearConverter.forPrefix('M'));
-        assertScale(   1000,    1, LinearConverter.forPrefix('k'));
-        assertScale(      1,  100, LinearConverter.forPrefix('c'));
-        assertScale(      1, 1000, LinearConverter.forPrefix('m'));
-    }
-
-    /**
      * Tests {@link LinearConverter#pow(UnitConverter, int, boolean)}.
      */
     @Test
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
new file mode 100644
index 0000000..2394e5e
--- /dev/null
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/PrefixesTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.measure;
+
+import java.lang.reflect.Field;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.util.ArraysExt;
+import org.junit.Test;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests the {@link Prefixes} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+@DependsOn(LinearConverterTest.class)
+public final strictfp class PrefixesTest extends TestCase {
+    /**
+     * Ensures that the characters in the {@link Prefixes#PREFIXES} array are in strictly
increasing order,
+     * and that {@link Prefixes#POWERS} has the same length.
+     * Those two arrays form a map used by {@link Prefixes#converter(char)}.
+     *
+     * @throws ReflectiveOperationException if this test can not access the private fields
of {@link LinearConverter}.
+     */
+    @Test
+    public void verifyConverterMap() throws ReflectiveOperationException {
+        Field f = Prefixes.class.getDeclaredField("PREFIXES");
+        f.setAccessible(true);
+        final char[] prefixes = (char[]) f.get(null);
+        assertTrue(ArraysExt.isSorted(prefixes, true));
+
+        f = Prefixes.class.getDeclaredField("POWERS");
+        f.setAccessible(true);
+        assertEquals("length", prefixes.length, ((byte[]) f.get(null)).length);
+    }
+
+    /**
+     * Ensures that the characters in the {@link Prefixes#ENUM} array match
+     * the prefixes recognized by {@link Prefixes#converter(char)}.
+     * This array forms a list used by {@link Prefixes#converter(char)}.
+     *
+     * @throws ReflectiveOperationException if this test can not access the private fields
of {@link LinearConverter}.
+     */
+    @Test
+    @DependsOnMethod("testConverter")
+    public void verifySymbolList() throws ReflectiveOperationException {
+        Field f = Prefixes.class.getDeclaredField("ENUM");
+        f.setAccessible(true);
+        double previousScale = StrictMath.pow(1000, -(Prefixes.MAX_POWER + 1));
+        for (final char prefix : (char[]) f.get(null)) {
+            final LinearConverter lc = Prefixes.converter(prefix);
+            final String asString = String.valueOf(prefix);
+            assertNotNull(asString, lc);
+            /*
+             * Ratio of previous scale with current scale shall be a power of 10.
+             */
+            final double scale = lc.derivative(0);
+            final double power = StrictMath.log10(scale / previousScale);
+            assertTrue  (asString,    power >= 1);
+            assertEquals(asString, 0, power % 1, STRICT);
+            /*
+             * 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)));
+            previousScale = scale;
+        }
+    }
+
+    /**
+     * Tests the {@link Prefixes#converter(char)} method. This also indirectly tests the
+     * {@link LinearConverter#scale(double, double)} and {@link LinearConverter#coefficients()}
+     * methods.
+     */
+    @Test
+    public void testConverter() {
+        LinearConverterTest.assertScale(1000000,    1, Prefixes.converter('M'));
+        LinearConverterTest.assertScale(   1000,    1, Prefixes.converter('k'));
+        LinearConverterTest.assertScale(      1,  100, Prefixes.converter('c'));
+        LinearConverterTest.assertScale(      1, 1000, Prefixes.converter('m'));
+    }
+
+    /**
+     * Tests {@link Prefixes#symbol(double)}.
+     */
+    @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));
+    }
+}
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
index b07af5e..f2086a9 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
@@ -96,6 +96,7 @@ import org.junit.BeforeClass;
     org.apache.sis.measure.LinearConverterTest.class,
     org.apache.sis.measure.UnitDimensionTest.class,
     org.apache.sis.measure.SystemUnitTest.class,
+    org.apache.sis.measure.PrefixesTest.class,
     org.apache.sis.measure.ConventionalUnitTest.class,
     org.apache.sis.measure.UnitFormatTest.class,
     org.apache.sis.measure.UnitsTest.class,


Mime
View raw message