1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function.library;
7   
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  import java.util.stream.Collectors;
12  import java.util.stream.Stream;
13  
14  import dev.metaschema.core.metapath.DynamicContext;
15  import dev.metaschema.core.metapath.MetapathConstants;
16  import dev.metaschema.core.metapath.function.ComparisonFunctions;
17  import dev.metaschema.core.metapath.function.FunctionUtils;
18  import dev.metaschema.core.metapath.function.IArgument;
19  import dev.metaschema.core.metapath.function.IFunction;
20  import dev.metaschema.core.metapath.function.InvalidArgumentFunctionException;
21  import dev.metaschema.core.metapath.item.IItem;
22  import dev.metaschema.core.metapath.item.ISequence;
23  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
24  import dev.metaschema.core.metapath.item.atomic.IAnyUriItem;
25  import dev.metaschema.core.metapath.item.atomic.IBase64BinaryItem;
26  import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
27  import dev.metaschema.core.metapath.item.atomic.IDateItem;
28  import dev.metaschema.core.metapath.item.atomic.IDateTimeItem;
29  import dev.metaschema.core.metapath.item.atomic.IDecimalItem;
30  import dev.metaschema.core.metapath.item.atomic.IDurationItem;
31  import dev.metaschema.core.metapath.item.atomic.IStringItem;
32  import dev.metaschema.core.metapath.item.atomic.IUntypedAtomicItem;
33  import dev.metaschema.core.util.ObjectUtils;
34  import edu.umd.cs.findbugs.annotations.NonNull;
35  import edu.umd.cs.findbugs.annotations.Nullable;
36  
37  /**
38   * Implements the XPath 3.1
39   * <a href="https://www.w3.org/TR/xpath-functions-31/#func-min">fn:min</a> and
40   * <a href="https://www.w3.org/TR/xpath-functions-31/#func-max">fn:max</a>
41   * functions.
42   */
43  public final class FnMinMax {
44    private static final String NAME_MIN = "min";
45    private static final String NAME_MAX = "max";
46    /**
47     * Defines the set of primitive atomic item types supported by the min/max
48     * functions. This set is used for type validation and normalization during
49     * comparison operations.
50     */
51    @NonNull
52    private static final Set<Class<? extends IAnyAtomicItem>> PRIMITIVE_ITEM_TYPES = ObjectUtils.notNull(Set.of(
53        IStringItem.class,
54        IBooleanItem.class,
55        IDecimalItem.class,
56        IDurationItem.class,
57        IDateTimeItem.class,
58        IDateItem.class,
59        IBase64BinaryItem.class,
60        IAnyUriItem.class));
61    @NonNull
62    static final IFunction SIGNATURE_MIN = IFunction.builder()
63        .name(NAME_MIN)
64        .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
65        .deterministic()
66        .contextIndependent()
67        .focusIndependent()
68        .argument(IArgument.builder()
69            .name("arg")
70            .type(IAnyAtomicItem.type())
71            .zeroOrMore()
72            .build())
73        .returnType(IAnyAtomicItem.type())
74        .returnZeroOrOne()
75        .functionHandler(FnMinMax::executeMin)
76        .build();
77  
78    @NonNull
79    static final IFunction SIGNATURE_MAX = IFunction.builder()
80        .name(NAME_MAX)
81        .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
82        .deterministic()
83        .contextIndependent()
84        .focusIndependent()
85        .argument(IArgument.builder()
86            .name("arg")
87            .type(IAnyAtomicItem.type())
88            .zeroOrMore()
89            .build())
90        .returnType(IAnyAtomicItem.type())
91        .returnZeroOrOne()
92        .functionHandler(FnMinMax::executeMax)
93        .build();
94  
95    private FnMinMax() {
96      // disable construction
97    }
98  
99    @SuppressWarnings("unused")
100   @NonNull
101   private static ISequence<IAnyAtomicItem> executeMin(
102       @NonNull IFunction function,
103       @NonNull List<ISequence<?>> arguments,
104       @NonNull DynamicContext dynamicContext,
105       IItem focus) {
106     ISequence<? extends IAnyAtomicItem> sequence = FunctionUtils.asType(
107         ObjectUtils.requireNonNull(arguments.get(0)));
108 
109     return ISequence.of(min(sequence));
110   }
111 
112   @SuppressWarnings("unused")
113   @NonNull
114   private static ISequence<IAnyAtomicItem> executeMax(
115       @NonNull IFunction function,
116       @NonNull List<ISequence<?>> arguments,
117       @NonNull DynamicContext dynamicContext,
118       IItem focus) {
119     ISequence<? extends IAnyAtomicItem> sequence = FunctionUtils.asType(
120         ObjectUtils.requireNonNull(arguments.get(0)));
121 
122     return ISequence.of(max(sequence));
123   }
124 
125   /**
126    * An implementation of XPath 3.1
127    * <a href="https://www.w3.org/TR/xpath-functions-31/#func-min">fn:min</a>.
128    *
129    * @param items
130    *          the items to find the minimum value for
131    * @return the average
132    */
133   @Nullable
134   public static IAnyAtomicItem min(@NonNull List<? extends IAnyAtomicItem> items) {
135     // FIXME: support implicit timezone
136     return normalize(items)
137         .reduce(null, (item1, item2) -> {
138           // FIXME: figure out a better way to handle implicit namespaces
139           return item1 != null && ComparisonFunctions.valueCompairison(
140               item1,
141               ComparisonFunctions.Operator.LE,
142               ObjectUtils.notNull(item2),
143               new DynamicContext()).toBoolean()
144                   ? item1
145                   : item2;
146         });
147   }
148 
149   /**
150    * An implementation of XPath 3.1
151    * <a href="https://www.w3.org/TR/xpath-functions-31/#func-max">fn:max</a>.
152    *
153    * @param items
154    *          the items to find the maximum value for
155    * @return the average
156    */
157   @Nullable
158   public static IAnyAtomicItem max(@NonNull List<? extends IAnyAtomicItem> items) {
159     // FIXME: support implicit timezone
160     return normalize(items)
161         .reduce(null, (item1, item2) -> {
162           // FIXME: figure out a better way to handle implicit namespaces
163           return item1 != null && ComparisonFunctions.valueCompairison(
164               item1,
165               ComparisonFunctions.Operator.GE,
166               ObjectUtils.notNull(item2),
167               new DynamicContext()).toBoolean()
168                   ? item1
169                   : item2;
170         });
171   }
172 
173   private static Stream<? extends IAnyAtomicItem> normalize(
174       @NonNull List<? extends IAnyAtomicItem> items) {
175     if (items.isEmpty()) {
176       return Stream.empty();
177     }
178 
179     if (items.size() == 1) {
180       return Stream.of(items.get(0));
181     }
182 
183     List<? extends IAnyAtomicItem> resultingItems = convertUntypedItems(items);
184     Map<Class<? extends IAnyAtomicItem>, Integer> counts = countItemTypes(resultingItems);
185     return createNormalizedStream(resultingItems, counts);
186   }
187 
188   @NonNull
189   private static List<? extends IAnyAtomicItem> convertUntypedItems(
190       @NonNull List<? extends IAnyAtomicItem> items) {
191     return ObjectUtils.notNull(items.stream()
192         .map(item -> item instanceof IUntypedAtomicItem ? IDecimalItem.cast(item) : item)
193         .collect(Collectors.toList()));
194   }
195 
196   @SuppressWarnings("unchecked")
197   @NonNull
198   private static Map<Class<? extends IAnyAtomicItem>, Integer> countItemTypes(
199       @NonNull List<? extends IAnyAtomicItem> items) {
200     return ISequence.ofCollection((List<IAnyAtomicItem>) items).countTypes(PRIMITIVE_ITEM_TYPES);
201   }
202 
203   @NonNull
204   private static Stream<? extends IAnyAtomicItem> createNormalizedStream(
205       @NonNull List<? extends IAnyAtomicItem> items,
206       @NonNull Map<Class<? extends IAnyAtomicItem>, Integer> counts) {
207 
208     // Single type - no conversion needed
209     if (counts.size() == 1) {
210       return ObjectUtils.notNull(items.stream());
211     }
212 
213     // Multiple types - attempt conversion
214     int size = items.size();
215     if (counts.size() > 1) {
216       // Check if all items are either String or AnyUri
217       if (counts.getOrDefault(IStringItem.class, 0) + counts.getOrDefault(IAnyUriItem.class, 0) == size) {
218         return ObjectUtils.notNull(items.stream().map(IAnyAtomicItem::asStringItem));
219       }
220 
221       // Check if all items are Decimal
222       if (counts.getOrDefault(IDecimalItem.class, 0) == size) {
223         return ObjectUtils.notNull(items.stream().map(item -> (IDecimalItem) item));
224       }
225     }
226 
227     // No valid conversion possible
228     @SuppressWarnings("unchecked")
229     List<IAnyAtomicItem> itemList = (List<IAnyAtomicItem>) items;
230     throw new InvalidArgumentFunctionException(
231         InvalidArgumentFunctionException.INVALID_ARGUMENT_TYPE,
232         String.format(
233             "Values must all be of a single atomic type. Found multiple types: [%s]",
234             ISequence.ofCollection(itemList).getItemTypes().stream()
235                 .map(Class::getSimpleName)
236                 .collect(Collectors.joining(", "))));
237   }
238 }