1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function;
7   
8   import java.util.Locale;
9   
10  import dev.metaschema.core.metapath.DynamicContext;
11  import dev.metaschema.core.metapath.function.impl.OperationFunctions;
12  import dev.metaschema.core.metapath.function.library.FnNot;
13  import dev.metaschema.core.metapath.item.ISequence;
14  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
15  import dev.metaschema.core.metapath.item.atomic.IBase64BinaryItem;
16  import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
17  import dev.metaschema.core.metapath.item.atomic.IDateItem;
18  import dev.metaschema.core.metapath.item.atomic.IDateTimeItem;
19  import dev.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
20  import dev.metaschema.core.metapath.item.atomic.IDecimalItem;
21  import dev.metaschema.core.metapath.item.atomic.IDurationItem;
22  import dev.metaschema.core.metapath.item.atomic.INumericItem;
23  import dev.metaschema.core.metapath.item.atomic.IStringItem;
24  import dev.metaschema.core.metapath.item.atomic.ITimeItem;
25  import dev.metaschema.core.metapath.item.atomic.IUntypedAtomicItem;
26  import dev.metaschema.core.metapath.item.atomic.IYearMonthDurationItem;
27  import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
28  import edu.umd.cs.findbugs.annotations.NonNull;
29  import edu.umd.cs.findbugs.annotations.Nullable;
30  
31  /**
32   * A collection of comparison functions supporting value and general
33   * comparisons.
34   * <p>
35   * Based on the XPath 3.1
36   * <a href="https://www.w3.org/TR/xpath-31/#id-comparisons">comparison
37   * expressions</a> syntax.
38   */
39  // FIXME: Add unit tests
40  @SuppressWarnings({ "PMD.GodClass", "PMD.CyclomaticComplexity" })
41  public final class ComparisonFunctions {
42    /**
43     * Comparison operators.
44     */
45    public enum Operator {
46      /**
47       * An equal comparison.
48       */
49      EQ,
50      /**
51       * A not equal comparison.
52       */
53      NE,
54      /**
55       * A less than comparison.
56       */
57      LT,
58      /**
59       * A less than or equal comparison.
60       */
61      LE,
62      /**
63       * A greater than comparison.
64       */
65      GT,
66      /**
67       * A greater than or equal comparison.
68       */
69      GE;
70    }
71  
72    private ComparisonFunctions() {
73      // disable construction
74    }
75  
76    /**
77     * Compare the two items using the provided {@code operator}.
78     *
79     * @param leftItem
80     *          the first item to compare
81     * @param operator
82     *          the comparison operator
83     * @param rightItem
84     *          the second item to compare
85     * @param dynamicContext
86     *          used to get the implicit timezone from the evaluation context
87     * @return the result of the comparison
88     */
89    @NonNull
90    public static IBooleanItem valueCompairison(
91        @NonNull IAnyAtomicItem leftItem,
92        @NonNull Operator operator,
93        @NonNull IAnyAtomicItem rightItem,
94        @Nullable DynamicContext dynamicContext) {
95      return compare(leftItem, operator, rightItem, dynamicContext);
96    }
97  
98    /**
99     * Compare the sets of atomic items.
100    *
101    * @param leftItems
102    *          the first set of items to compare
103    * @param operator
104    *          the comparison operator
105    * @param rightItems
106    *          the second set of items to compare
107    * @param dynamicContext
108    *          used to get the implicit timezone from the evaluation context
109    * @return a or an empty {@link ISequence} if either item is {@code null}
110    */
111   @NonNull
112   public static IBooleanItem generalComparison( // NOPMD - acceptable complexity
113       @NonNull ISequence<? extends IAnyAtomicItem> leftItems,
114       @NonNull Operator operator,
115       @NonNull ISequence<? extends IAnyAtomicItem> rightItems,
116       @NonNull DynamicContext dynamicContext) {
117 
118     IBooleanItem retval = IBooleanItem.FALSE;
119     for (IAnyAtomicItem left : leftItems) {
120       assert left != null;
121       for (IAnyAtomicItem right : rightItems) {
122         assert right != null;
123         IAnyAtomicItem leftCast;
124         IAnyAtomicItem rightCast;
125         if (left instanceof IUntypedAtomicItem) {
126           if (right instanceof IUntypedAtomicItem) {
127             leftCast = IStringItem.cast(left);
128             rightCast = IStringItem.cast(right);
129           } else {
130             leftCast = applyGeneralComparisonCast(right, left);
131             rightCast = right;
132           }
133         } else if (right instanceof IUntypedAtomicItem) {
134           leftCast = left;
135           rightCast = applyGeneralComparisonCast(left, right);
136         } else {
137           leftCast = left;
138           rightCast = right;
139         }
140 
141         assert leftCast != null;
142         IBooleanItem result = compare(leftCast, operator, rightCast, dynamicContext);
143         if (IBooleanItem.TRUE.equals(result)) {
144           retval = IBooleanItem.TRUE;
145         }
146       }
147     }
148     return retval;
149   }
150 
151   /**
152    * Attempts to cast the provided {@code other} item to the type of the
153    * {@code item}.
154    *
155    * @param item
156    *          the item whose type the other item is to be cast to
157    * @param other
158    *          the item to cast
159    * @return the casted item
160    */
161   @NonNull
162   private static IAnyAtomicItem applyGeneralComparisonCast(
163       @NonNull IAnyAtomicItem item,
164       @NonNull IAnyAtomicItem other) {
165     IAnyAtomicItem retval;
166     if (item instanceof INumericItem) {
167       retval = IDecimalItem.cast(other);
168     } else if (item instanceof IDayTimeDurationItem) {
169       retval = IDayTimeDurationItem.cast(other);
170     } else if (item instanceof IYearMonthDurationItem) {
171       retval = IYearMonthDurationItem.cast(other);
172     } else {
173       retval = item.castAsType(other);
174     }
175     return retval;
176   }
177 
178   /**
179    * Compare the {@code right} item with the {@code left} item using the specified
180    * {@code operator}.
181    *
182    * @param left
183    *          the value to compare against
184    * @param operator
185    *          the comparison operator
186    * @param right
187    *          the value to compare with
188    * @param dynamicContext
189    *          used to get the implicit timezone from the evaluation context
190    * @return the comparison result
191    */
192   @NonNull
193   public static IBooleanItem compare( // NOPMD - unavoidable
194       @NonNull IAnyAtomicItem left,
195       @NonNull Operator operator,
196       @NonNull IAnyAtomicItem right,
197       @Nullable DynamicContext dynamicContext) {
198     @NonNull
199     IBooleanItem retval;
200     if (left instanceof IStringItem || right instanceof IStringItem) {
201       retval = stringCompare(IStringItem.cast(left), operator, IStringItem.cast(right));
202     } else if (left instanceof INumericItem && right instanceof INumericItem) {
203       retval = numericCompare((INumericItem) left, operator, (INumericItem) right);
204     } else if (left instanceof IBooleanItem && right instanceof IBooleanItem) {
205       retval = booleanCompare((IBooleanItem) left, operator, (IBooleanItem) right);
206     } else if (left instanceof IDateTimeItem && right instanceof IDateTimeItem) {
207       retval = dateTimeCompare((IDateTimeItem) left, operator, (IDateTimeItem) right, dynamicContext);
208     } else if (left instanceof IDateItem && right instanceof IDateItem) {
209       retval = dateTimeCompare(
210           ((IDateItem) left).asDateTime(),
211           operator,
212           ((IDateItem) right).asDateTime(),
213           dynamicContext);
214     } else if (left instanceof ITimeItem && right instanceof ITimeItem) {
215       retval = dateTimeCompare(
216           IDateTimeItem.valueOf(OperationFunctions.referenceDate(), (ITimeItem) left),
217           operator,
218           IDateTimeItem.valueOf(OperationFunctions.referenceDate(), (ITimeItem) right),
219           dynamicContext);
220     } else if (left instanceof IDurationItem && right instanceof IDurationItem) {
221       retval = durationCompare((IDurationItem) left, operator, (IDurationItem) right);
222     } else if (left instanceof IBase64BinaryItem && right instanceof IBase64BinaryItem) {
223       retval = binaryCompare((IBase64BinaryItem) left, operator, (IBase64BinaryItem) right);
224     } else {
225       throw new InvalidTypeMetapathException(
226           null,
227           String.format("invalid types for comparison: %s %s %s", left.getClass().getName(),
228               operator.name().toLowerCase(Locale.ROOT), right.getClass().getName()));
229     }
230     return retval;
231   }
232 
233   /**
234    * Perform a string-based comparison of the {@code right} item against the
235    * {@code left} item using the specified {@code operator}.
236    *
237    * @param left
238    *          the value to compare against
239    * @param operator
240    *          the comparison operator
241    * @param right
242    *          the value to compare with
243    * @return the comparison result
244    */
245   @NonNull
246   public static IBooleanItem stringCompare(
247       @NonNull IStringItem left,
248       @NonNull Operator operator,
249       @NonNull IStringItem right) {
250     int result = left.compareTo(right);
251     Boolean retval = null;
252     switch (operator) {
253     case EQ:
254       retval = result == 0;
255       break;
256     case GE:
257       retval = result >= 0;
258       break;
259     case GT:
260       retval = result > 0;
261       break;
262     case LE:
263       retval = result <= 0;
264       break;
265     case LT:
266       retval = result < 0;
267       break;
268     case NE:
269       retval = result != 0;
270       break;
271     }
272 
273     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
274 
275     return IBooleanItem.valueOf(retval);
276   }
277 
278   /**
279    * Perform a number-based comparison of the {@code right} item against the
280    * {@code left} item using the specified {@code operator}.
281    *
282    * @param left
283    *          the value to compare against
284    * @param operator
285    *          the comparison operator
286    * @param right
287    *          the value to compare with
288    * @return the comparison result
289    */
290   @NonNull
291   public static IBooleanItem numericCompare(@NonNull INumericItem left, @NonNull Operator operator,
292       @NonNull INumericItem right) {
293     IBooleanItem retval = null;
294     switch (operator) {
295     case EQ:
296       retval = OperationFunctions.opNumericEqual(left, right);
297       break;
298     case GE: {
299       IBooleanItem gt = OperationFunctions.opNumericGreaterThan(left, right);
300       IBooleanItem eq = OperationFunctions.opNumericEqual(left, right);
301       retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
302       break;
303     }
304     case GT:
305       retval = OperationFunctions.opNumericGreaterThan(left, right);
306       break;
307     case LE: {
308       IBooleanItem lt = OperationFunctions.opNumericLessThan(left, right);
309       IBooleanItem eq = OperationFunctions.opNumericEqual(left, right);
310       retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
311       break;
312     }
313     case LT:
314       retval = OperationFunctions.opNumericLessThan(left, right);
315       break;
316     case NE:
317       retval = FnNot.fnNot(OperationFunctions.opNumericEqual(left, right));
318       break;
319     }
320     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
321 
322     return retval;
323   }
324 
325   /**
326    * Perform a boolean-based comparison of the {@code right} item against the
327    * {@code left} item using the specified {@code operator}.
328    *
329    * @param left
330    *          the value to compare against
331    * @param operator
332    *          the comparison operator
333    * @param right
334    *          the value to compare with
335    * @return the comparison result
336    */
337   @NonNull
338   public static IBooleanItem booleanCompare(
339       @NonNull IBooleanItem left,
340       @NonNull Operator operator,
341       @NonNull IBooleanItem right) {
342     IBooleanItem retval = null;
343     switch (operator) {
344     case EQ:
345       retval = OperationFunctions.opBooleanEqual(left, right);
346       break;
347     case GE: {
348       IBooleanItem gt = OperationFunctions.opBooleanGreaterThan(left, right);
349       IBooleanItem eq = OperationFunctions.opBooleanEqual(left, right);
350       retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
351       break;
352     }
353     case GT:
354       retval = OperationFunctions.opBooleanGreaterThan(left, right);
355       break;
356     case LE: {
357       IBooleanItem lt = OperationFunctions.opBooleanLessThan(left, right);
358       IBooleanItem eq = OperationFunctions.opBooleanEqual(left, right);
359       retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
360       break;
361     }
362     case LT:
363       retval = OperationFunctions.opBooleanLessThan(left, right);
364       break;
365     case NE:
366       retval = FnNot.fnNot(OperationFunctions.opBooleanEqual(left, right));
367       break;
368     }
369     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
370 
371     return retval;
372   }
373 
374   /**
375    * Perform a date and time-based comparison of the {@code right} item against
376    * the {@code left} item using the specified {@code operator}.
377    *
378    * @param left
379    *          the value to compare against
380    * @param operator
381    *          the comparison operator
382    * @param right
383    *          the value to compare with
384    * @param dynamicContext
385    *          used to get the implicit timezone from the evaluation context
386    * @return the comparison result
387    */
388   @NonNull
389   public static IBooleanItem dateTimeCompare(
390       @NonNull IDateTimeItem left,
391       @NonNull Operator operator,
392       @NonNull IDateTimeItem right,
393       @Nullable DynamicContext dynamicContext) {
394     IBooleanItem retval = null;
395     switch (operator) {
396     case EQ:
397       retval = OperationFunctions.opDateTimeEqual(left, right, dynamicContext);
398       break;
399     case GE: {
400       // pre-normalize for efficiency
401       IDateTimeItem leftNormalized = dynamicContext == null ? left : left.normalize(dynamicContext);
402       IDateTimeItem rightNormalized = dynamicContext == null ? right : right.normalize(dynamicContext);
403       retval = IBooleanItem.valueOf(
404           OperationFunctions.opDateTimeGreaterThan(leftNormalized, rightNormalized, null).toBoolean()
405               || OperationFunctions.opDateTimeEqual(leftNormalized, rightNormalized, null).toBoolean());
406       break;
407     }
408     case GT:
409       retval = OperationFunctions.opDateTimeGreaterThan(left, right, dynamicContext);
410       break;
411     case LE: {
412       // pre-normalize for efficiency
413       IDateTimeItem leftNormalized = dynamicContext == null ? left : left.normalize(dynamicContext);
414       IDateTimeItem rightNormalized = dynamicContext == null ? right : right.normalize(dynamicContext);
415       retval = IBooleanItem.valueOf(
416           OperationFunctions.opDateTimeLessThan(leftNormalized, rightNormalized, null).toBoolean()
417               || OperationFunctions.opDateTimeEqual(leftNormalized, rightNormalized, null).toBoolean());
418       break;
419     }
420     case LT:
421       retval = OperationFunctions.opDateTimeLessThan(left, right, dynamicContext);
422       break;
423     case NE:
424       retval = FnNot.fnNot(OperationFunctions.opDateTimeEqual(left, right, dynamicContext));
425       break;
426     }
427     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
428 
429     return retval;
430   }
431 
432   /**
433    * Perform a duration-based comparison of the {@code right} item against the
434    * {@code left} item using the specified {@code operator}.
435    *
436    * @param left
437    *          the value to compare against
438    * @param operator
439    *          the comparison operator
440    * @param right
441    *          the value to compare with
442    * @return the comparison result
443    */
444   @NonNull
445   public static IBooleanItem durationCompare( // NOPMD - unavoidable
446       @NonNull IDurationItem left,
447       @NonNull Operator operator,
448       @NonNull IDurationItem right) {
449     IBooleanItem retval = null;
450     switch (operator) {
451     case EQ:
452       retval = OperationFunctions.opDurationEqual(left, right);
453       break;
454     case GE:
455       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
456         IBooleanItem gt = OperationFunctions.opYearMonthDurationGreaterThan(
457             (IYearMonthDurationItem) left,
458             (IYearMonthDurationItem) right);
459         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
460         retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
461       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
462         IBooleanItem gt = OperationFunctions.opDayTimeDurationGreaterThan(
463             (IDayTimeDurationItem) left,
464             (IDayTimeDurationItem) right);
465         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
466         retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
467       }
468       break;
469     case GT:
470       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
471         retval = OperationFunctions.opYearMonthDurationGreaterThan(
472             (IYearMonthDurationItem) left,
473             (IYearMonthDurationItem) right);
474       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
475         retval = OperationFunctions.opDayTimeDurationGreaterThan(
476             (IDayTimeDurationItem) left,
477             (IDayTimeDurationItem) right);
478       }
479       break;
480     case LE:
481       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
482         IBooleanItem lt = OperationFunctions.opYearMonthDurationLessThan(
483             (IYearMonthDurationItem) left,
484             (IYearMonthDurationItem) right);
485         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
486         retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
487       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
488         IBooleanItem lt = OperationFunctions.opDayTimeDurationLessThan(
489             (IDayTimeDurationItem) left,
490             (IDayTimeDurationItem) right);
491         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
492         retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
493       }
494       break;
495     case LT:
496       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
497         retval = OperationFunctions.opYearMonthDurationLessThan(
498             (IYearMonthDurationItem) left,
499             (IYearMonthDurationItem) right);
500       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
501         retval = OperationFunctions.opDayTimeDurationLessThan(
502             (IDayTimeDurationItem) left,
503             (IDayTimeDurationItem) right);
504       }
505       break;
506     case NE:
507       retval = FnNot.fnNot(OperationFunctions.opDurationEqual(left, right));
508       break;
509     }
510 
511     if (retval == null) {
512       throw new InvalidTypeMetapathException(
513           null,
514           String.format("The item types '%s' and '%s' are not comparable",
515               left.getClass().getName(),
516               right.getClass().getName()));
517     }
518     return retval;
519   }
520 
521   /**
522    * Perform a binary data-based comparison of the {@code right} item against the
523    * {@code left} item using the specified {@code operator}.
524    *
525    * @param left
526    *          the value to compare against
527    * @param operator
528    *          the comparison operator
529    * @param right
530    *          the value to compare with
531    * @return the comparison result
532    */
533   @NonNull
534   public static IBooleanItem binaryCompare(@NonNull IBase64BinaryItem left, @NonNull Operator operator,
535       @NonNull IBase64BinaryItem right) {
536     IBooleanItem retval = null;
537     switch (operator) {
538     case EQ:
539       retval = OperationFunctions.opBase64BinaryEqual(left, right);
540       break;
541     case GE: {
542       IBooleanItem gt = OperationFunctions.opBase64BinaryGreaterThan(left, right);
543       IBooleanItem eq = OperationFunctions.opBase64BinaryEqual(left, right);
544       retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
545       break;
546     }
547     case GT:
548       retval = OperationFunctions.opBase64BinaryGreaterThan(left, right);
549       break;
550     case LE: {
551       IBooleanItem lt = OperationFunctions.opBase64BinaryLessThan(left, right);
552       IBooleanItem eq = OperationFunctions.opBase64BinaryEqual(left, right);
553       retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
554       break;
555     }
556     case LT:
557       retval = OperationFunctions.opBase64BinaryLessThan(left, right);
558       break;
559     case NE:
560       retval = FnNot.fnNot(OperationFunctions.opBase64BinaryEqual(left, right));
561       break;
562     }
563     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
564 
565     return retval;
566   }
567 }