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 08239e9570 Fix the extensive tests with EPSG 9.9.1. This commit does 
not include the fixes for EPSG 12.
08239e9570 is described below

commit 08239e9570e059bced5c2c85ab26dbc435e2a252
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Aug 21 18:31:39 2025 +0200

    Fix the extensive tests with EPSG 9.9.1.
    This commit does not include the fixes for EPSG 12.
---
 .../main/org/apache/sis/io/wkt/Formatter.java      |   2 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    | 125 ++++++++++-----------
 .../sis/referencing/crs/DefaultGeodeticCRS.java    |   2 +-
 .../referencing/datum/DefaultVerticalDatum.java    |  22 +++-
 .../sis/referencing/factory/sql/SQLTranslator.java |   4 +-
 .../referencing/internal/VerticalDatumTypes.java   |  11 +-
 .../operation/AbstractCoordinateOperation.java     |   4 +-
 .../sis/referencing/privy/AxisDirections.java      |   4 +-
 .../factory/GeodeticObjectFactoryTest.java         |   3 +-
 .../sis/test/integration/ConsistencyTest.java      |  11 +-
 .../main/org/apache/sis/util/CharSequences.java    |  66 +++++++----
 .../org/apache/sis/util/CharSequencesTest.java     |   5 +-
 12 files changed, 149 insertions(+), 110 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
index 513ac1491a..345354868a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
@@ -1714,7 +1714,7 @@ public class Formatter implements Localized {
     }
 
     /**
-     * Returns the enclosing WKT element, or {@code null} if element being 
formatted is the root.
+     * Returns the enclosing <abbr>WKT</abbr> element, or {@code null} if the 
element being formatted is the root.
      * This method can be invoked by child elements having some aspects that 
depend on the enclosing element.
      *
      * @param  depth  1 for the immediate parent, 2 for the parent of the 
parent, <i>etc.</i>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 796b7abe65..da10560a59 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -683,13 +683,16 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
      * @param  dimension    the minimal number of dimensions. Can be 1 if 
unknown.
      * @param  isWKT1       {@code true} if the parent element is an element 
from the WKT 1 standard.
      * @param  defaultUnit  the contextual unit (usually {@code Units.METRE} 
or {@code Units.RADIAN}), or {@code null} if unknown.
-     * @param  datum        the datum of the enclosing CRS, or {@code null} if 
unknown.
+     * @param  geodetic     whether the datum of the enclosing 
<abbr>CRS</abbr> is geodetic.
+     * @param  vertical     the realization method (formally known as vertical 
datum type), or {@code null} if unknown or not applicable.
      * @return the {@code "CS"}, {@code "UNIT"} and/or {@code "AXIS"} elements 
as a Coordinate System, or {@code null}.
      * @throws ParseException if an element cannot be parsed.
      * @throws FactoryException if the factory cannot create the coordinate 
system.
      */
     private CoordinateSystem parseCoordinateSystem(final Element parent, 
String type, int dimension,
-            final boolean isWKT1, final Unit<?> defaultUnit, final Datum 
datum) throws ParseException, FactoryException
+                                                   final boolean isWKT1, final 
Unit<?> defaultUnit,
+                                                   final boolean geodetic, 
final RealizationMethod vertical)
+            throws ParseException, FactoryException
     {
         axisOrder.clear();
         final boolean is3D = (dimension >= 3);
@@ -774,7 +777,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
                  * are for two- or three-dimensional Projected or 
three-dimensional Geocentric CRS.
                  */
                 case WKTKeywords.Cartesian: {
-                    if (datum != null && !(datum instanceof GeodeticDatum)) {
+                    if (!geodetic) {
                         throw parent.missingComponent(WKTKeywords.Axis);
                     }
                     if (defaultUnit == null) {
@@ -829,20 +832,17 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
                     z         = "h";
                     nz        = "Height";
                     direction = AxisDirection.UP;
-                    if (datum instanceof VerticalDatum) {
-                        final RealizationMethod vt = ((VerticalDatum) 
datum).getRealizationMethod().orElse(null);
-                        if (vt == RealizationMethod.GEOID) {
-                            nz = AxisNames.GRAVITY_RELATED_HEIGHT;
-                            z  = "H";
-                        } else if (vt == RealizationMethod.TIDAL) {
-                            direction = AxisDirection.DOWN;
-                            nz = AxisNames.DEPTH;
-                            z  = "D";
-                        } else if (VerticalDatumTypes.ellipsoidal(vt)) {
-                            // Not allowed by ISO 19111 as a standalone axis, 
but SIS is
-                            // tolerant to this case since it is sometimes 
hard to avoid.
-                            nz = AxisNames.ELLIPSOIDAL_HEIGHT;
-                        }
+                    if (vertical == RealizationMethod.GEOID) {
+                        nz = AxisNames.GRAVITY_RELATED_HEIGHT;
+                        z  = "H";
+                    } else if (vertical == RealizationMethod.TIDAL) {
+                        direction = AxisDirection.DOWN;
+                        nz = AxisNames.DEPTH;
+                        z  = "D";
+                    } else if (VerticalDatumTypes.ellipsoidal(vertical)) {
+                        // Not allowed by ISO 19111 as a standalone axis, but 
SIS is
+                        // tolerant to this case since it is sometimes hard to 
avoid.
+                        nz = AxisNames.ELLIPSOIDAL_HEIGHT;
                     }
                     break;
                 }
@@ -894,7 +894,8 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             if (type != null && !type.isEmpty()) {
                 final int c = type.codePointAt(0);
                 buffer.appendCodePoint(Character.toUpperCase(c))
-                        .append(type, Character.charCount(c), 
type.length()).append(' ');
+                      .append(type, Character.charCount(c), type.length())
+                      .append(' ');
             }
             name = AxisDirections.appendTo(buffer.append("CS"), axes);
         }
@@ -1643,7 +1644,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
         final CRSFactory crsFactory = factories.getCRSFactory();
         try {
-            final CoordinateSystem cs = parseCoordinateSystem(element, null, 
1, isWKT1, unit, datum);
+            final CoordinateSystem cs = parseCoordinateSystem(element, null, 
1, isWKT1, unit, false, null);
             final Map<String,Object> properties = 
parseMetadataAndClose(element, name, datum);
             if (baseCRS != null) {
                 properties.put(Legacy.DERIVED_TYPE_KEY, EngineeringCRS.class);
@@ -1675,7 +1676,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         final Unit<?> unit  = parseUnit(element);
         final CoordinateSystem cs;
         try {
-            cs = parseCoordinateSystem(element, WKTKeywords.Cartesian, 2, 
false, unit, datum);
+            cs = parseCoordinateSystem(element, WKTKeywords.Cartesian, 2, 
false, unit, false, null);
             final Map<String,?> properties = parseMetadataAndClose(element, 
name, datum);
             if (cs instanceof AffineCS) {
                 return new DefaultImageCRS(properties, datum, (AffineCS) cs);
@@ -1824,7 +1825,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         final CRSFactory crsFactory = factories.getCRSFactory();
         final CoordinateSystem cs;
         try {
-            cs = parseCoordinateSystem(element, csType, dimension, isWKT1, 
csUnit, null);
+            cs = parseCoordinateSystem(element, csType, dimension, isWKT1, 
csUnit, true, null);
             if (baseCRS != null) {
                 final Map<String,?> properties = 
parseMetadataAndClose(element, name, null);
                 return crsFactory.createDerivedCRS(properties, baseCRS, 
fromBase, cs);
@@ -1924,7 +1925,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
         final CoordinateSystem cs;
         try {
-            cs = parseCoordinateSystem(element, WKTKeywords.vertical, 1, 
isWKT1, unit, datum);
+            cs = parseCoordinateSystem(element, WKTKeywords.vertical, 1, 
isWKT1, unit, false, datum.getRealizationMethod().orElse(null));
             final Map<String,?> properties = parseMetadataAndClose(element, 
name, datum);
             if (cs instanceof VerticalCS) {
                 final CRSFactory crsFactory = factories.getCRSFactory();
@@ -2005,7 +2006,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
         final CoordinateSystem cs;
         try {
-            cs = parseCoordinateSystem(element, WKTKeywords.temporal, 1, 
false, unit, datum);
+            cs = parseCoordinateSystem(element, WKTKeywords.temporal, 1, 
false, unit, false, null);
             final Map<String,?> properties = parseMetadataAndClose(element, 
name, datum);
             if (cs instanceof TimeCS) {
                 final CRSFactory crsFactory = factories.getCRSFactory();
@@ -2067,7 +2068,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
         final CoordinateSystem cs;
         try {
-            cs = parseCoordinateSystem(element, WKTKeywords.parametric, 1, 
false, unit, datum);
+            cs = parseCoordinateSystem(element, WKTKeywords.parametric, 1, 
false, unit, false, null);
             final Map<String,?> properties = parseMetadataAndClose(element, 
name, datum);
             if (cs instanceof ParametricCS) {
                 final CRSFactory crsFactory = factories.getCRSFactory();
@@ -2111,55 +2112,49 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         if (element == null) {
             return null;
         }
-        final boolean   isWKT1 = element.getKeywordIndex() == 2;               
 // Index of "ProjCS" above.
-        final String    name   = element.pullString("name");
-        final SingleCRS geoCRS = parseGeodeticCRS(MANDATORY, element, 2, 
WKTKeywords.ellipsoidal);
-        if (!(geoCRS instanceof GeodeticCRS)) {
-            throw new UnparsableObjectException(errorLocale, 
Errors.Keys.IllegalCRSType_1,
-                    new Object[] {geoCRS.getClass()}, element.offset);
-        }
-        /*
-         * Parse the projection parameters. If a default linear unit is 
specified, it will apply to
-         * all parameters that do not specify explicitly a LengthUnit. If no 
such crs-wide unit was
-         * specified, then the default will be degrees.
-         *
-         * More specifically §9.3.4 in the specification said about the 
default units:
-         *
-         *    - lengths shall be given in the unit for the projected CRS axes.
-         *    - angles shall be given in the unit for the base geographic CRS 
of the projected CRS.
-         */
-        Unit<Length> csUnit = parseScaledUnit(element, WKTKeywords.LengthUnit, 
Units.METRE);
-        final Unit<Length> linearUnit;
-        final Unit<Angle>  angularUnit;
-        if (isWKT1 && usesCommonUnits) {
-            linearUnit  = Units.METRE;
-            angularUnit = Units.DEGREE;
-        } else {
-            linearUnit  = csUnit;
-            angularUnit = 
AxisDirections.getAngularUnit(geoCRS.getCoordinateSystem(), Units.DEGREE);
-        }
-        final Conversion conversion = parseDerivingConversion(MANDATORY, 
element,
-                isWKT1 ? null : WKTKeywords.Conversion, linearUnit, 
angularUnit);
-        /*
-         * Parse the coordinate system. The linear unit must be specified 
somewhere, either explicitly in each axis
-         * or for the whole CRS with the above `csUnit` value. If `csUnit` is 
null, then an exception will be thrown
-         * with a message like "A LengthUnit component is missing in 
ProjectedCRS".
-         *
-         * However, we make an exception if we are parsing a BaseProjCRS, 
since the coordinate system is unspecified
-         * in the WKT of base CRS. In this case only, we will default to metre.
-         */
+        final boolean isWKT1 = element.getKeywordIndex() == 2;                 
 // Index of "ProjCS" above.
+        final String  name   = element.pullString("name");
+        Unit<Length>  csUnit = parseScaledUnit(element, 
WKTKeywords.LengthUnit, Units.METRE);
         if (csUnit == null && isBaseCRS) {
             csUnit = Units.METRE;
+            /*
+             * Except when parsing a BaseProjCRS, the linear unit must be 
specified somewhere,
+             * either explicitly in each axis or for the whole CRS with the 
`csUnit` value.
+             * If `csUnit` is null, an exception will be thrown by 
`parseCoordinateSystem(…)`
+             * with a message like "A LengthUnit component is missing in 
ProjectedCRS".
+             */
         }
         final CoordinateSystem cs;
         try {
-            cs = parseCoordinateSystem(element, WKTKeywords.Cartesian, 2, 
isWKT1, csUnit, geoCRS.getDatum());
-            final Map<String,?> properties = parseMetadataAndClose(element, 
name, conversion);
+            cs = parseCoordinateSystem(element, WKTKeywords.Cartesian, 2, 
isWKT1, csUnit, true, null);
             if (cs instanceof CartesianCS) {
+                final SingleCRS geoCRS = parseGeodeticCRS(MANDATORY, element, 
cs.getDimension(), WKTKeywords.ellipsoidal);
+                if (!(geoCRS instanceof GeodeticCRS)) {
+                    throw new UnparsableObjectException(errorLocale, 
Errors.Keys.IllegalCRSType_1,
+                            new Object[] {geoCRS.getClass()}, element.offset);
+                }
                 /*
-                 * TODO: if the CartesianCS is three-dimensional, we need to 
ensure that the base CRS is also
-                 * three-dimensional. We could do that by parsing the CS 
before to invoke `parseGeodeticCRS`.
+                 * Parse the projection parameters. If a default linear unit 
is specified, it will apply to
+                 * all parameters that do not specify explicitly a LengthUnit. 
If no such crs-wide unit was
+                 * specified, then the default will be degrees.
+                 *
+                 * More specifically §9.3.4 in the specification said about 
the default units:
+                 *
+                 *    - lengths shall be given in the unit for the projected 
CRS axes.
+                 *    - angles shall be given in the unit for the base 
geographic CRS of the projected CRS.
                  */
+                final Unit<Length> linearUnit;
+                final Unit<Angle>  angularUnit;
+                if (isWKT1 && usesCommonUnits) {
+                    linearUnit  = Units.METRE;
+                    angularUnit = Units.DEGREE;
+                } else {
+                    linearUnit  = csUnit;
+                    angularUnit = 
AxisDirections.getAngularUnit(geoCRS.getCoordinateSystem(), Units.DEGREE);
+                }
+                final Conversion conversion = 
parseDerivingConversion(MANDATORY, element,
+                        isWKT1 ? null : WKTKeywords.Conversion, linearUnit, 
angularUnit);
+                final Map<String,?> properties = 
parseMetadataAndClose(element, name, conversion);
                 final CRSFactory crsFactory = factories.getCRSFactory();
                 return crsFactory.createProjectedCRS(properties, (GeodeticCRS) 
geoCRS, conversion, (CartesianCS) cs);
             }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
index e1ad9ffe9b..7e695e2db6 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
@@ -253,7 +253,7 @@ class DefaultGeodeticCRS extends 
AbstractSingleCRS<GeodeticDatum> implements Geo
         /*
          * Format the coordinate system, except if this CRS is the base CRS of 
an AbstractDerivedCRS in WKT 2 format.
          * This is because ISO 19162 omits the coordinate system definition of 
enclosed base CRS in order to simplify
-         * the WKT. The 'formatCS(…)' method may write axis unit before or 
after the axes depending on whether we are
+         * the WKT. The `formatCS(…)` method may write axis unit before or 
after the axes depending on whether we are
          * formatting WKT version 1 or 2 respectively.
          *
          * Note that even if we do not format the CS, we may still write the 
units if we are formatting in "simplified"
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
index 0d27307af2..58fa153b79 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
@@ -25,7 +25,10 @@ import jakarta.xml.bind.annotation.XmlRootElement;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.opengis.referencing.datum.VerticalDatum;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.privy.LegacyNamespaces;
@@ -215,6 +218,21 @@ public class DefaultVerticalDatum extends AbstractDatum 
implements VerticalDatum
         return Optional.ofNullable(method);
     }
 
+    /**
+     * Returns the realization method if it was explicitly specified, or 
otherwise tries to guess it.
+     * This method may return {@code null} if it cannot guess the method. This 
is used for compatibility
+     * with legacy formats such as WKT 1 or GML 3.1, before realization method 
became a formal property.
+     */
+    private RealizationMethod getOrGuessMethod(final FormattableObject parent) 
{
+        return getRealizationMethod().orElseGet(() -> {
+            CoordinateSystemAxis axis = null;
+            if (parent instanceof CoordinateReferenceSystem) {
+                axis = ((CoordinateReferenceSystem) 
parent).getCoordinateSystem().getAxis(0);
+            }
+            return VerticalDatumTypes.fromDatum(getName().getCode(), 
getAlias(), axis);
+        });
+    }
+
     /**
      * A vertical reference frame in which some of the defining parameters 
have time dependency.
      * The parameter values are valid at the time given by the
@@ -368,7 +386,7 @@ public class DefaultVerticalDatum extends AbstractDatum 
implements VerticalDatum
     protected String formatTo(final Formatter formatter) {
         super.formatTo(formatter);
         if (formatter.getConvention().majorVersion() == 1) {
-            formatter.append(VerticalDatumTypes.toLegacyCode(method));
+            
formatter.append(VerticalDatumTypes.toLegacyCode(getOrGuessMethod(formatter.getEnclosingElement(1))));
             return WKTKeywords.Vert_Datum;
         }
         return formatter.shortOrLong(WKTKeywords.VDatum, 
WKTKeywords.VerticalDatum);
@@ -410,7 +428,7 @@ public class DefaultVerticalDatum extends AbstractDatum 
implements VerticalDatum
         if (Context.isGMLVersion(Context.current(), 
LegacyNamespaces.VERSION_3_2)) {
             return null;
         }
-        return VerticalDatumTypes.toLegacyName(method);
+        return VerticalDatumTypes.toLegacyName(getOrGuessMethod(null));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
index 9228a36e80..1ec4515c09 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
@@ -475,10 +475,12 @@ check:  for (;;) {
                     /*
                      * Detect if the tables use enumeration (on PostgreSQL 
database) instead of VARCHAR.
                      * Enumerations appear in various tables, including in a 
WHERE clause for the Alias table.
+                     * Note: we cannot rely on 
`result.getInt(Reflection.DATA_TYPE)` because the declared type
+                     * of enumeration is `Types.VARCHAR` (at least on 
PostgresSQL)`.
                      */
                     if (ENUMERATION_COLUMN.equals(column)) {
                         String type = result.getString(Reflection.TYPE_NAME);
-                        if (!CharSequences.startsWith(type, "VARCHAR", true)) {
+                        if (!(CharSequences.startsWith(type, "VARCHAR", true) 
|| CharSequences.startsWith(type, "CHARACTER", true))) {
                             if (!type.contains(identifierQuote)) {
                                 type = identifierQuote + type + 
identifierQuote;
                             }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/VerticalDatumTypes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/VerticalDatumTypes.java
index 6f7c9b7c7b..91a2b9111b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/VerticalDatumTypes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/VerticalDatumTypes.java
@@ -246,6 +246,7 @@ public final class VerticalDatumTypes {
                     switch (abbreviation.charAt(0)) {
                         case 'h': method = ellipsoidal(); break;
                         case 'H': method = RealizationMethod.GEOID; break;
+                        case 'd': // Fall through
                         case 'D': method = RealizationMethod.TIDAL; dir = 
AxisDirection.DOWN; break;
                         default:  return null;
                     }
@@ -272,9 +273,13 @@ public final class VerticalDatumTypes {
             if (CharSequences.equalsFiltered("Mean Sea Level", name, 
Characters.Filter.LETTERS_AND_DIGITS, true)) {
                 return RealizationMethod.TIDAL;
             }
-            if (name.regionMatches(true, 0, "geoid", 0, 5)) {
-                return RealizationMethod.GEOID;
-            }
+            int i = 0;
+            do {
+                if (name.regionMatches(true, i, "geoid", 0, 5)) {
+                    return RealizationMethod.GEOID;
+                }
+                i = name.indexOf(' ', i) + 1;
+            } while (i > 0);
         }
         return null;
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 463ab03524..3619628b83 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -830,7 +830,7 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
     public boolean equals(final Object object, ComparisonMode mode) {
         if (super.equals(object, mode)) {
             if (mode == ComparisonMode.STRICT) {
-                final AbstractCoordinateOperation that = 
(AbstractCoordinateOperation) object;
+                final var that = (AbstractCoordinateOperation) object;
                 if (Objects.equals(sourceCRS,                   
that.sourceCRS)        &&
                     Objects.equals(interpolationCRS,            
that.interpolationCRS) &&
                     Objects.equals(transform,                   
that.transform)        &&
@@ -854,7 +854,7 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
                  *   - Scope, domain and accuracy properties only if NOT in 
"ignore metadata" mode.
                  *   - Interpolation CRS in all cases (regardless if ignoring 
metadata or not).
                  */
-                final CoordinateOperation that = (CoordinateOperation) object;
+                final var that = (CoordinateOperation) object;
                 if ((mode.isIgnoringMetadata() ||
                     (deepEquals(getCoordinateOperationAccuracy(), 
that.getCoordinateOperationAccuracy(), mode))) &&
                      deepEquals(getInterpolationCRS(),            
that.getInterpolationCRS(), mode))
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
index 178de792bb..d25a371428 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
@@ -567,9 +567,7 @@ next:       for (int i=0; i <= limit; i++) {
     public static AxisDirection find(final String name, final AxisDirection[] 
directions) {
         for (final AxisDirection candidate : directions) {
             final String identifier = candidate.name();
-            if (equalsFiltered(name, identifier, 
Characters.Filter.LETTERS_AND_DIGITS, true)
-                    || isAcronymForWords(name, identifier))
-            {
+            if (equalsFiltered(name, identifier, 
Characters.Filter.LETTERS_AND_DIGITS, true) || isAcronymForWords(name, 
identifier)) {
                 return candidate;
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/GeodeticObjectFactoryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/GeodeticObjectFactoryTest.java
index 762cad6ece..6209578b96 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/GeodeticObjectFactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/GeodeticObjectFactoryTest.java
@@ -113,7 +113,8 @@ public final class GeodeticObjectFactoryTest extends 
ObjectFactoryTest {
                 "    PARAMETER[“Central parallel”, 41.75]],\n" +       // 
Wrong parameter.
                 "  CS[Cartesian, 2],\n" +
                 "    AXIS[“(Y)”, north],\n" +
-                "    AXIS[“(X)”, east]]"),
+                "    AXIS[“(X)”, east],\n" +
+                "  UNIT[“metre”, 1]]"),
                 "Should not have parsed a WKT with wrong projection 
parameter.");
         assertMessageContains(e, "Central parallel");
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/ConsistencyTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/ConsistencyTest.java
index 714b681bb9..64d3f4ff07 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/ConsistencyTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/ConsistencyTest.java
@@ -65,6 +65,7 @@ public final class ConsistencyTest extends TestCase {
     private static final Set<String> EXCLUDES = Set.of(
         "CRS:1",            // Computer display: WKT parser alters the (i,j) 
axis names.
         "EPSG:5819",        // EPSG topocentric example A: DerivedCRS wrongly 
handled as a ProjectedCRS. See SIS-518.
+        "EPSG:5820",        // EPSG topocentric example B.
         "AUTO2:42001",      // This projection requires parameters, but we 
provide none.
         "AUTO2:42002",      // This projection requires parameters, but we 
provide none.
         "AUTO2:42003",      // This projection requires parameters, but we 
provide none.
@@ -126,6 +127,9 @@ public final class ConsistencyTest extends TestCase {
                 } catch (UnavailableFactoryException | 
NoSuchIdentifierException | FactoryDataException e) {
                     print(code, "WARNING", e.getLocalizedMessage());
                     continue;
+                } catch (FactoryException e) {
+                    fail("Cannot create CRS for \"" + code + "\".", e);
+                    continue;
                 }
                 lookup(parseAndFormat(v2,  code, crs), crs);
                 lookup(parseAndFormat(v2s, code, crs), crs);
@@ -170,9 +174,7 @@ public final class ConsistencyTest extends TestCase {
      * @param  crs   the CRS to test.
      * @return the parsed CRS.
      */
-    private CoordinateReferenceSystem parseAndFormat(final WKTFormat f,
-            final String code, final CoordinateReferenceSystem crs)
-    {
+    private CoordinateReferenceSystem parseAndFormat(final WKTFormat f, final 
String code, final CoordinateReferenceSystem crs) {
         String wkt = f.format(crs);
         final Warnings warnings = f.getWarnings();
         if (warnings != null && !warnings.getExceptions().isEmpty()) {
@@ -185,8 +187,7 @@ public final class ConsistencyTest extends TestCase {
             print(code, "ERROR", "Cannot parse the WKT below.");
             out.println(wkt);
             out.println();
-            e.printStackTrace(out);
-            fail(e.getLocalizedMessage());
+            fail(e);
             return null;
         }
         final String again = f.format(parsed);
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/CharSequences.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/CharSequences.java
index c68e32c10d..61e12993ec 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/CharSequences.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/CharSequences.java
@@ -643,7 +643,7 @@ search:     for (; fromIndex <= toIndex; fromIndex++) {
             }
             return split;
         }
-        // 'excludeEmpty' must use the same criterion as trimWhitespaces(…).
+        // `excludeEmpty` must use the same criterion as `trimWhitespaces(…)`.
         final boolean excludeEmpty = isWhitespace(separator);
         CharSequence[] split = createSplitArray(text);
         final int length = text.length();
@@ -1130,8 +1130,8 @@ search:     for (; fromIndex <= toIndex; fromIndex++) {
             if (toRemove > 0) {
                 toRemove += 5;          // Space needed for the " (…) " string.
                 /*
-                 * We will remove characters from 'lower' to 'upper' both 
exclusive. We try to
-                 * adjust 'lower' and 'upper' in such a way that the first and 
last characters
+                 * We will remove characters from `lower` to `upper` both 
exclusive. We try to
+                 * adjust `lower` and `upper` in such a way that the first and 
last characters
                  * to be removed will be spaces or punctuation characters.
                  */
                 int lower = length >>> 1;
@@ -1316,7 +1316,7 @@ searchWordBreak:    while (true) {
             return null;
         }
         /*
-         * Implementation note: the 'camelCaseToSentence' method needs
+         * Implementation note: the `camelCaseToSentence` method needs
          * this method to unconditionally returns a new StringBuilder.
          */
         final int length = identifier.length();
@@ -1424,11 +1424,15 @@ searchWordBreak:    while (true) {
     /**
      * Returns {@code true} if the first string is likely to be an acronym of 
the second string.
      * An acronym is a sequence of {@linkplain Character#isLetterOrDigit(int) 
letters or digits}
-     * built from at least one character of each word in the {@code words} 
string. More than
-     * one character from the same word may appear in the acronym, but they 
must always
-     * be the first consecutive characters. The comparison is case-insensitive.
+     * built from at least one character of each word in the {@code words} 
string.
+     * More than one character from the same word may appear in the acronym,
+     * but they must always be the first consecutive characters.
+     * The comparison is case-insensitive.
      * If any of the given arguments is {@code null}, this method returns 
{@code false}.
      *
+     * <p>If a word contains digits, than the digits shall either be all 
absent or all present in the acronym.
+     * An acronym with only the first digits is not considered as a match 
because it changes the numerical value.</p>
+     *
      * <h4>Example</h4>
      * Given the {@code "Open Geospatial Consortium"} words, the following 
strings are recognized as acronyms:
      * {@code "OGC"}, {@code "ogc"}, {@code "O.G.C."}, {@code "OpGeoCon"}.
@@ -1459,9 +1463,10 @@ searchWordBreak:    while (true) {
         }
 cmp:    while (ia < lga) {
             if (ic >= lgc) {
-                // There is more letters in the acronym than in the complete 
name.
+                // There is more letters in the acronym than in the remaining 
part of the complete name.
                 return false;
             }
+            final int last = ca;
             ca = codePointAt(acronym, ia); ia += charCount(ca);
             cc = codePointAt(words,   ic); ic += charCount(cc);
             if (isLetterOrDigit(ca)) {
@@ -1470,14 +1475,20 @@ cmp:    while (ia < lga) {
                     // Continue the comparison with next letter of both 
strings.
                     continue;
                 }
-                // Will search for the next word after the 'else' block.
-            } else do {
-                if (ia >= lga) break cmp;
-                ca = codePointAt(acronym, ia);
-                ia += charCount(ca);
-            } while (!isLetterOrDigit(ca));
+                // Will search for the next word after the `else` block.
+            } else {
+                if (isDigit(cc) && isDigit(last)) {
+                    // Acronym contains a truncated number.
+                    return false;
+                }
+                do {
+                    if (ia >= lga) break cmp;
+                    ca = codePointAt(acronym, ia);
+                    ia += charCount(ca);
+                } while (!isLetterOrDigit(ca));
+            }
             /*
-             * At this point, 'ca' is the next acronym letter to compare and we
+             * At this point, `ca` is the next acronym letter to compare and we
              * need to search for the next word in the complete name. We first
              * skip remaining letters, then we skip non-letter characters.
              */
@@ -1499,15 +1510,22 @@ cmp:    while (ia < lga) {
          * any additional word. We can only finish the current word and skip 
trailing non-
          * letter characters.
          */
+        boolean disallowDigit = isDigit(ca);
         boolean skipLetters = true;
-        do {
-            do {
-                if (ic >= lgc) return true;
-                cc = codePointAt(words, ic);
-                ic += charCount(cc);
-            } while (isLetterOrDigit(cc) == skipLetters);
-        } while ((skipLetters = !skipLetters) == false);
-        return false;
+        while (ic < lgc) {
+            cc = codePointAt(words, ic);
+            ic += charCount(cc);
+            if (skipLetters) {
+                if (disallowDigit) {
+                    if (isDigit(cc)) return false;
+                    disallowDigit = false;
+                }
+                skipLetters = isLetterOrDigit(cc);
+            } else if (isLetterOrDigit(cc)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -2166,7 +2184,7 @@ cmp:    while (ia < lga) {
         int upper = fromIndex;
         /*
          * Skip whitespaces. At the end of this loop,
-         * 'c' will be the first non-blank character.
+         * `c` will be the first non-blank character.
          */
         int c;
         do {
diff --git 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/CharSequencesTest.java
 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/CharSequencesTest.java
index 6fb5cd1ad8..1dd201c5eb 100644
--- 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/CharSequencesTest.java
+++ 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/CharSequencesTest.java
@@ -324,7 +324,7 @@ public final class CharSequencesTest extends TestCase {
     @Test
     public void testIsAcronymForWords() {
         /*
-         * Following shall be accepted as acronyms...
+         * Following shall be accepted as acronyms.
          */
         assertTrue(isAcronymForWords("OGC",                        "Open 
Geospatial Consortium"));
         assertTrue(isAcronymForWords("O.G.C.",                     "Open 
Geospatial Consortium"));
@@ -334,7 +334,7 @@ public final class CharSequencesTest extends TestCase {
         assertTrue(isAcronymForWords("E",                          "EAST"));
         assertTrue(isAcronymForWords("ENE",                        
"EAST_NORTH_EAST"));
         /*
-         * Following shall be rejected...
+         * Following shall be rejected.
          */
         assertFalse(isAcronymForWords("ORC",    "Open Geospatial Consortium"));
         assertFalse(isAcronymForWords("O.C.G.", "Open Geospatial Consortium"));
@@ -382,6 +382,7 @@ public final class CharSequencesTest extends TestCase {
          * otherwise it leads to a confusion in `EPSGDataAccess`.
          */
         assertFalse(isAcronymForWords("coordoperation", "Coordinate_Operation 
Method"));
+        assertFalse(isAcronymForWords("North along 15°W", "North along 
150°W"));
     }
 
     /**


Reply via email to