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 37c1c6ef59 Support mathematical operations in filters. 
https://issues.apache.org/jira/browse/SIS-622
37c1c6ef59 is described below

commit 37c1c6ef5916fb4a323831943dde2a0c29782031
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Oct 30 00:46:44 2025 +0100

    Support mathematical operations in filters.
    https://issues.apache.org/jira/browse/SIS-622
---
 .../apache/sis/cloud/aws/s3/ClientFileSystem.java  |   5 +-
 .../org.apache.sis.filter.FunctionRegister         |   3 +
 .../org.apache.sis.feature/main/module-info.java   |   3 +
 .../feature/internal/shared/FeatureExpression.java |  49 +++-
 .../org/apache/sis/filter/ArithmeticFunction.java  |  44 +---
 .../org/apache/sis/filter/AssociationValue.java    |   8 +
 .../apache/sis/filter/BinaryGeometryFilter.java    |   3 +-
 .../org/apache/sis/filter/ComparisonFilter.java    |   3 +-
 .../apache/sis/filter/DefaultFilterFactory.java    |   5 +-
 .../org/apache/sis/filter/IdentifierFilter.java    |   2 +-
 .../main/org/apache/sis/filter/LeafExpression.java |  11 +-
 .../main/org/apache/sis/filter/LikeFilter.java     |   2 +-
 .../main/org/apache/sis/filter/LogicalFilter.java  |   4 +-
 .../main/org/apache/sis/filter/Optimization.java   |   2 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |  18 +-
 .../main/org/apache/sis/filter/TemporalFilter.java |  12 +-
 .../sis/filter/{ => function}/BinaryFunction.java  |   7 +-
 .../sis/filter/{ => function}/ConvertFunction.java |  26 +-
 .../{internal => function}/GeometryConverter.java  |   4 +-
 .../GeometryFromFeature.java                       |   2 +-
 .../sis/filter/{internal => function}/Node.java    |   6 +-
 .../sis/filter/{ => function}/UnaryFunction.java   |  16 +-
 .../sis/filter/function/math/BinaryOperator.java   | 115 +++++++++
 .../apache/sis/filter/function/math/Function.java  | 287 +++++++++++++++++++++
 .../apache/sis/filter/function/math/Registry.java  | 102 ++++++++
 .../sis/filter/function/math/UnaryOperator.java    | 109 ++++++++
 .../{internal => function/math}/package-info.java  |   6 +-
 .../{internal => function}/package-info.java       |   4 +-
 .../sis/filter/internal/shared/FunctionNames.java  |   2 +-
 .../sis/filter/internal/shared/WarningEvent.java   |   2 +-
 .../sis/filter/internal/shared/package-info.java   |   2 +-
 .../sis/filter/sqlmm/FunctionDescription.java      |   5 +-
 .../apache/sis/filter/sqlmm/GeometryParser.java    |   2 +-
 .../main/org/apache/sis/filter/sqlmm/Registry.java |   4 +-
 .../apache/sis/filter/sqlmm/SpatialFunction.java   |  14 +-
 .../org/apache/sis/filter/sqlmm/package-info.java  |   2 +-
 .../org/apache/sis/filter/LogicalFilterTest.java   |   2 +-
 .../sis/filter/function/math/RegistryTest.java     |  62 +++++
 .../apache/sis/filter/sqlmm/RegistryTestCase.java  |   2 +-
 .../main/org/apache/sis/util/ObjectConverters.java |  14 +-
 .../org/apache/sis/util/collection/Containers.java |  35 ++-
 .../apache/sis/util/collection}/DerivedList.java   |  28 +-
 .../apache/sis/util/collection/package-info.java   |   2 +-
 netbeans-project/nbproject/project.xml             |   2 +
 44 files changed, 907 insertions(+), 131 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java
 
b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java
index afb73ee54d..b8aa19d3eb 100644
--- 
a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java
+++ 
b/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/ClientFileSystem.java
@@ -37,6 +37,7 @@ import software.amazon.awssdk.core.exception.SdkException;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.internal.shared.Strings;
 
 
@@ -188,7 +189,7 @@ final class ClientFileSystem extends FileSystem {
      */
     @Override
     public Iterable<Path> getRootDirectories() {
-        return new DerivedList<>(client().listBuckets().buckets(), (root) -> 
new KeyPath(this, root));
+        return Containers.derivedList(client().listBuckets().buckets(), (root) 
-> new KeyPath(this, root));
     }
 
     /**
@@ -200,7 +201,7 @@ final class ClientFileSystem extends FileSystem {
      */
     @Override
     public Iterable<FileStore> getFileStores() {
-        return new DerivedList<>(client().listBuckets().buckets(), 
BucketStore::new);
+        return Containers.derivedList(client().listBuckets().buckets(), 
BucketStore::new);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/META-INF/services/org.apache.sis.filter.FunctionRegister
 
b/endorsed/src/org.apache.sis.feature/main/META-INF/services/org.apache.sis.filter.FunctionRegister
new file mode 100644
index 0000000000..5a09fc780c
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/META-INF/services/org.apache.sis.filter.FunctionRegister
@@ -0,0 +1,3 @@
+# Workaround for Maven bug https://issues.apache.org/jira/browse/MNG-7855
+# Should be used only if the JAR file was on class-path rather than 
module-path.
+org.apache.sis.filter.function.math.Registry
diff --git a/endorsed/src/org.apache.sis.feature/main/module-info.java 
b/endorsed/src/org.apache.sis.feature/main/module-info.java
index 4930f89a37..732957e8d4 100644
--- a/endorsed/src/org.apache.sis.feature/main/module-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/module-info.java
@@ -33,6 +33,9 @@ module org.apache.sis.feature {
 
     uses org.apache.sis.filter.FunctionRegister;
 
+    provides org.apache.sis.filter.FunctionRegister
+        with org.apache.sis.filter.function.math.Registry;
+
     exports org.apache.sis.image;
     exports org.apache.sis.coverage;
     exports org.apache.sis.coverage.grid;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureExpression.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureExpression.java
index 0c86b600f7..c88f946989 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureExpression.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureExpression.java
@@ -21,7 +21,9 @@ import org.apache.sis.math.FunctionProperty;
 import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.filter.Optimization;
 import org.apache.sis.filter.DefaultFilterFactory;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.ConvertFunction;
+import org.apache.sis.filter.function.Node;
+import org.apache.sis.util.resources.Errors;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.feature.Feature;
@@ -59,19 +61,11 @@ public interface FeatureExpression<R,V> extends 
Expression<R,V> {
     }
 
     /**
-     * Returns the type of values computed by this expression, or {@code 
Object.class} if unknown.
-     *
-     * <h4>Note on type safety</h4>
-     * The parameterized type should be {@code <? extends V>} because some 
implementations get this
-     * information by a call to {@code value.getClass()}. But it should also 
be {@code <? super V>}
-     * for supporting the {@code Object.class} return value. Those 
contradictory requirements force
-     * us to use {@code <?>}.
+     * Returns the type of values computed by this expression, or {@code null} 
if unknown.
      *
      * @return the type of values computed by this expression.
      */
-    default Class<?> getValueClass() {
-        return Object.class;
-    }
+    Class<? extends V> getResultClass();
 
     /**
      * Provides the expected type of values produced by this expression when a 
feature of a given type is evaluated.
@@ -107,6 +101,39 @@ public interface FeatureExpression<R,V> extends 
Expression<R,V> {
      */
     FeatureProjectionBuilder.Item expectedType(FeatureProjectionBuilder addTo);
 
+    /**
+     * Returns an expression doing the same evaluation as this method, but 
returning results
+     * as values of the specified type. This method can return {@code this} if 
this expression
+     * is already guaranteed to provide results of the specified type.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation returns {@code this} if this expression 
already provides values
+     * of the specified type, or otherwise returns an expression doing 
conversions on-the-fly.
+     *
+     * @param  <N>     compile-time value of {@code target} type.
+     * @param  target  desired type of expression results.
+     * @return expression doing the same operation this this expression but 
with results of the specified type.
+     * @throws ClassCastException if the specified type is not a supported 
target type.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    default <N> Expression<R,N> toValueType(final Class<N> target) {
+        UnconvertibleObjectException error = null;
+        final Class<? extends V> current = getResultClass();
+        if (current != null) {
+            if (target.isAssignableFrom(current)) {
+                return (Expression<R,N>) this;
+            } else try {
+                return new ConvertFunction<>(this, current, target);
+            } catch (UnconvertibleObjectException e) {
+                error = e;
+            }
+        }
+        var e = new 
ClassCastException(Errors.format(Errors.Keys.CanNotConvertValue_2, 
getFunctionName(), target));
+        e.initCause(error);
+        throw e;
+    }
+
     /**
      * Tries to cast or convert the given expression to a {@link 
FeatureExpression}.
      * If the given expression cannot be cast, then this method creates a copy
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
index 0e5521269e..031b06a7ae 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
@@ -21,9 +21,8 @@ import java.math.BigInteger;
 import org.opengis.util.ScopedName;
 import org.apache.sis.feature.internal.shared.FeatureExpression;
 import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
+import org.apache.sis.filter.function.BinaryFunction;
 import org.apache.sis.filter.internal.shared.FunctionNames;
-import org.apache.sis.util.UnconvertibleObjectException;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Fraction;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -40,8 +39,8 @@ import org.opengis.filter.Expression;
  *
  * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
  */
-abstract class ArithmeticFunction<R> extends BinaryFunction<R,Number,Number>
-        implements FeatureExpression<R,Number>, 
Optimization.OnExpression<R,Number>
+abstract class ArithmeticFunction<R> extends BinaryFunction<R, Number, Number>
+        implements FeatureExpression<R, Number>, Optimization.OnExpression<R, 
Number>
 {
     /**
      * For cross-version compatibility.
@@ -57,6 +56,14 @@ abstract class ArithmeticFunction<R> extends 
BinaryFunction<R,Number,Number>
         super(expression1, expression2);
     }
 
+    /**
+     * Returns the type of values computed by this expression.
+     */
+    @Override
+    public final Class<Number> getResultClass() {
+        return Number.class;
+    }
+
     /**
      * Creates an attribute type for numeric values of the given name.
      * The attribute is mandatory, unbounded and has no default value.
@@ -64,7 +71,7 @@ abstract class ArithmeticFunction<R> extends 
BinaryFunction<R,Number,Number>
      * @param  name  name of the attribute to create.
      * @return an attribute of the given name for numbers.
      */
-    static AttributeType<Number> createNumericType(final String name) {
+    private static AttributeType<Number> createNumericType(final String name) {
         return createType(Number.class, name);
     }
 
@@ -73,14 +80,6 @@ abstract class ArithmeticFunction<R> extends 
BinaryFunction<R,Number,Number>
      */
     protected abstract AttributeType<Number> expectedType();
 
-    /**
-     * Returns the type of values computed by this expression.
-     */
-    @Override
-    public final Class<?> getValueClass() {
-        return Number.class;
-    }
-
     /**
      * Provides the type of results computed by this expression. That type 
depends only
      * on the {@code ArithmeticFunction} subclass and is given by {@link 
#expectedType()}.
@@ -108,25 +107,6 @@ abstract class ArithmeticFunction<R> extends 
BinaryFunction<R,Number,Number>
         return null;
     }
 
-    /**
-     * Returns {@code this} if this expression provides values of the 
specified type,
-     * or otherwise returns an expression doing conversions on-the-fly.
-     *
-     * @throws ClassCastException if the specified type is not a supported 
target type.
-     */
-    @Override
-    @SuppressWarnings("unchecked")
-    public <N> Expression<R,N> toValueType(final Class<N> target) {
-        if (target.isAssignableFrom(Number.class)) {
-            return (Expression<R,N>) this;
-        } else try {
-            return new ConvertFunction<>(this, Number.class, target);
-        } catch (UnconvertibleObjectException e) {
-            throw (ClassCastException) new ClassCastException(Errors.format(
-                    Errors.Keys.CanNotConvertValue_2, getFunctionName(), 
target)).initCause(e);
-        }
-    }
-
     /**
      * The "Add" (+) expression.
      *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
index 2e0f126734..8db223d154 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
@@ -98,6 +98,14 @@ final class AssociationValue<V> extends 
LeafExpression<Feature, V>
         return Feature.class;
     }
 
+    /**
+     * Returns the type of values computed by this expression.
+     */
+    @Override
+    public Class<? extends V> getResultClass() {
+        return accessor.getResultClass();
+    }
+
     /**
      * Returns the manner in which values are computed from given resources.
      * This method assumes an initially empty set of properties, then adds the 
transitive properties.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index d8e1b00f43..52ae4215e2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@ -24,7 +24,7 @@ import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.geometry.wrapper.SpatialOperationContext;
 import org.apache.sis.feature.internal.shared.AttributeConvention;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.util.Exceptions;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -85,6 +85,7 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
      * @param  geometry2   the second of the two expressions to be used by 
this function.
      * @param  systemUnit  if the CRS needs to be in some units of 
measurement, the {@link Unit#getSystemUnit()} value.
      */
+    @SuppressWarnings("UseSpecificCatch")
     protected BinaryGeometryFilter(final Geometries<?> library,
                                    final Expression<R,?> geometry1,
                                    final Expression<R,?> geometry2,
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
index 04b5b9d050..40d1474a8f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
@@ -36,7 +36,8 @@ import java.time.chrono.ChronoZonedDateTime;
 import java.time.temporal.ChronoField;
 import java.time.temporal.Temporal;
 import org.apache.sis.math.Fraction;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
+import org.apache.sis.filter.function.BinaryFunction;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Filter;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index 22bab587df..156724aa33 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@ -29,6 +29,7 @@ import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.geometry.WraparoundMethod;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.filter.function.UnaryFunction;
 import org.apache.sis.filter.sqlmm.Registry;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.iso.AbstractFactory;
@@ -1049,9 +1050,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
             if (availableFunctions.isEmpty()) {
                 /*
                  * Load functions when first needed or if the module path 
changed since last invocation.
-                 * The SQLMM factory is hard-coded because it is considered as 
a basic service to
-                 * be provided by all DefaultFilterFactory implementations, 
and for avoiding the
-                 * need to make SQLMM registry class public.
+                 * The SQLMM factory is hard-coded because it depends on the 
geometry library.
                  */
                 final Registry r = new Registry(library);
                 for (final String fn : r.getNames()) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
index 39d8bc7b3d..411cf97321 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
@@ -19,7 +19,7 @@ package org.apache.sis.filter;
 import java.util.List;
 import java.util.Collection;
 import java.util.Objects;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.feature.internal.shared.AttributeConvention;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
index 8008b303e5..1078a36f3a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
@@ -28,7 +28,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.WeakValueHashMap;
 import org.apache.sis.feature.internal.shared.FeatureExpression;
 import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.math.FunctionProperty;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -131,8 +131,8 @@ abstract class LeafExpression<R,V> extends Node implements 
FeatureExpression<R,V
         }
 
         /** Returns the type of values computed by this expression. */
-        @Override public Class<?> getValueClass() {
-            return (value != null) ? value.getClass() : Object.class;
+        @Override public Class<? extends V> getResultClass() {
+            return Classes.getClass(value);
         }
 
         /** Expression evaluation, which just returns the constant value. */
@@ -189,7 +189,10 @@ abstract class LeafExpression<R,V> extends Node implements 
FeatureExpression<R,V
          */
         @Override
         public FeatureProjectionBuilder.Item expectedType(final 
FeatureProjectionBuilder addTo) {
-            final Class<?> valueType = getValueClass();
+            Class<?> valueType = getResultClass();
+            if (valueType == null) {
+                valueType = Object.class;
+            }
             AttributeType<?> propertyType;
             synchronized (TYPES) {
                 propertyType = TYPES.get(valueType);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LikeFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LikeFilter.java
index 25a68709e0..bf2e46ff25 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LikeFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LikeFilter.java
@@ -20,7 +20,7 @@ import java.util.List;
 import java.util.Collection;
 import java.util.Objects;
 import java.util.regex.Pattern;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Filter;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
index d641643596..b0913adc9c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
@@ -21,7 +21,7 @@ import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.Objects;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.util.internal.shared.CollectionsExt;
 import org.apache.sis.util.internal.shared.UnmodifiableArrayList;
 
@@ -61,7 +61,7 @@ abstract class LogicalFilter<R> extends Node implements 
LogicalOperator<R>, Opti
     LogicalFilter(final Collection<? extends Filter<R>> op) {
         ArgumentChecks.ensureNonEmpty("operands", op);
         operands = op.toArray(Filter[]::new);
-        ArgumentChecks.ensureCountBetween("operands", true, 2, 
Integer.MAX_VALUE, operands.length);
+        ArgumentChecks.ensureCountBetween("operands", false, 2, 
Integer.MAX_VALUE, operands.length);
         for (int i=0; i<operands.length; i++) {
             ArgumentChecks.ensureNonNullElement("operands", i, operands[i]);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
index 1c0cb4bc3b..023e15aa45 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
@@ -26,7 +26,7 @@ import java.util.function.Predicate;
 import org.opengis.util.CodeList;
 import org.apache.sis.math.FunctionProperty;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.util.internal.shared.CollectionsExt;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index 8df2b62ed6..c6c0a63b90 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
@@ -160,7 +160,7 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
 
     /**
      * Returns the type of values fetched from {@link Feature} instance.
-     * This is the type before conversion to the {@linkplain #getValueClass() 
target type}.
+     * This is the type before conversion to the {@linkplain #getResultClass() 
target type}.
      * The type is always {@link Object} on newly created expression because 
the type of feature property
      * values is unknown, but may become a specialized type after {@link 
Optimization} has been applied.
      */
@@ -177,7 +177,8 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
      * @see #expectedType(FeatureProjectionBuilder)
      */
     final FeatureProjectionBuilder.Item defaultType(final 
FeatureProjectionBuilder addTo) {
-        return 
addTo.addComputedProperty(addTo.addAttribute(getValueClass()).setMinimumOccurs(0).setName(name),
 true);
+        // `getResultClass()` should never return null with our subtypes of 
`PropertyValue`.
+        return 
addTo.addComputedProperty(addTo.addAttribute(getResultClass()).setMinimumOccurs(0).setName(name),
 true);
     }
 
     /**
@@ -186,7 +187,8 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
     @Override
     @SuppressWarnings("unchecked")
     public final <N> PropertyValue<N> toValueType(final Class<N> target) {
-        if (target.equals(getValueClass())) {
+        // `getResultClass()` should never return null with our subtypes of 
`PropertyValue`.
+        if (target == getResultClass()) {
             return (PropertyValue<N>) this;
         }
         final Class<?> source = getSourceClass();
@@ -225,6 +227,14 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
             super(name, isVirtual);
         }
 
+        /**
+         * Returns the type of objects retuned by this expression.
+         */
+        @Override
+        public Class<Object> getResultClass() {
+            return Object.class;
+        }
+
         /**
          * Returns the value of the property of the name given at construction 
time.
          * If no value is found for the given feature, then this method 
returns {@code null}.
@@ -290,7 +300,7 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
          * Returns the type of values computed by this expression.
          */
         @Override
-        public final Class<V> getValueClass() {
+        public final Class<V> getResultClass() {
             return type;
         }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
index 64a5121d8e..b52a8e656a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.filter;
 
+import org.apache.sis.filter.function.BinaryFunction;
 import java.time.DateTimeException;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.resources.Errors;
@@ -126,14 +127,13 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
 
     /**
      * Returns the class of values computed by the given expression, or {@code 
type} if unknown.
+     *
+     * @param  e     the expression from which to get the type.
+     * @param  type  the base type, used as a default type.
      */
-    @SuppressWarnings("unchecked")
-    private static <T> Class<? extends T> getValueClass(final Expression<?,? 
extends T> e, final Class<T> type) {
+    private static <T> Class<? extends T> getValueClass(final Expression<?, ? 
extends T> e, final Class<T> type) {
         if (e instanceof FeatureExpression<?,?>) {
-            final Class<?> c = ((FeatureExpression<?, ? extends T>) 
e).getValueClass();
-            if (type.isAssignableFrom(c)) {
-                return (Class<? extends T>) c;
-            }
+            return ((FeatureExpression<?, ? extends T>) e).getResultClass();
         }
         return type;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/BinaryFunction.java
similarity index 98%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryFunction.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/BinaryFunction.java
index ddafb45f34..3fe34f8462 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/BinaryFunction.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter;
+package org.apache.sis.filter.function;
 
 import java.util.List;
 import java.util.Collection;
@@ -24,7 +24,6 @@ import java.math.BigDecimal;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.math.Fraction;
 import org.apache.sis.math.DecimalFunctions;
-import org.apache.sis.filter.internal.Node;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Filter;
@@ -44,7 +43,7 @@ import org.opengis.filter.Expression;
  * @param  <V1>  the type of value computed by the first expression.
  * @param  <V2>  the type of value computed by the second expression.
  */
-abstract class BinaryFunction<R,V1,V2> extends Node {
+public abstract class BinaryFunction<R,V1,V2> extends Node {
     /**
      * For cross-version compatibility.
      */
@@ -104,7 +103,7 @@ abstract class BinaryFunction<R,V1,V2> extends Node {
      *
      * @return a list of size 2 containing the two expressions.
      */
-    public List<Expression<R,?>> getExpressions() {
+    public final List<Expression<R,?>> getExpressions() {
         return List.of(expression1, expression2);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/ConvertFunction.java
similarity index 91%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/ConvertFunction.java
index faa913ae15..af760f3199 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/ConvertFunction.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter;
+package org.apache.sis.filter.function;
 
 import java.util.Set;
 import java.util.List;
@@ -23,10 +23,11 @@ import org.opengis.util.ScopedName;
 import org.apache.sis.util.ObjectConverter;
 import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.feature.internal.shared.FeatureExpression;
 import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
+import org.apache.sis.filter.Optimization;
 import org.apache.sis.math.FunctionProperty;
-import org.apache.sis.util.resources.Errors;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Expression;
@@ -41,9 +42,9 @@ import org.opengis.filter.Expression;
  * @param  <S>  the type of value computed by the wrapped exception. This is 
the type to convert.
  * @param  <V>  the type of value computed by this expression. This is the 
type after conversion.
  *
- * @see org.apache.sis.filter.internal.shared.GeometryConverter
+ * @see GeometryConverter
  */
-final class ConvertFunction<R,S,V> extends UnaryFunction<R,S>
+public final class ConvertFunction<R,S,V> extends UnaryFunction<R,S>
         implements FeatureExpression<R,V>, Optimization.OnExpression<R,V>
 {
     /**
@@ -70,7 +71,10 @@ final class ConvertFunction<R,S,V> extends UnaryFunction<R,S>
      * @param  target      the desired type for the expression result.
      * @throws UnconvertibleObjectException if no converter is found.
      */
-    ConvertFunction(final Expression<R, ? extends S> expression, final 
Class<S> source, final Class<V> target) {
+    public ConvertFunction(final Expression<R, ? extends S> expression,
+                           final Class<? extends S> source,
+                           final Class<V> target)
+    {
         super(expression);
         converter = ObjectConverters.find(source, target);
     }
@@ -94,8 +98,8 @@ final class ConvertFunction<R,S,V> extends UnaryFunction<R,S>
     public Expression<R,V> recreate(Expression<R,?>[] effective) {
         final Expression<R,?> e = effective[0];
         if (e instanceof FeatureExpression<?,?>) {
-            final Class<? extends V> target = getValueClass();                 
         // This is <V>.
-            final Class<?> source = ((FeatureExpression<?,?>) 
e).getValueClass();       // May become <S>.
+            final Class<? extends V> target = getResultClass();                
         // This is <V>.
+            final Class<?> source = ((FeatureExpression<?,?>) 
e).getResultClass();      // May become <S>.
             if (target.isAssignableFrom(source)) {
                 return (Expression<R,V>) e;
             }
@@ -131,7 +135,7 @@ final class ConvertFunction<R,S,V> extends 
UnaryFunction<R,S>
      */
     @Override
     protected Collection<?> getChildren() {
-        return List.of(expression, converter.getSourceClass(), 
converter.getTargetClass());
+        return List.of(expression, converter.getSourceClass(), 
getResultClass());
     }
 
     /**
@@ -158,7 +162,7 @@ final class ConvertFunction<R,S,V> extends 
UnaryFunction<R,S>
      * Returns the type of values computed by this expression.
      */
     @Override
-    public Class<? extends V> getValueClass() {
+    public final Class<? extends V> getResultClass() {
         return converter.getTargetClass();
     }
 
@@ -175,7 +179,7 @@ final class ConvertFunction<R,S,V> extends 
UnaryFunction<R,S>
             return null;
         }
         final FeatureProjectionBuilder.Item item = 
addTo.addTemplateProperty(fex);
-        item.replaceValueClass((c) -> getValueClass());
+        item.replaceValueClass((c) -> getResultClass());
         return item;
     }
 
@@ -191,7 +195,7 @@ final class ConvertFunction<R,S,V> extends 
UnaryFunction<R,S>
     @Override
     @SuppressWarnings("unchecked")
     public <N> Expression<R,N> toValueType(final Class<N> target) {
-        if (target.isAssignableFrom(getValueClass())) {
+        if (target.isAssignableFrom(getResultClass())) {
             return (Expression<R,N>) this;
         }
         final Class<? super S> source = converter.getSourceClass();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/GeometryConverter.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/GeometryConverter.java
index 5e77717b8c..aa06be8c95 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/GeometryConverter.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter.internal;
+package org.apache.sis.filter.function;
 
 import java.util.List;
 import java.util.Collection;
@@ -48,7 +48,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
  * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
  * @param  <G>  the geometry implementation type.
  *
- * @see org.apache.sis.filter.ConvertFunction
+ * @see ConvertFunction
  */
 class GeometryConverter<R,G> extends Node implements 
Optimization.OnExpression<R, GeometryWrapper> {
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/GeometryFromFeature.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/GeometryFromFeature.java
index de82d0562e..9b99ad9ca7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/GeometryFromFeature.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter.internal;
+package org.apache.sis.filter.function;
 
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometry.wrapper.Geometries;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/Node.java
similarity index 98%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/Node.java
index 37951b1e6f..35e356b6d3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/Node.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter.internal;
+package org.apache.sis.filter.function;
 
 import java.util.Set;
 import java.util.Map;
@@ -97,7 +97,7 @@ public abstract class Node implements Serializable {
      *
      * @see Expression#getFunctionName()
      */
-    protected static <T> AttributeType<T> createType(final Class<T> type, 
final Object name) {
+    public static <T> AttributeType<T> createType(final Class<T> type, final 
Object name) {
         // We do not use `Map.of(…)` for better exception message in case of 
null name.
         return new 
DefaultAttributeType<>(Collections.singletonMap(DefaultAttributeType.NAME_KEY, 
name),
                                           type, 1, 1, null, 
(AttributeType<?>[]) null);
@@ -167,7 +167,7 @@ public abstract class Node implements Serializable {
      * @param  tip  the expression name in SIS namespace.
      * @return an expression name in the SIS namespace.
      */
-    protected static ScopedName createName(final String tip) {
+    public static ScopedName createName(final String tip) {
         return Names.createScopedName(SCOPE, null, tip);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/UnaryFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/UnaryFunction.java
similarity index 92%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/UnaryFunction.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/UnaryFunction.java
index ee78c0e1fa..dce8e0173f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/UnaryFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/UnaryFunction.java
@@ -14,14 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter;
+package org.apache.sis.filter.function;
 
 import java.util.List;
 import java.util.Collection;
 import java.util.Objects;
 import java.util.Optional;
+import org.apache.sis.filter.Optimization;
 import org.apache.sis.xml.NilReason;
-import org.apache.sis.filter.internal.Node;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Filter;
@@ -40,7 +40,7 @@ import org.opengis.filter.NullOperator;
  * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
  * @param  <V>  the type of value computed by the expression.
  */
-class UnaryFunction<R,V> extends Node {
+public class UnaryFunction<R,V> extends Node {
     /**
      * For cross-version compatibility.
      */
@@ -57,7 +57,7 @@ class UnaryFunction<R,V> extends Node {
     /**
      * Creates a new unary operator.
      */
-    UnaryFunction(final Expression<R, ? extends V> expression) {
+    protected UnaryFunction(final Expression<R, ? extends V> expression) {
         this.expression = Objects.requireNonNull(expression);
     }
 
@@ -106,14 +106,14 @@ class UnaryFunction<R,V> extends Node {
      *
      * @param  <R>  the type of resources used as inputs.
      */
-    static final class IsNull<R> extends UnaryFunction<R,Object>
+    public static final class IsNull<R> extends UnaryFunction<R,Object>
             implements NullOperator<R>, Optimization.OnFilter<R>
     {
         /** For cross-version compatibility. */
         private static final long serialVersionUID = 2960285515924533419L;
 
         /** Creates a new operator. */
-        IsNull(final Expression<R,?> expression) {
+        public IsNull(final Expression<R,?> expression) {
             super(expression);
         }
 
@@ -141,7 +141,7 @@ class UnaryFunction<R,V> extends Node {
      *
      * @param  <R>  the type of resources used as inputs.
      */
-    static final class IsNil<R> extends UnaryFunction<R,Object>
+    public static final class IsNil<R> extends UnaryFunction<R,Object>
             implements NilOperator<R>, Optimization.OnFilter<R>
     {
         /** For cross-version compatibility. */
@@ -151,7 +151,7 @@ class UnaryFunction<R,V> extends Node {
         private final String nilReason;
 
         /** Creates a new operator. */
-        IsNil(final Expression<R,?> expression, final String nilReason) {
+        public IsNil(final Expression<R,?> expression, final String nilReason) 
{
             super(expression);
             this.nilReason = nilReason;
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/BinaryOperator.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/BinaryOperator.java
new file mode 100644
index 0000000000..6d9714d05b
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/BinaryOperator.java
@@ -0,0 +1,115 @@
+/*
+ * 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.filter.function.math;
+
+import java.util.Objects;
+import java.util.function.DoubleBinaryOperator;
+import java.io.ObjectStreamException;
+import org.opengis.util.ScopedName;
+import org.apache.sis.filter.Optimization;
+import org.apache.sis.filter.function.BinaryFunction;
+import org.apache.sis.feature.internal.shared.FeatureExpression;
+import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.Expression;
+
+
+/**
+ * An operation upon two operands.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
+ */
+final class BinaryOperator<R> extends BinaryFunction<R, Number, Number>
+        implements FeatureExpression<R, Double>, Optimization.OnExpression<R, 
Double>
+{
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 8021641013005967925L;
+
+    /**
+     * The function to apply.
+     */
+    private final Function function;
+
+    /**
+     * The {@link Function#binary} value, guaranteed non-null.
+     */
+    private final transient DoubleBinaryOperator math;
+
+    /**
+     * Creates a new function.
+     */
+    BinaryOperator(final Function function,
+            final Expression<R, ? extends Number> expression1,
+            final Expression<R, ? extends Number> expression2)
+    {
+        super(expression1, expression2);
+        this.function = function;
+        math = Objects.requireNonNull(function.binary);
+    }
+
+    /**
+     * Invoked at deserialization time for setting the {@link #math} field.
+     */
+    private Object readResolve() throws ObjectStreamException {
+        return new BinaryOperator<>(function, expression1, expression2);
+    }
+
+    /**
+     * Returns the name of the function to be called.
+     */
+    @Override
+    public ScopedName getFunctionName() {
+        return function.getFunctionName();
+    }
+
+    /**
+     * Returns the type of values computed by this expression.
+     */
+    @Override
+    public final Class<Double> getResultClass() {
+        return Double.class;
+    }
+
+    /**
+     * Provides the type of results computed by this expression. That type 
depends only
+     * on the {@code ArithmeticFunction} subclass and is given by {@link 
#expectedType()}.
+     */
+    @Override
+    public final FeatureProjectionBuilder.Item 
expectedType(FeatureProjectionBuilder addTo) {
+        return addTo.addSourceProperty(function.getResultType(), false);
+    }
+
+    /**
+     * Evaluates the expression.
+     */
+    @Override
+    public final Double apply(final R feature) {
+        final Number left  = expression1.apply(feature);
+        if (left != null) {
+            final Number right = expression2.apply(feature);
+            if (right != null) {
+                return math.applyAsDouble(left.doubleValue(), 
right.doubleValue());
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/Function.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/Function.java
new file mode 100644
index 0000000000..74694d827a
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/Function.java
@@ -0,0 +1,287 @@
+/*
+ * 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.filter.function.math;
+
+import java.util.Map;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.DoubleUnaryOperator;
+import java.util.function.DoubleBinaryOperator;
+import org.opengis.util.TypeName;
+import org.opengis.util.LocalName;
+import org.opengis.util.ScopedName;
+import org.opengis.parameter.ParameterDescriptor;
+import org.apache.sis.parameter.DefaultParameterDescriptor;
+import org.apache.sis.feature.internal.shared.FeatureExpression;
+import org.apache.sis.filter.function.Node;
+import org.apache.sis.util.iso.Names;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.capability.AvailableFunction;
+import org.opengis.feature.AttributeType;
+
+
+/**
+ * Descriptions of mathematical operations.
+ * These functions are not standard in the ANSI SQL-92 specification,
+ * therefore they may or may not be available on a specific database.
+ * However, most of them seem available on major database systems.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+enum Function implements AvailableFunction {
+    /*
+     * MIN and MAX are omitted because it needs more generic code working with 
Comparable.
+     * We may need specializations here for the handling of NaN, but this is 
deferred to a
+     * future version.
+     */
+
+    /**
+     * The absolute value of <var>x</var>.
+     */
+    ABS(Math::abs),
+
+    /**
+     * The sign of input <var>x</var> as -1, 0, or 1.
+     */
+    SIGN(Math::signum),
+
+    /**
+     * The value of <var>x</var> rounded to the nearest whole integer.
+     */
+    ROUND(Math::rint),
+
+    /**
+     * The largest integer value that is less than or equal to <var>x</var>.
+     */
+    FLOOR(Math::floor),
+
+    /**
+     * The smallest integer value that is greater than or equal to 
<var>x</var>.
+     * This is named {@code CEILING} in some databases.
+     */
+    CEIL(Math::ceil),
+
+    /**
+     * The logarithm in base 10 of <var>x</var>.
+     */
+    LOG10(Math::log10),
+
+    /**
+     * The logarithm in base {@linkplain Math#E e} of <var>x</var>.
+     */
+    LOG(Math::log),
+
+    /**
+     * The value {@linkplain Math#E e} raised to power <var>x</var>.
+     */
+    EXP(Math::exp),
+
+    /**
+     * The value of <var>x</var> raised to the power of <var>y</var>.
+     */
+    POWER(Math::pow),
+
+    /**
+     * The square-root value of <var>x</var>.
+     */
+    SQRT(Math::sqrt),
+
+    /**
+     * The cubic-root value of <var>x</var>.
+     */
+    CBRT(Math::cbrt),
+
+    /**
+     * Hypotenuse of <var>x</var> and <var>y</var>.
+     */
+    HYPOT(Math::hypot),
+
+    /**
+     * The arc sine of <var>x</var>.
+     */
+    ASIN(Math::asin),
+
+    /**
+     * The arc cosine of <var>x</var>.
+     */
+    ACOS(Math::acos),
+
+    /**
+     * The arc tangent of <var>x</var>.
+     */
+    ATAN(Math::atan),
+
+    /**
+     * The arc tangent of <var>y</var>/<var>x</var>.
+     * Note that <var>y</var> is the first argument and <var>x</var> is the 
second argument.
+     */
+    ATAN2(Math::atan2),
+
+    /**
+     * The hyperbolic sine of <var>x</var>.
+     */
+    SINH(Math::sinh),
+
+    /**
+     * The hyperbolic cosine of <var>x</var>.
+     */
+    COSH(Math::cosh),
+
+    /**
+     * The hyperbolic tangent of <var>x</var>.
+     */
+    TANH(Math::tanh);
+
+    /**
+     * The mathematical function to invoke if this operation is unary, or 
{@code null}.
+     */
+    @SuppressWarnings("serial")
+    final DoubleUnaryOperator unary;
+
+    /**
+     * The mathematical function to invoke if this operation is binary, or 
{@code null}.
+     */
+    @SuppressWarnings("serial")
+    final DoubleBinaryOperator binary;
+
+    /**
+     * The name of this function, created when first needed.
+     *
+     * @see #getFunctionName()
+     */
+    private ScopedName name;
+
+    /**
+     * Description of the result, created when first needed.
+     *
+     * @see #getResultType()
+     */
+    private AttributeType<Double> resultType;
+
+    /**
+     * Creates a new function description for a unary operation.
+     */
+    private Function(final DoubleUnaryOperator math) {
+        unary  = math;
+        binary = null;
+    }
+
+    /**
+     * Creates a new function description for a binary operation.
+     */
+    private Function(final DoubleBinaryOperator math) {
+        unary  = null;
+        binary = math;
+    }
+
+    /**
+     * Returns the minimum number of parameters expected by this function.
+     */
+    final int getMinParameterCount() {
+        if (unary  != null) return 1;
+        if (binary != null) return 2;
+        return 0;
+    }
+
+    /**
+     * Returns the maximum number of parameters expected by this function.
+     */
+    final int getMaxParameterCount() {
+        if (binary != null) return 2;
+        if (unary  != null) return 1;
+        return 0;
+    }
+
+    /**
+     * Returns the function name.
+     */
+    @Override
+    public LocalName getName() {
+        return getFunctionName().tip();
+    }
+
+    /**
+     * Returns the function name returned by the expression.
+     *
+     * @see FeatureExpression#getFunctionName()
+     */
+    final synchronized ScopedName getFunctionName() {
+        if (name == null) {
+            name = Node.createName(camelCaseName());
+        }
+        return name;
+    }
+
+    /**
+     * Returns the function name in the case to show to users.
+     */
+    final String camelCaseName() {
+        return name().toLowerCase(Locale.US).intern();
+    }
+
+    /**
+     * Returns the attribute type to declare in feature types that store 
result of this function.
+     */
+    final synchronized AttributeType<Double> getResultType() {
+        if (resultType == null) {
+            resultType = Node.createType(Double.class, camelCaseName());
+        }
+        return resultType;
+    }
+
+    /**
+     * Returns the type of return value.
+     */
+    @Override
+    public TypeName getReturnType() {
+        return RESULT_TYPE;
+    }
+
+    /**
+     * The type of values produced by all functions in this enumeration.
+     */
+    private static final TypeName RESULT_TYPE = 
Names.createTypeName(Double.class);
+
+    /**
+     * Returns the list of arguments expected by the function.
+     */
+    @Override
+    public List<? extends ParameterDescriptor<?>> getArguments() {
+        if (unary != null) {
+            return List.of(X);
+        } else if (this != ATAN2) {
+            return List.of(X, Y);
+        } else {
+            return List.of(Y, X);   // Special case for `atan2(y, x)`.
+        }
+    }
+
+    /**
+     * Description of a function parameter.
+     */
+    private static final DefaultParameterDescriptor<Number> X = 
parameter("x"), Y = parameter("y");
+
+    /**
+     * Creates a parameter descriptor of the given name.
+     */
+    private static DefaultParameterDescriptor<Number> parameter(final String 
name) {
+        return new DefaultParameterDescriptor<>(
+                Map.of(DefaultParameterDescriptor.NAME_KEY, name),
+                1, 1, Number.class, null, null, null);
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/Registry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/Registry.java
new file mode 100644
index 0000000000..bc5dd1d342
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/Registry.java
@@ -0,0 +1,102 @@
+/*
+ * 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.filter.function.math;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import org.apache.sis.filter.FunctionRegister;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.internal.shared.Constants;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.Expression;
+import org.opengis.filter.capability.AvailableFunction;
+
+
+/**
+ * A register of mathematical functions.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class Registry implements FunctionRegister {
+    /**
+     * Creates a new registry.
+     *
+     * @todo Replace by a static {@code provider()} method after we abandon 
classpath support.
+     */
+    public Registry() {
+    }
+
+    /**
+     * Returns the name of body defining the functions.
+     * Since we have no standard to refer to, we use "SIS" for now.
+     */
+    @Override
+    public String getAuthority() {
+        return Constants.SIS;
+    }
+
+    /**
+     * Returns the names of all functions that this factory can create.
+     */
+    @Override
+    public Collection<String> getNames() {
+        return Containers.derivedList(Arrays.asList(Function.values()), 
Function::camelCaseName);
+    }
+
+    /**
+     * Describes the parameters of a function.
+     *
+     * @param  name  name of the function to describe (not null).
+     * @return description of the function parameters.
+     * @throws IllegalArgumentException if function name is unknown..
+     */
+    @Override
+    public AvailableFunction describe(String name) {
+        return Function.valueOf(name.toUpperCase(Locale.US));
+    }
+
+    /**
+     * Creates a new function of the given name with given parameters.
+     *
+     * @param  <R>         the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
+     * @param  name        name of the function to create (not null).
+     * @param  parameters  function parameters.
+     * @return function for the given name and parameters.
+     * @throws IllegalArgumentException if function name is unknown or some 
parameters are illegal.
+     */
+    @Override
+    public <R> Expression<R, ?> create(final String name, final 
Expression<R,?>[] parameters) {
+        final Function function = 
Function.valueOf(name.toUpperCase(Locale.US));
+        ArgumentChecks.ensureCountBetween("parameters", false,
+                                          function.getMinParameterCount(),
+                                          function.getMaxParameterCount(),
+                                          parameters.length);
+        switch (parameters.length) {
+            case 1: return new UnaryOperator<>(function,
+                    parameters[0].toValueType(Number.class));
+
+            case 2: return new BinaryOperator<>(function,
+                    parameters[0].toValueType(Number.class),
+                    parameters[1].toValueType(Number.class));
+        }
+        throw new IllegalArgumentException();   // Should never happen.
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/UnaryOperator.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/UnaryOperator.java
new file mode 100644
index 0000000000..b308a070b4
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/UnaryOperator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.filter.function.math;
+
+import java.util.Objects;
+import java.util.function.DoubleUnaryOperator;
+import java.io.ObjectStreamException;
+import org.opengis.util.ScopedName;
+import org.apache.sis.filter.Optimization;
+import org.apache.sis.filter.function.UnaryFunction;
+import org.apache.sis.feature.internal.shared.FeatureExpression;
+import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.Expression;
+
+
+/**
+ * An operation on a single operand.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
+ */
+final class UnaryOperator<R> extends UnaryFunction<R, Number>
+        implements FeatureExpression<R, Double>, Optimization.OnExpression<R, 
Double>
+{
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -6215509464490587978L;
+
+    /**
+     * The function to apply.
+     */
+    private final Function function;
+
+    /**
+     * The {@link Function#binary} value, guaranteed non-null.
+     */
+    private final transient DoubleUnaryOperator math;
+
+    /**
+     * Creates a new function.
+     */
+    UnaryOperator(final Function function, final Expression<R, ? extends 
Number> expression) {
+        super(expression);
+        this.function = function;
+        math = Objects.requireNonNull(function.unary);
+    }
+
+    /**
+     * Invoked at deserialization time for setting the {@link #math} field.
+     */
+    private Object readResolve() throws ObjectStreamException {
+        return new UnaryOperator<>(function, expression);
+    }
+
+    /**
+     * Returns the name of the function to be called.
+     */
+    @Override
+    public ScopedName getFunctionName() {
+        return function.getFunctionName();
+    }
+
+    /**
+     * Returns the type of values computed by this expression.
+     */
+    @Override
+    public final Class<Double> getResultClass() {
+        return Double.class;
+    }
+
+    /**
+     * Provides the type of results computed by this expression. That type 
depends only
+     * on the {@code ArithmeticFunction} subclass and is given by {@link 
#expectedType()}.
+     */
+    @Override
+    public final FeatureProjectionBuilder.Item 
expectedType(FeatureProjectionBuilder addTo) {
+        return addTo.addSourceProperty(function.getResultType(), false);
+    }
+
+    /**
+     * Evaluates the expression.
+     */
+    @Override
+    public final Double apply(final R feature) {
+        final Number value  = expression.apply(feature);
+        if (value != null) {
+            return math.applyAsDouble(value.doubleValue());
+        }
+        return null;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/package-info.java
similarity index 79%
copy from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/package-info.java
copy to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/package-info.java
index b43d313abf..911de396d2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/math/package-info.java
@@ -16,9 +16,11 @@
  */
 
 /**
- * Base implementation shared by the main {@code filter} package and the SQLMM 
extension.
+ * Partial implementation of mathematical operations as filter expressions.
+ * The main public class in this package is {@link Registry},
+ * which is the single entry point for all functions.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-package org.apache.sis.filter.internal;
+package org.apache.sis.filter.function.math;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/package-info.java
similarity index 92%
rename from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/package-info.java
rename to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/package-info.java
index b43d313abf..b7b091de5c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/function/package-info.java
@@ -16,9 +16,9 @@
  */
 
 /**
- * Base implementation shared by the main {@code filter} package and the SQLMM 
extension.
+ * Base implementation shared by the main {@code filter} package and the 
<abbr>SQLMM</abbr> extension.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-package org.apache.sis.filter.internal;
+package org.apache.sis.filter.function;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/FunctionNames.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/FunctionNames.java
index e248d4b306..de621e6fad 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/FunctionNames.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/FunctionNames.java
@@ -20,7 +20,7 @@ import org.apache.sis.filter.sqlmm.SQLMM;
 
 
 /**
- * Names of some expressions used in Apache SIS.
+ * Names of some expressions used in Apache <abbr>SIS</abbr>.
  * This class defines only the names that need to be referenced from at least 
two different classes.
  *
  * @author  Martin Desruisseaux (Geomatys)
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
index bea30abf46..fd9a542401 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
@@ -19,7 +19,7 @@ package org.apache.sis.filter.internal.shared;
 import java.util.Optional;
 import java.util.function.Consumer;
 import org.opengis.util.ScopedName;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.util.CodeList;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/package-info.java
index bbc43f568a..5d0794eb45 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/package-info.java
@@ -16,7 +16,7 @@
  */
 
 /**
- * A set of helper classes for the SIS implementation.
+ * A set of helper classes for the <abbr>SIS</abbr> implementation.
  * also contains classes that may move to the public API someday,
  * but are considered not yet ready.
  *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionDescription.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionDescription.java
index 2f60877371..78c8aaf0a3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionDescription.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionDescription.java
@@ -24,6 +24,7 @@ import org.opengis.util.LocalName;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.filter.capability.AvailableFunction;
+import org.apache.sis.pending.jdk.Record;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.iso.Names;
@@ -35,7 +36,7 @@ import org.apache.sis.referencing.NamedIdentifier;
 
 
 /**
- * Description of a SQLMM function with its parameters.
+ * Description of a <abbr>SQLMM</abbr> function with its parameters.
  *
  * @todo Argument descriptions are incomplete. They have no good names,
  *       and the types are missing (they are {@code null}) except for geometry 
types.
@@ -44,7 +45,7 @@ import org.apache.sis.referencing.NamedIdentifier;
  *
  * @see SQLMM#description(Geometries)
  */
-final class FunctionDescription implements AvailableFunction, Serializable {
+final class FunctionDescription extends Record implements AvailableFunction, 
Serializable {
     /**
      * For cross-version compatibility.
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
index c9fe68215e..210d419f2e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
@@ -101,7 +101,7 @@ abstract class GeometryParser<R,G> extends 
GeometryConstructor<R,G> {
                     case ST_BdMPolyFromText: break;
                     default: warning(new 
InvalidFilterValueException(Errors.format(
                                             
Errors.Keys.IllegalArgumentClass_3, inputName(),
-                                            getValueClass(),
+                                            getResultClass(),
                                             
Classes.getClass(library.getGeometry(result)))), true);
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
index 632b473cdf..d0a7a26723 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
@@ -20,7 +20,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.filter.FunctionRegister;
-import org.apache.sis.pending.jdk.JDK16;
+import org.apache.sis.util.collection.Containers;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Expression;
@@ -62,7 +62,7 @@ public final class Registry implements FunctionRegister {
      */
     @Override
     public Collection<String> getNames() {
-        return JDK16.toList(Arrays.stream(SQLMM.values()).map(SQLMM::name));
+        return Containers.derivedList(Arrays.asList(SQLMM.values()), 
SQLMM::name);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
index 64200551e3..87e11fe356 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
@@ -21,7 +21,7 @@ import java.util.Collection;
 import org.opengis.util.LocalName;
 import org.opengis.util.ScopedName;
 import org.apache.sis.filter.Optimization;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.feature.internal.shared.FeatureExpression;
 import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
@@ -43,7 +43,9 @@ import org.opengis.filter.InvalidFilterValueException;
  *
  * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
  */
-abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Object>, Optimization.OnExpression<R,Object> {
+abstract class SpatialFunction<R> extends Node
+        implements FeatureExpression<R, Object>, Optimization.OnExpression<R, 
Object>
+{
     /**
      * For cross-version compatibility.
      */
@@ -78,7 +80,7 @@ abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Ob
      */
     SpatialFunction(final SQLMM operation, final Expression<R,?>[] parameters) 
{
         this.operation = operation;
-        ArgumentChecks.ensureCountBetween("parameters", true,
+        ArgumentChecks.ensureCountBetween("parameters", false,
                 operation.minParamCount, operation.maxParamCount, 
parameters.length);
     }
 
@@ -149,7 +151,7 @@ abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Ob
      * Returns the kind of objects evaluated by this expression.
      */
     @Override
-    public final Class<?> getValueClass() {
+    public final Class<?> getResultClass() {
         return operation.getReturnType(getGeometryLibrary());
     }
 
@@ -160,7 +162,7 @@ abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Ob
     @Override
     @SuppressWarnings("unchecked")
     public final <N> Expression<R,N> toValueType(final Class<N> target) {
-        if (target.isAssignableFrom(getValueClass())) {
+        if (target.isAssignableFrom(getResultClass())) {
             return (Expression<R,N>) this;
         } else {
             throw new 
ClassCastException(Errors.format(Errors.Keys.CanNotConvertValue_2, 
getFunctionName(), target));
@@ -200,6 +202,6 @@ abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Ob
             }
             throw new 
InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
         }
-        return 
addTo.addComputedProperty(addTo.addAttribute(getValueClass()).setName(getFunctionName()),
 false);
+        return 
addTo.addComputedProperty(addTo.addAttribute(getResultClass()).setName(getFunctionName()),
 false);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/package-info.java
index 303c7d65f5..78b67c5ab6 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/package-info.java
@@ -16,7 +16,7 @@
  */
 
 /**
- * Partial implementation of SQLMM operations as filter expressions.
+ * Partial implementation of <abbr>SQLMM</abbr> operations as filter 
expressions.
  * This package supports only for the simplest types (point, line string, 
polygon).
  * Other types (curve, circular string, compound curve, curve polygon, 
triangle,
  * polyhedral surface, TIN, multi curve, multi surface) are not supported.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
index 39c3c62c35..e4dade8c0e 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
@@ -260,7 +260,7 @@ public final class LogicalFilterTest extends TestCase {
 
         final var property = assertInstanceOf(PropertyValue.class, 
optimized.getParameters().get(0));
         assertEquals(String.class, property.getSourceClass());
-        assertEquals(Number.class, property.getValueClass());
+        assertEquals(Number.class, property.getResultClass());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/function/math/RegistryTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/function/math/RegistryTest.java
new file mode 100644
index 0000000000..52f6861c3e
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/function/math/RegistryTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.filter.function.math;
+
+import java.util.Map;
+import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.filter.DefaultFilterFactory;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Tests expressions using mathematical functions.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class RegistryTest {
+    /**
+     * Type of features used for testing.
+     */
+    private final DefaultFeatureType feature;
+
+    /**
+     * Creates a new test case.
+     */
+    public RegistryTest() {
+        feature = new DefaultFeatureType(Map.of(DefaultFeatureType.NAME_KEY, 
"Test"), false, null,
+                new 
DefaultAttributeType<>(Map.of(DefaultAttributeType.NAME_KEY, "value"), 
Double.class, 1, 1, null));
+    }
+
+    /**
+     * Tests {@link Function#ABS}.
+     */
+    @Test
+    public void testAbs() {
+        final var ff = DefaultFilterFactory.forFeatures();
+        final var ex = ff.function("abs", ff.property("value"));
+
+        final var f = feature.newInstance();
+        f.setPropertyValue("value", 12.5);
+        assertEquals(12.5, ex.apply(f));
+        f.setPropertyValue("value", -18.25);
+        assertEquals(18.25, ex.apply(f));
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
index 70fe0ae7e2..59111a9524 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
@@ -28,7 +28,7 @@ import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.filter.Optimization;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.WraparoundMethod;
-import org.apache.sis.filter.internal.Node;
+import org.apache.sis.filter.function.Node;
 import org.apache.sis.geometry.wrapper.Dimensions;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryType;
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ObjectConverters.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ObjectConverters.java
index 5d76ea2a72..bdff2bd324 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ObjectConverters.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ObjectConverters.java
@@ -59,7 +59,7 @@ import org.apache.sis.converter.SystemRegistry;
  *     }
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.3
+ * @version 1.6
  *
  * @see ObjectConverter
  *
@@ -93,12 +93,20 @@ public final class ObjectConverters {
      * @return the converter from the specified source class to the target 
class.
      * @throws UnconvertibleObjectException if no converter is found.
      */
-    public static <S,T> ObjectConverter<? super S, ? extends T> find(final 
Class<S> source, final Class<T> target)
+    @SuppressWarnings("unchecked")
+    public static <S,T> ObjectConverter<? super S, ? extends T> find(
+            final Class<? extends S> source,
+            final Class<?  super  T> target)
             throws UnconvertibleObjectException
     {
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("target", target);
-        return SystemRegistry.INSTANCE.find(source, target);
+        /*
+         * The `(Class<S>)` cast is safe because the generic type of the 
returned converter is `<? super S>`.
+         * Therefore, even if the actual class is a subtype of `S`, the `? 
super S` declaration stay valid.
+         * A similar argument applies also to the `(Class<T>)` cast.
+         */
+        return SystemRegistry.INSTANCE.find((Class<S>) source, (Class<T>) 
target);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/Containers.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/Containers.java
index 5448bb1195..a8110729ba 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/Containers.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/Containers.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.Iterator;
 import java.util.Collection;
 import java.util.Objects;
+import java.util.function.Function;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ObjectConverter;
 import org.apache.sis.util.resources.Errors;
@@ -34,7 +35,7 @@ import 
org.apache.sis.util.internal.shared.UnmodifiableArrayList;
  * in this class implement the {@code CheckedContainer} interface.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.6
  * @since   0.3
  */
 public final class Containers {
@@ -126,7 +127,7 @@ public final class Containers {
      * Returns a set whose elements are derived <i>on-the-fly</i> from the 
given set.
      * Conversions from the original elements to the derived elements are 
performed when needed
      * by invoking the {@link ObjectConverter#apply(Object)} method on the 
given converter.
-     * Those conversions are repeated every time a {@code Set} method is 
invoked; there is no cache.
+     * Those conversions are repeated every time that a {@code Set} method 
needs to access values.
      * Consequently, any change in the original set is immediately visible in 
the derived set,
      * and conversely.
      *
@@ -162,11 +163,39 @@ public final class Containers {
         return DerivedSet.create(storage, converter);
     }
 
+    /**
+     * Returns a list whose elements are derived <i>on-the-fly</i> from the 
given list.
+     * Conversions from the original elements to the derived elements are 
performed when needed
+     * by invoking the {@link Function#apply(Object)} method on the given 
converter.
+     * Those conversions are repeated every time that a {@code List} method 
needs to access values.
+     * Consequently, any change in the original list is immediately visible in 
the derived list.
+     *
+     * <p>The returned list can be serialized if the given list and converter 
are serializable.
+     * The returned list is not synchronized by itself, but is nevertheless 
thread-safe if the
+     * given list (including its iterator) and converter are thread-safe.</p>
+     *
+     * @param  <S>        the type of elements in the storage (original) list.
+     * @param  <E>        the type of elements in the derived list.
+     * @param  storage    the storage list containing the original elements, 
or {@code null}.
+     * @param  converter  the converter from the elements in the storage list 
to the elements in the derived list.
+     * @return a view over the {@code storage} list containing all elements 
converted by the given converter,
+     *         or {@code null} if {@code storage} was null.
+     *
+     * @since 1.6
+     */
+    public static <S,E> List<E> derivedList(final List<S> storage, final 
Function<S,E> converter) {
+        ArgumentChecks.ensureNonNull("converter", converter);
+        if (storage == null) {
+            return null;
+        }
+        return new DerivedList<>(storage, converter);
+    }
+
     /**
      * Returns a map whose keys and values are derived <i>on-the-fly</i> from 
the given map.
      * Conversions from the original entries to the derived entries are 
performed when needed
      * by invoking the {@link ObjectConverter#apply(Object)} method on the 
given converters.
-     * Those conversions are repeated every time a {@code Map} method is 
invoked; there is no cache.
+     * Those conversions are repeated every time that a {@code Map} method 
needs to access values.
      * Consequently, any change in the original map is immediately visible in 
the derived map,
      * and conversely.
      *
diff --git 
a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/DerivedList.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/DerivedList.java
similarity index 82%
rename from 
endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/DerivedList.java
rename to 
endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/DerivedList.java
index 45aff686e6..3639be2413 100644
--- 
a/endorsed/src/org.apache.sis.cloud.aws/main/org/apache/sis/cloud/aws/s3/DerivedList.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/DerivedList.java
@@ -14,8 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.cloud.aws.s3;
+package org.apache.sis.util.collection;
 
+import java.io.Serializable;
 import java.util.List;
 import java.util.AbstractList;
 import java.util.Iterator;
@@ -26,7 +27,7 @@ import java.util.function.Consumer;
 
 /**
  * A list in which values are derived from another list using a given function.
- * The conversion is done the fly every times an element is accessed.
+ * The conversion is done on-the-fly every times that an element is accessed.
  * Consequently, this wrapper should be used only for elements that are cheap 
to wrap.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -34,15 +35,22 @@ import java.util.function.Consumer;
  * @param  <S>  type of elements in the source list.
  * @param  <E>  type of elements in this list.
  */
-final class DerivedList<S,E> extends AbstractList<E> {
+final class DerivedList<S,E> extends AbstractList<E> implements Serializable {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = 5616103170191124327L;
+
     /**
      * The list of source elements.
      */
+    @SuppressWarnings("serial")         // Not statically typed as 
Serializable.
     private final List<S> source;
 
     /**
      * The function for deriving an element in this list from an element in 
the source list.
      */
+    @SuppressWarnings("serial")
     private final Function<S,E> adapter;
 
     /**
@@ -89,8 +97,18 @@ final class DerivedList<S,E> extends AbstractList<E> {
     }
 
     /**
-     * An iterator over the elements in the source list,
-     * converted on-the-fly to elements of type {@code <E>}.
+     * Returns an iterator over the elements in this list.
+     *
+     * @return a new iterator.
+     */
+    @Override
+    public Iterator<E> iterator() {
+        return new Iter<>(source.iterator(), adapter);
+    }
+
+    /**
+     * An iterator over the elements in the source list, converted on-the-fly 
to elements of type {@code <E>}.
+     * Contrarily to {@link DerivedIterator}, this iterator does not skip null 
elements.
      */
     private static final class Iter<S,E> implements Iterator<E> {
         /** The iterator over source elements. */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/package-info.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/package-info.java
index fae84cb6a0..ab988b178c 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/package-info.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/package-info.java
@@ -51,7 +51,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 package org.apache.sis.util.collection;
diff --git a/netbeans-project/nbproject/project.xml 
b/netbeans-project/nbproject/project.xml
index 558c4502b4..bf34c520bf 100644
--- a/netbeans-project/nbproject/project.xml
+++ b/netbeans-project/nbproject/project.xml
@@ -35,8 +35,10 @@
             <word>geospatial</word>
             <word>Molodensky</word>
             <word>namespace</word>
+            <word>nullary</word>
             <word>programmatically</word>
             <word>transformative</word>
+            <word>unary</word>
             <word>untiled</word>
         </spellchecker-wordlist>
     </configuration>


Reply via email to