diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java index e6520ca76..9eb4b0922 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java @@ -64,7 +64,8 @@ public DefaultFunctionLibrary() { // NOPMD - intentional // https://www.w3.org/TR/xpath-functions-31/#func-days-from-duration // https://www.w3.org/TR/xpath-functions-31/#func-deep-equal registerFunction(FnDeepEqual.SIGNATURE_TWO_ARG); - // P1: https://www.w3.org/TR/xpath-functions-31/#func-distinct-values + // https://www.w3.org/TR/xpath-functions-31/#func-distinct-values + registerFunction(FnDistinctValues.SIGNATURE_ONE_ARG); // https://www.w3.org/TR/xpath-functions-31/#func-doc registerFunction(FnDoc.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-doc-available diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnDistinctValues.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnDistinctValues.java new file mode 100644 index 000000000..ce6ce60da --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnDistinctValues.java @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import gov.nist.secauto.metaschema.core.metapath.DynamicContext; +import gov.nist.secauto.metaschema.core.metapath.MetapathConstants; +import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils; +import gov.nist.secauto.metaschema.core.metapath.function.IArgument; +import gov.nist.secauto.metaschema.core.metapath.function.IFunction; +import gov.nist.secauto.metaschema.core.metapath.function.InvalidTypeFunctionException; +import gov.nist.secauto.metaschema.core.metapath.function.InvalidValueForCastFunctionException; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * /** Implements fn:distinct-values + * functions. This implementation does not implement the two-arg variant with + * collation at this time. + */ +public final class FnDistinctValues { + @NonNull + private static final String NAME = "distinct-values"; + @NonNull + static final IFunction SIGNATURE_ONE_ARG = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS) + .deterministic() + .contextDependent() + .focusIndependent() + .argument(IArgument.builder() + .name("arg") + .type(IAnyAtomicItem.type()) + .zeroOrMore() + .build()) + .returnType(IAnyAtomicItem.type()) + .returnZeroOrMore() + .functionHandler(FnDistinctValues::executeOneArg) + .build(); + + @SuppressWarnings("unused") + @NonNull + private static ISequence executeOneArg(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + ISequence seq = FunctionUtils.asType(ObjectUtils.requireNonNull(arguments.get(0))); + + return ISequence.of(fnDistinctValues(seq)); + } + + /** + * Get the first occurrence of each distinct value in the provided list. + *

+ * Based on the XPath 3.1 fn:distinct-values + * function. + * + * @param values + * the items to get destinct values for + * @return a the list of distinct values + */ + @NonNull + public static Stream fnDistinctValues(@NonNull List values) { + Set distinctValues = new TreeSet<>(FnDistinctValues::compare); + distinctValues.addAll(values); + return ObjectUtils.notNull(distinctValues.stream()); + } + + private static int compare(@NonNull IAnyAtomicItem item1, @NonNull IAnyAtomicItem item2) { + int retval; + try { + retval = item1.compareTo(item2); + } catch (InvalidTypeFunctionException | InvalidValueForCastFunctionException ex) { + retval = 1; + } + return retval; + } + + private FnDistinctValues() { + // disable construction + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnDistinctValuesTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnDistinctValuesTest.java new file mode 100644 index 000000000..49a3fc468 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnDistinctValuesTest.java @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.decimal; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.integer; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.sequence; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.string; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase; +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +class FnDistinctValuesTest + extends ExpressionTestBase { + private static Stream provideValues() { + return Stream.of( + Arguments.of( + sequence(integer(1), decimal(2.0), integer(3)), + "fn:distinct-values((1, 2.0, 3, 2))"), + Arguments.of( + sequence(string("cherry"), string("plum")), + "fn:distinct-values((meta:string('cherry'),meta:string('plum'),meta:string('plum')))"), + Arguments.of( + sequence(string("a"), integer(2)), + "fn:distinct-values(('a', 2, 'a', 2.0))")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void test(@NonNull ISequence expected, @NonNull String metapath) { + assertEquals(expected, IMetapathExpression.compile(metapath) + .evaluate(null, newDynamicContext())); + } +}