1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function.impl;
7   
8   import java.time.Duration;
9   import java.time.LocalDate;
10  import java.time.OffsetTime;
11  import java.time.Period;
12  import java.time.ZonedDateTime;
13  import java.time.temporal.Temporal;
14  import java.util.Set;
15  
16  import dev.metaschema.core.metapath.DynamicContext;
17  import dev.metaschema.core.metapath.function.ArithmeticFunctionException;
18  import dev.metaschema.core.metapath.function.CastFunctionException;
19  import dev.metaschema.core.metapath.function.DateTimeFunctionException;
20  import dev.metaschema.core.metapath.function.library.FnDateTime;
21  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
22  import dev.metaschema.core.metapath.item.atomic.IAnyUriItem;
23  import dev.metaschema.core.metapath.item.atomic.IBase64BinaryItem;
24  import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
25  import dev.metaschema.core.metapath.item.atomic.IDateItem;
26  import dev.metaschema.core.metapath.item.atomic.IDateTimeItem;
27  import dev.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
28  import dev.metaschema.core.metapath.item.atomic.IDecimalItem;
29  import dev.metaschema.core.metapath.item.atomic.IDurationItem;
30  import dev.metaschema.core.metapath.item.atomic.IIntegerItem;
31  import dev.metaschema.core.metapath.item.atomic.INumericItem;
32  import dev.metaschema.core.metapath.item.atomic.IStringItem;
33  import dev.metaschema.core.metapath.item.atomic.ITimeItem;
34  import dev.metaschema.core.metapath.item.atomic.IUntypedAtomicItem;
35  import dev.metaschema.core.metapath.item.atomic.IYearMonthDurationItem;
36  import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
37  import dev.metaschema.core.util.ObjectUtils;
38  import edu.umd.cs.findbugs.annotations.NonNull;
39  import edu.umd.cs.findbugs.annotations.Nullable;
40  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
41  
42  /**
43   * Implementations of the XPath 3.1 operation functions.
44   */
45  @SuppressWarnings({
46      // FIXME: break out methods into separate classes organized as in XPath
47      "PMD.CouplingBetweenObjects",
48      "PMD.ExcessivePublicCount",
49      "PMD.CyclomaticComplexity"
50  })
51  public final class OperationFunctions {
52    @SuppressFBWarnings(value = "MS_EXPOSE_REP", justification = "Value is immutable")
53    @NonNull
54    private static final IDateItem DATE_1972_12_31 = IDateItem.valueOf(ObjectUtils.notNull(LocalDate.of(1972, 12, 31)));
55    /**
56     * Identifies the types and substypes that support aggregation.
57     */
58    @SuppressFBWarnings(value = "MS_EXPOSE_REP", justification = "Value is immutable")
59    @NonNull
60    private static final Set<Class<? extends IAnyAtomicItem>> AGGREGATE_MATH_TYPES = ObjectUtils.notNull(Set.of(
61        IDayTimeDurationItem.class,
62        IYearMonthDurationItem.class,
63        INumericItem.class));
64  
65    /**
66     * Get a reference date for use in date-based operations.
67     *
68     * @return the reference date
69     */
70    @NonNull
71    public static IDateItem referenceDate() {
72      return DATE_1972_12_31;
73    }
74  
75    /**
76     * Get the types supported by aggregation operations.
77     * <p>
78     * Any associated subtypes are also supported.
79     *
80     * @return the set of supported types
81     */
82    @NonNull
83    public static Set<Class<? extends IAnyAtomicItem>> aggregateMathTypes() {
84      return AGGREGATE_MATH_TYPES;
85    }
86  
87    private OperationFunctions() {
88      // disable
89    }
90  
91    /**
92     * Based on XPath 3.1 <a href=
93     * "https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDuration-to-date">op:add-yearMonthDuration-to-date</a>.
94     *
95     * @param instant
96     *          a point in time
97     * @param duration
98     *          the duration to add
99     * @return the result of adding the duration to the date
100    */
101   @NonNull
102   public static IDateItem opAddYearMonthDurationToDate(@NonNull IDateItem instant,
103       @NonNull IYearMonthDurationItem duration) {
104     return opAddYearMonthDurationToDateTime(instant.asDateTime(), duration).asDate();
105   }
106 
107   /**
108    * Based on XPath 3.1 <a href=
109    * "https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-date">op:add-dayTimeDuration-to-date</a>.
110    *
111    * @param instant
112    *          a point in time
113    * @param duration
114    *          the duration to add
115    * @return the result of adding the duration to the date
116    */
117   @NonNull
118   public static IDateItem opAddDayTimeDurationToDate(
119       @NonNull IDateItem instant,
120       @NonNull IDayTimeDurationItem duration) {
121     return opAddDayTimeDurationToDateTime(instant.asDateTime(), duration).asDate();
122   }
123 
124   /**
125    * Based on XPath 3.1 <a href=
126    * "https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDurations">op:add-yearMonthDurations</a>.
127    *
128    * @param arg1
129    *          the first duration
130    * @param arg2
131    *          the second duration
132    * @return the sum of two duration values
133    */
134   @NonNull
135   public static IYearMonthDurationItem opAddYearMonthDurations(
136       @NonNull IYearMonthDurationItem arg1,
137       @NonNull IYearMonthDurationItem arg2) {
138     Period duration1 = arg1.asPeriod();
139     Period duration2 = arg2.asPeriod();
140 
141     Period result;
142     try {
143       result = duration1.plus(duration2);
144     } catch (ArithmeticException ex) {
145       throw new DateTimeFunctionException(DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR, ex);
146     }
147     assert result != null;
148     return IYearMonthDurationItem.valueOf(result);
149   }
150 
151   /**
152    * Based on XPath 3.1 <a href=
153    * "https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDurations">op:add-dayTimeDurations</a>.
154    *
155    * @param arg1
156    *          the first duration
157    * @param arg2
158    *          the second duration
159    * @return the sum of two duration values
160    */
161   @NonNull
162   public static IDayTimeDurationItem opAddDayTimeDurations(
163       @NonNull IDayTimeDurationItem arg1,
164       @NonNull IDayTimeDurationItem arg2) {
165     Duration duration1 = arg1.asDuration();
166     Duration duration2 = arg2.asDuration();
167 
168     Duration result;
169     try {
170       result = duration1.plus(duration2);
171     } catch (ArithmeticException ex) {
172       throw new DateTimeFunctionException(DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR, ex);
173     }
174     assert result != null;
175     return IDayTimeDurationItem.valueOf(result);
176   }
177 
178   /**
179    * Based on XPath 3.1 <a href=
180    * "https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDuration-to-dateTime">op:add-yearMonthDuration-to-dateTime</a>.
181    *
182    * @param instant
183    *          a point in time
184    * @param duration
185    *          the duration to add
186    * @return the result of adding the duration to the date
187    */
188   @NonNull
189   public static IDateTimeItem opAddYearMonthDurationToDateTime(
190       @NonNull IDateTimeItem instant,
191       @NonNull IYearMonthDurationItem duration) {
192     ZonedDateTime result;
193     try {
194       result = instant.asZonedDateTime().plus(duration.asPeriod());
195     } catch (ArithmeticException ex) {
196       throw new ArithmeticFunctionException(ArithmeticFunctionException.OVERFLOW_UNDERFLOW_ERROR, ex);
197     }
198     assert result != null;
199     // preserve the same timezone "presence"
200     return IDateTimeItem.valueOf(result, instant.hasTimezone());
201   }
202 
203   /**
204    * Based on XPath 3.1 <a href=
205    * "https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-dateTime">op:add-dayTimeDuration-to-dateTime</a>.
206    *
207    * @param instant
208    *          a point in time
209    * @param duration
210    *          the duration to add
211    * @return the result of adding the duration to the date
212    */
213   @NonNull
214   public static IDateTimeItem opAddDayTimeDurationToDateTime(
215       @NonNull IDateTimeItem instant,
216       @NonNull IDayTimeDurationItem duration) {
217     ZonedDateTime result;
218     try {
219       result = instant.asZonedDateTime().plus(duration.asDuration());
220     } catch (ArithmeticException ex) {
221       throw new ArithmeticFunctionException(ArithmeticFunctionException.OVERFLOW_UNDERFLOW_ERROR, ex);
222     }
223     assert result != null;
224     // preserve the same timezone "presence"
225     return IDateTimeItem.valueOf(result, instant.hasTimezone());
226   }
227 
228   /**
229    * Based on XPath 3.1 <a href=
230    * "https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-time">op:add-dayTimeDuration-to-time</a>.
231    *
232    * @param instant
233    *          a point in time
234    * @param duration
235    *          the duration to add
236    * @return the result of adding the duration to the date
237    */
238   @NonNull
239   public static ITimeItem opAddDayTimeDurationToTime(
240       @NonNull ITimeItem instant,
241       @NonNull IDayTimeDurationItem duration) {
242     long seconds = duration.asDuration().toSeconds() % 86_400;
243     int nano = duration.asDuration().getNano();
244 
245     OffsetTime result;
246     try {
247       result = instant.asOffsetTime().plus(Duration.ofSeconds(seconds, nano));
248     } catch (ArithmeticException ex) {
249       throw new ArithmeticFunctionException(ArithmeticFunctionException.OVERFLOW_UNDERFLOW_ERROR, ex);
250     }
251     assert result != null;
252     // preserve the same timezone "presence"
253     return ITimeItem.valueOf(result, instant.hasTimezone());
254   }
255 
256   /**
257    * Based on XPath 3.1 <a href=
258    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dates">op:subtract-dates</a>.
259    *
260    * @param date1
261    *          the first point in time
262    * @param date2
263    *          the second point in time
264    * @param dynamicContext
265    *          the dynamic context used to provide the implicit timezone
266    * @return the elapsed time between the starting instant and ending instant
267    */
268   @NonNull
269   public static IDayTimeDurationItem opSubtractDates(
270       @NonNull IDateItem date1,
271       @NonNull IDateItem date2,
272       @NonNull DynamicContext dynamicContext) {
273     return opSubtractDateTimes(
274         date1.asDateTime(),
275         date2.asDateTime(),
276         dynamicContext);
277   }
278 
279   /**
280    * Based on XPath 3.1 <a href=
281    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDuration-from-date">op:subtract-yearMonthDuration-from-date</a>.
282    *
283    * @param date
284    *          a point in time
285    * @param duration
286    *          the duration to subtract
287    * @return the result of subtracting the duration from the date
288    */
289   @NonNull
290   public static IDateItem opSubtractYearMonthDurationFromDate(
291       @NonNull IDateItem date,
292       @NonNull IYearMonthDurationItem duration) {
293     return opAddYearMonthDurationToDate(date, duration.negate());
294   }
295 
296   /**
297    * Based on XPath 3.1 <a href=
298    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-date">op:subtract-dayTimeDuration-from-date</a>.
299    *
300    * @param date
301    *          a point in time
302    * @param duration
303    *          the duration to subtract
304    * @return the result of subtracting the duration from the date
305    */
306   @NonNull
307   public static IDateItem opSubtractDayTimeDurationFromDate(
308       @NonNull IDateItem date,
309       @NonNull IDayTimeDurationItem duration) {
310     return opAddDayTimeDurationToDate(date, duration.negate());
311   }
312 
313   /**
314    * Based on XPath 3.1 <a href=
315    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-time">op:subtract-dayTimeDuration-from-time</a>.
316    *
317    * @param time
318    *          a point in time
319    * @param duration
320    *          the duration to subtract
321    * @return the result of subtracting the duration from the time
322    */
323   @NonNull
324   public static ITimeItem opSubtractDayTimeDurationFromTime(
325       @NonNull ITimeItem time,
326       @NonNull IDayTimeDurationItem duration) {
327     return opAddDayTimeDurationToTime(time, duration.negate());
328   }
329 
330   /**
331    * Based on XPath 3.1 <a href=
332    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-times">op:subtract-times</a>.
333    *
334    * @param arg1
335    *          the first duration
336    * @param arg2
337    *          the second duration
338    * @param dynamicContext
339    *          the dynamic context used to provide the implicit timezone
340    * @return the result of subtracting the second duration from the first
341    */
342   @NonNull
343   public static IDayTimeDurationItem opSubtractTimes(
344       @NonNull ITimeItem arg1,
345       @NonNull ITimeItem arg2,
346       @NonNull DynamicContext dynamicContext) {
347     return opSubtractDateTimes(
348         FnDateTime.fnDateTime(DATE_1972_12_31, arg1),
349         FnDateTime.fnDateTime(DATE_1972_12_31, arg2),
350         dynamicContext);
351   }
352 
353   /**
354    * Based on XPath 3.1 <a href=
355    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDurations">op:subtract-yearMonthDurations</a>.
356    *
357    * @param arg1
358    *          the first duration
359    * @param arg2
360    *          the second duration
361    * @return the result of subtracting the second duration from the first
362    */
363   @NonNull
364   public static IYearMonthDurationItem opSubtractYearMonthDurations(
365       @NonNull IYearMonthDurationItem arg1,
366       @NonNull IYearMonthDurationItem arg2) {
367     Period duration1 = arg1.asPeriod();
368     Period duration2 = arg2.asPeriod();
369 
370     Period result;
371     try {
372       result = ObjectUtils.notNull(duration1.minus(duration2));
373     } catch (ArithmeticException ex) {
374       throw new DateTimeFunctionException(DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR, ex);
375     }
376     return IYearMonthDurationItem.valueOf(result);
377   }
378 
379   /**
380    * Based on XPath 3.1 <a href=
381    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDurations">op:subtract-dayTimeDurations</a>.
382    *
383    * @param arg1
384    *          the first duration
385    * @param arg2
386    *          the second duration
387    * @return the result of subtracting the second duration from the first
388    */
389   @NonNull
390   public static IDayTimeDurationItem opSubtractDayTimeDurations(
391       @NonNull IDayTimeDurationItem arg1,
392       @NonNull IDayTimeDurationItem arg2) {
393     Duration duration1 = arg1.asDuration();
394     Duration duration2 = arg2.asDuration();
395 
396     Duration result;
397     try {
398       result = ObjectUtils.notNull(duration1.minus(duration2));
399     } catch (ArithmeticException ex) {
400       throw new DateTimeFunctionException(DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR, ex);
401     }
402     return IDayTimeDurationItem.valueOf(result);
403   }
404 
405   /**
406    * Based on XPath 3.1 <a href=
407    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dateTimes">op:subtract-dateTimes</a>.
408    *
409    * @param instant1
410    *          the first point in time
411    * @param instant2
412    *          the second point in time
413    * @param dynamicContext
414    *          the dynamic context used to provide the implicit timezone
415    * @return the duration the occurred between the two points in time
416    */
417   @NonNull
418   public static IDayTimeDurationItem opSubtractDateTimes(
419       @NonNull IDateTimeItem instant1,
420       @NonNull IDateTimeItem instant2,
421       @NonNull DynamicContext dynamicContext) {
422     return between(
423         instant2.normalize(dynamicContext).asZonedDateTime(),
424         instant1.normalize(dynamicContext).asZonedDateTime());
425   }
426 
427   /**
428    * Based on XPath 3.1 <a href=
429    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dateTimes">op:subtract-dateTimes</a>
430    * and <a href=
431    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-times">op:subtract-times</a>.
432    *
433    * @param time1
434    *          the first point in time
435    * @param time2
436    *          the second point in time
437    * @return the duration the occurred between the two points in time
438    */
439   @NonNull
440   private static IDayTimeDurationItem between(@NonNull Temporal time1, @NonNull Temporal time2) {
441     return IDayTimeDurationItem.valueOf(ObjectUtils.notNull(Duration.between(time1, time2)));
442   }
443 
444   /**
445    * Based on XPath 3.1 <a href=
446    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDuration-from-dateTime">op:subtract-yearMonthDuration-from-dateTime</a>.
447    *
448    * @param moment
449    *          a point in time
450    * @param duration
451    *          the duration to subtract
452    * @return the result of subtracting the duration from a point in time
453    */
454   @NonNull
455   public static IDateTimeItem opSubtractYearMonthDurationFromDateTime(
456       @NonNull IDateTimeItem moment,
457       @NonNull IYearMonthDurationItem duration) {
458     return opAddYearMonthDurationToDateTime(moment, duration.negate());
459   }
460 
461   /**
462    * Based on XPath 3.1 <a href=
463    * "https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-dateTime">op:subtract-dayTimeDuration-from-dateTime</a>.
464    *
465    * @param moment
466    *          a point in time
467    * @param duration
468    *          the duration to subtract
469    * @return the result of subtracting the duration from a point in time
470    */
471   @NonNull
472   public static IDateTimeItem opSubtractDayTimeDurationFromDateTime(
473       @NonNull IDateTimeItem moment,
474       @NonNull IDayTimeDurationItem duration) {
475     // preserve the same timezone "presence"
476     return opAddDayTimeDurationToDateTime(moment, duration.negate());
477   }
478 
479   /**
480    * Based on XPath 3.1 <a href=
481    * "https://www.w3.org/TR/xpath-functions-31/#func-multiply-yearMonthDuration">op:multiply-yearMonthDuration</a>.
482    *
483    * @param arg1
484    *          the duration value
485    * @param arg2
486    *          the number to multiply by
487    * @return the result of multiplying a {@link IYearMonthDurationItem} by a
488    *         number
489    * @throws DateTimeFunctionException
490    *           with the code
491    *           {@link DateTimeFunctionException#DURATION_OVERFLOW_UNDERFLOW_ERROR}
492    *           if arithmetic overflow occurs
493    */
494   @NonNull
495   public static IYearMonthDurationItem opMultiplyYearMonthDuration(
496       @NonNull IYearMonthDurationItem arg1,
497       @NonNull INumericItem arg2) {
498     IDecimalItem months = IDecimalItem.valueOf(arg1.asTotalMonths());
499     INumericItem result = months.multiply(arg2);
500     Period period;
501     try {
502       period = Period.ofMonths(result.round().toIntValueExact());
503     } catch (CastFunctionException ex) {
504       throw new DateTimeFunctionException(DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR, ex);
505     }
506     return IYearMonthDurationItem.valueOf(ObjectUtils.notNull(period));
507   }
508 
509   /**
510    * Based on XPath 3.1 <a href=
511    * "https://www.w3.org/TR/xpath-functions-31/#func-multiply-dayTimeDuration">op:multiply-dayTimeDuration</a>.
512    *
513    * @param arg1
514    *          the duration value
515    * @param arg2
516    *          the number to multiply by
517    * @return the result of multiplying a {@link IDayTimeDurationItem} by a number
518    */
519   @NonNull
520   public static IDayTimeDurationItem opMultiplyDayTimeDuration(
521       @NonNull IDayTimeDurationItem arg1,
522       @NonNull INumericItem arg2) {
523     IDecimalItem seconds = IDecimalItem.valueOf(arg1.asSeconds());
524     INumericItem result = seconds.multiply(arg2);
525     Duration duration;
526     try {
527       duration = Duration.ofSeconds(result.round().toIntValueExact());
528     } catch (CastFunctionException ex) {
529       throw new DateTimeFunctionException(DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR, ex);
530     }
531     return IDayTimeDurationItem.valueOf(ObjectUtils.notNull(duration));
532   }
533 
534   /**
535    * Based on XPath 3.1 <a href=
536    * "https://www.w3.org/TR/xpath-functions-31/#func-divide-yearMonthDuration">op:divide-yearMonthDuration</a>.
537    *
538    * @param arg1
539    *          the duration value
540    * @param arg2
541    *          the number to divide by
542    * @return the result of dividing a {@link IYearMonthDurationItem} by a number
543    */
544   @NonNull
545   public static IYearMonthDurationItem opDivideYearMonthDuration(
546       @NonNull IYearMonthDurationItem arg1,
547       @NonNull INumericItem arg2) {
548     IDecimalItem months = IDecimalItem.valueOf(arg1.asTotalMonths());
549     INumericItem result = months.divide(arg2);
550 
551     Period period = Period.ofMonths(result.round().toIntValueExact());
552     return IYearMonthDurationItem.valueOf(ObjectUtils.notNull(period));
553   }
554 
555   /**
556    * Based on XPath 3.1 <a href=
557    * "https://www.w3.org/TR/xpath-functions-31/#func-divide-yearMonthDuration-by-yearMonthDuration">op:divide-yearMonthDuration-by-yearMonthDuration</a>.
558    *
559    * @param arg1
560    *          the first duration value
561    * @param arg2
562    *          the second duration value
563    * @return the result of dividing a the first duration value by the second
564    *         duration value
565    */
566   @NonNull
567   public static IDecimalItem opDivideYearMonthDurationByYearMonthDuration(
568       @NonNull IYearMonthDurationItem arg1,
569       @NonNull IYearMonthDurationItem arg2) {
570     IIntegerItem totalMonths1 = IIntegerItem.valueOf(arg1.asTotalMonths());
571     IIntegerItem totalMonths2 = IIntegerItem.valueOf(arg2.asTotalMonths());
572 
573     return totalMonths1.divide(totalMonths2);
574   }
575 
576   /**
577    * Based on XPath 3.1 <a href=
578    * "https://www.w3.org/TR/xpath-functions-31/#func-divide-dayTimeDuration">op:divide-dayTimeDuration</a>.
579    *
580    * @param arg1
581    *          the duration value
582    * @param arg2
583    *          the number to divide by
584    * @return the result of dividing a {@link IDayTimeDurationItem} by a number
585    */
586   @NonNull
587   public static IDayTimeDurationItem opDivideDayTimeDuration(
588       @NonNull IDayTimeDurationItem arg1,
589       @NonNull INumericItem arg2) {
590     IDecimalItem seconds = IDecimalItem.valueOf(arg1.asSeconds());
591     INumericItem result = seconds.divide(arg2);
592 
593     Duration duration = Duration.ofSeconds(result.round().toIntValueExact());
594     return IDayTimeDurationItem.valueOf(ObjectUtils.notNull(duration));
595   }
596 
597   /**
598    * Based on XPath 3.1 <a href=
599    * "https://www.w3.org/TR/xpath-functions-31/#func-divide-dayTimeDuration-by-dayTimeDuration">op:divide-dayTimeDuration-by-dayTimeDuration</a>.
600    *
601    * @param arg1
602    *          the first duration value
603    * @param arg2
604    *          the second duration value
605    * @return the ratio of two {@link IDayTimeDurationItem} values, as a decimal
606    *         number
607    */
608   @NonNull
609   public static IDecimalItem opDivideDayTimeDurationByDayTimeDuration(
610       @NonNull IDayTimeDurationItem arg1,
611       @NonNull IDayTimeDurationItem arg2) {
612     return IDecimalItem.cast(
613         opNumericDivide(
614             IDecimalItem.valueOf(arg1.asSeconds()),
615             IDecimalItem.valueOf(arg2.asSeconds())));
616   }
617 
618   /**
619    * Based on XPath 3.1 <a href=
620    * "https://www.w3.org/TR/xpath-functions-31/#func-date-equal">op:date-equal</a>.
621    *
622    * @param arg1
623    *          the first value
624    * @param arg2
625    *          the second value
626    * @param dynamicContext
627    *          used to get the implicit timezone from the evaluation context
628    * @return {@code true} if the first argument is the same instant in time as the
629    *         second, or {@code false} otherwise
630    */
631   @NonNull
632   public static IBooleanItem opDateEqual(
633       @NonNull IDateItem arg1,
634       @NonNull IDateItem arg2,
635       @NonNull DynamicContext dynamicContext) {
636     return opDateTimeEqual(arg1.asDateTime(), arg2.asDateTime(), dynamicContext);
637   }
638 
639   /**
640    * Based on XPath 3.1 <a href=
641    * "https://www.w3.org/TR/xpath-functions-31/#func-dateTime-equal">op:dateTime-equal</a>.
642    *
643    * @param arg1
644    *          the first value
645    * @param arg2
646    *          the second value
647    * @param dynamicContext
648    *          used to get the implicit timezone from the evaluation context
649    * @return {@code true} if the first argument is the same instant in time as the
650    *         second, or {@code false} otherwise
651    */
652   @NonNull
653   public static IBooleanItem opDateTimeEqual(
654       @NonNull IDateTimeItem arg1,
655       @NonNull IDateTimeItem arg2,
656       @Nullable DynamicContext dynamicContext) {
657     IDateTimeItem arg1Normalized = dynamicContext == null ? arg1 : arg1.normalize(dynamicContext);
658     IDateTimeItem arg2Normalized = dynamicContext == null ? arg2 : arg2.normalize(dynamicContext);
659     return IBooleanItem.valueOf(arg1Normalized.asZonedDateTime().isEqual(arg2Normalized.asZonedDateTime()));
660   }
661 
662   /**
663    * Based on XPath 3.1 <a href=
664    * "https://www.w3.org/TR/xpath-functions-31/#func-time-equal">op:time-equal</a>.
665    *
666    * @param arg1
667    *          the first value
668    * @param arg2
669    *          the second value
670    * @param dynamicContext
671    *          used to get the implicit timezone from the evaluation context
672    * @return {@code true} if the first argument is the same instant in time as the
673    *         second, or {@code false} otherwise
674    */
675   @NonNull
676   public static IBooleanItem opTimeEqual(
677       @NonNull ITimeItem arg1,
678       @NonNull ITimeItem arg2,
679       @NonNull DynamicContext dynamicContext) {
680     IDateTimeItem time1 = IDateTimeItem.valueOf(DATE_1972_12_31, arg1);
681     IDateTimeItem time2 = IDateTimeItem.valueOf(DATE_1972_12_31, arg2);
682     return opDateTimeEqual(time1, time2, dynamicContext);
683   }
684 
685   /**
686    * Based on XPath 3.1 <a href=
687    * "https://www.w3.org/TR/xpath-functions-31/#func-duration-equal">op:duration-equal</a>.
688    *
689    * @param arg1
690    *          the first value
691    * @param arg2
692    *          the second value
693    * @return {@code true} if the first argument is the same duration as the
694    *         second, or {@code false} otherwise
695    */
696   @NonNull
697   public static IBooleanItem opDurationEqual(@NonNull IDurationItem arg1, @NonNull IDurationItem arg2) {
698     return IBooleanItem.valueOf(arg1.compareTo(arg2) == 0);
699   }
700 
701   /**
702    * Based on XPath 3.1 <a href=
703    * "https://www.w3.org/TR/xpath-functions-31/#func-base64Binary-equal">op:base64Binary-equal</a>.
704    *
705    * @param arg1
706    *          the first value
707    * @param arg2
708    *          the second value
709    * @return {@code true} if the first argument is equal to the second, or
710    *         {@code false} otherwise
711    */
712   @NonNull
713   public static IBooleanItem opBase64BinaryEqual(@NonNull IBase64BinaryItem arg1, @NonNull IBase64BinaryItem arg2) {
714     return IBooleanItem.valueOf(arg1.asByteBuffer().equals(arg2.asByteBuffer()));
715   }
716 
717   /**
718    * Based on XPath 3.1 <a href=
719    * "https://www.w3.org/TR/xpath-functions-31/#func-date-greater-than">op:date-greater-than</a>.
720    *
721    * @param arg1
722    *          the first value
723    * @param arg2
724    *          the second value
725    * @param dynamicContext
726    *          used to get the implicit timezone from the evaluation context
727    * @return {@code true} if the first argument is a later instant in time than
728    *         the second, or {@code false} otherwise
729    */
730   @NonNull
731   public static IBooleanItem opDateGreaterThan(
732       @NonNull IDateItem arg1,
733       @NonNull IDateItem arg2,
734       @NonNull DynamicContext dynamicContext) {
735     return opDateLessThan(arg2, arg1, dynamicContext);
736   }
737 
738   /**
739    * Based on XPath 3.1 <a href=
740    * "https://www.w3.org/TR/xpath-functions-31/#func-dateTime-greater-than">op:dateTime-greater-than</a>.
741    *
742    * @param arg1
743    *          the first value
744    * @param arg2
745    *          the second value
746    * @param dynamicContext
747    *          used to get the implicit timezone from the evaluation context
748    * @return {@code true} if the first argument is a later instant in time than
749    *         the second, or {@code false} otherwise
750    */
751   @NonNull
752   public static IBooleanItem opDateTimeGreaterThan(
753       @NonNull IDateTimeItem arg1,
754       @NonNull IDateTimeItem arg2,
755       @Nullable DynamicContext dynamicContext) {
756     return opDateTimeLessThan(arg2, arg1, dynamicContext);
757   }
758 
759   /**
760    * Based on XPath 3.1 <a href=
761    * "https://www.w3.org/TR/xpath-functions-31/#func-time-greater-than">op:time-greater-than</a>.
762    *
763    * @param arg1
764    *          the first value
765    * @param arg2
766    *          the second value
767    * @param dynamicContext
768    *          used to get the implicit timezone from the evaluation context
769    * @return {@code true} if the first argument is a later instant in time than
770    *         the second, or {@code false} otherwise
771    */
772   @NonNull
773   public static IBooleanItem opTimeGreaterThan(
774       @NonNull ITimeItem arg1,
775       @NonNull ITimeItem arg2,
776       @NonNull DynamicContext dynamicContext) {
777     return opTimeLessThan(arg2, arg1, dynamicContext);
778   }
779 
780   /**
781    * Based on XPath 3.1 <a href=
782    * "https://www.w3.org/TR/xpath-functions-31/#func-yearMonthDuration-greater-than">op:yearMonthDuration-greater-than</a>.
783    *
784    * @param arg1
785    *          the first value
786    * @param arg2
787    *          the second value
788    * @return {@code true} if the first argument is a longer duration than the
789    *         second, or {@code false} otherwise
790    */
791   @NonNull
792   public static IBooleanItem opYearMonthDurationGreaterThan(
793       @NonNull IYearMonthDurationItem arg1,
794       @NonNull IYearMonthDurationItem arg2) {
795     return opYearMonthDurationLessThan(arg2, arg1);
796   }
797 
798   /**
799    * Based on XPath 3.1 <a href=
800    * "https://www.w3.org/TR/xpath-functions-31/#func-dayTimeDuration-greater-than">op:dayTimeDuration-greater-than</a>.
801    *
802    * @param arg1
803    *          the first value
804    * @param arg2
805    *          the second value
806    * @return {@code true} if the first argument is a longer duration than the
807    *         second, or {@code false} otherwise
808    */
809   @NonNull
810   public static IBooleanItem opDayTimeDurationGreaterThan(
811       @NonNull IDayTimeDurationItem arg1,
812       @NonNull IDayTimeDurationItem arg2) {
813     return opDayTimeDurationLessThan(arg2, arg1);
814   }
815 
816   /**
817    * Based on XPath 3.1 <a href=
818    * "https://www.w3.org/TR/xpath-functions-31/#func-base64Binary-greater-than">op:base64Binary-greater-than</a>.
819    *
820    * @param arg1
821    *          the first value
822    * @param arg2
823    *          the second value
824    * @return {@code true} if the first argument is greater than the second, or
825    *         {@code false} otherwise
826    */
827   @NonNull
828   public static IBooleanItem opBase64BinaryGreaterThan(
829       @NonNull IBase64BinaryItem arg1,
830       @NonNull IBase64BinaryItem arg2) {
831     return opBase64BinaryLessThan(arg2, arg1);
832   }
833 
834   /**
835    * Based on XPath 3.1 <a href=
836    * "https://www.w3.org/TR/xpath-functions-31/#func-date-less-than">op:date-less-than</a>.
837    *
838    * @param arg1
839    *          the first value
840    * @param arg2
841    *          the second value
842    * @param dynamicContext
843    *          used to get the implicit timezone from the evaluation context
844    * @return {@code true} if the first argument is an earlier instant in time than
845    *         the second, or {@code false} otherwise
846    */
847   @NonNull
848   public static IBooleanItem opDateLessThan(
849       @NonNull IDateItem arg1,
850       @NonNull IDateItem arg2,
851       @NonNull DynamicContext dynamicContext) {
852     return opDateTimeLessThan(arg1.asDateTime(), arg2.asDateTime(), dynamicContext);
853   }
854 
855   /**
856    * Based on XPath 3.1 <a href=
857    * "https://www.w3.org/TR/xpath-functions-31/#func-dateTime-less-than">op:dateTime-less-than</a>.
858    *
859    * @param arg1
860    *          the first value
861    * @param arg2
862    *          the second value
863    * @param dynamicContext
864    *          used to get the implicit timezone from the evaluation context
865    * @return {@code true} if the first argument is an earlier instant in time than
866    *         the second, or {@code false} otherwise
867    */
868   @NonNull
869   public static IBooleanItem opTimeLessThan(
870       @NonNull ITimeItem arg1,
871       @NonNull ITimeItem arg2,
872       @NonNull DynamicContext dynamicContext) {
873     return opDateTimeLessThan(
874         IDateTimeItem.valueOf(DATE_1972_12_31, arg1),
875         IDateTimeItem.valueOf(DATE_1972_12_31, arg2),
876         dynamicContext);
877   }
878 
879   /**
880    * Based on XPath 3.1 <a href=
881    * "https://www.w3.org/TR/xpath-functions-31/#func-time-less-than">op:time-less-than</a>.
882    *
883    * @param arg1
884    *          the first value
885    * @param arg2
886    *          the second value
887    * @param dynamicContext
888    *          used to get the implicit timezone from the evaluation context
889    * @return {@code true} if the first argument is an earlier instant in time than
890    *         the second, or {@code false} otherwise
891    */
892   @NonNull
893   public static IBooleanItem opDateTimeLessThan(
894       @NonNull IDateTimeItem arg1,
895       @NonNull IDateTimeItem arg2,
896       @Nullable DynamicContext dynamicContext) {
897     IDateTimeItem arg1Normalized = dynamicContext == null ? arg1 : arg1.normalize(dynamicContext);
898     IDateTimeItem arg2Normalized = dynamicContext == null ? arg2 : arg2.normalize(dynamicContext);
899     return IBooleanItem.valueOf(arg1Normalized.asZonedDateTime().isBefore(arg2Normalized.asZonedDateTime()));
900   }
901 
902   /**
903    * Based on XPath 3.1 <a href=
904    * "https://www.w3.org/TR/xpath-functions-31/#func-yearMonthDuration-less-than">op:yearMonthDuration-less-than</a>.
905    *
906    * @param arg1
907    *          the first value
908    * @param arg2
909    *          the second value
910    * @return {@code true} if the first argument is a shorter duration than the
911    *         second, or {@code false} otherwise
912    */
913   @NonNull
914   public static IBooleanItem opYearMonthDurationLessThan(
915       @NonNull IYearMonthDurationItem arg1,
916       @NonNull IYearMonthDurationItem arg2) {
917     return IBooleanItem.valueOf(arg1.asTotalMonths() < arg2.asTotalMonths());
918   }
919 
920   /**
921    * Based on XPath 3.1 <a href=
922    * "https://www.w3.org/TR/xpath-functions-31/#func-dayTimeDuration-less-than">op:dayTimeDuration-less-than</a>.
923    *
924    * @param arg1
925    *          the first value
926    * @param arg2
927    *          the second value
928    * @return {@code true} if the first argument is a shorter duration than the
929    *         second, or {@code false} otherwise
930    */
931   @NonNull
932   public static IBooleanItem opDayTimeDurationLessThan(
933       @NonNull IDayTimeDurationItem arg1,
934       @NonNull IDayTimeDurationItem arg2) {
935     return IBooleanItem.valueOf(arg1.asSeconds() < arg2.asSeconds());
936   }
937 
938   /**
939    * Based on XPath 3.1 <a href=
940    * "https://www.w3.org/TR/xpath-functions-31/#func-base64Binary-less-than">op:base64Binary-less-than</a>.
941    *
942    * @param arg1
943    *          the first value
944    * @param arg2
945    *          the second value
946    * @return {@code true} if the first argument is less than the second, or
947    *         {@code false} otherwise
948    */
949   @NonNull
950   public static IBooleanItem opBase64BinaryLessThan(
951       @NonNull IBase64BinaryItem arg1,
952       @NonNull IBase64BinaryItem arg2) {
953     return IBooleanItem.valueOf(arg1.compareTo(arg2) < 0);
954   }
955 
956   @NonNull
957   private static <R extends INumericItem> R performArithmeticOperation(
958       @NonNull INumericItem left,
959       @NonNull INumericItem right,
960       @NonNull ArithmeticOperation<IIntegerItem, IIntegerItem, R> integerOp,
961       @NonNull ArithmeticOperation<IDecimalItem, IDecimalItem, R> decimalOp) {
962     R retval;
963     if (left instanceof IIntegerItem && right instanceof IIntegerItem) {
964       retval = integerOp.apply((IIntegerItem) left, (IIntegerItem) right);
965     } else {
966       retval = decimalOp.apply((IDecimalItem) left, (IDecimalItem) right);
967     }
968     return retval;
969   }
970 
971   /**
972    * Represents an arithmetic operation performed using two arguments.
973    *
974    * @param <T>
975    *          the Java type of the first argument to the function
976    * @param <U>
977    *          the Java type of the second argument to the function
978    * @param <R>
979    *          the Java type of the result of the function
980    */
981   @FunctionalInterface
982   private interface ArithmeticOperation<T, U, R> {
983     /**
984      * Perform the operation.
985      *
986      * @param left
987      *          the left side of the operation
988      * @param right
989      *          the right side of the operation
990      * @return the operation result
991      */
992     @NonNull
993     R apply(@NonNull T left, @NonNull U right);
994   }
995 
996   /**
997    * Create a new sum by adding first provided addend value to the second provided
998    * addend value.
999    * <p>
1000    * Based on XPath 3.1 <a href=
1001    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-add">op:numeric-add</a>.
1002    *
1003    * @param addend1
1004    *          the first addend
1005    * @param addend2
1006    *          the second addend
1007    * @return the result of adding the second number to the first number
1008    */
1009   @NonNull
1010   public static INumericItem opNumericAdd(@NonNull INumericItem addend1, @NonNull INumericItem addend2) {
1011     return performArithmeticOperation(
1012         addend1,
1013         addend2,
1014         IIntegerItem::add,
1015         IDecimalItem::add);
1016   }
1017 
1018   /**
1019    * Determine the difference by subtracting the provided subtrahend value from
1020    * the provided minuend value.
1021    * <p>
1022    * Based on XPath 3.1 <a href=
1023    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-subtract">op:numeric-subtract</a>.
1024    *
1025    * @param minuend
1026    *          the value to subtract from
1027    * @param subtrahend
1028    *          the value to subtract
1029    * @return a new value resulting from subtracting the subtrahend from the
1030    *         minuend
1031    */
1032   @NonNull
1033   public static INumericItem opNumericSubtract(
1034       @NonNull INumericItem minuend,
1035       @NonNull INumericItem subtrahend) {
1036     return performArithmeticOperation(
1037         minuend,
1038         subtrahend,
1039         IIntegerItem::subtract,
1040         IDecimalItem::subtract);
1041   }
1042 
1043   /**
1044    * Multiply the provided multiplicand value by the provided multiplier value.
1045    * <p>
1046    * Based on XPath 3.1 <a href=
1047    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-multiply">op:numeric-multiply</a>.
1048    *
1049    * @param multiplicand
1050    *          the value to multiply
1051    * @param multiplier
1052    *          the value to multiply by
1053    * @return a new value resulting from multiplying the multiplicand by the
1054    *         multiplier
1055    */
1056   @NonNull
1057   public static INumericItem opNumericMultiply(
1058       @NonNull INumericItem multiplicand,
1059       @NonNull INumericItem multiplier) {
1060     return performArithmeticOperation(
1061         multiplicand,
1062         multiplier,
1063         IIntegerItem::multiply,
1064         IDecimalItem::multiply);
1065   }
1066 
1067   /**
1068    * Based on XPath 3.1 <a href=
1069    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-divide">op:numeric-divide</a>.
1070    *
1071    * @param dividend
1072    *          the number to be divided
1073    * @param divisor
1074    *          the number to divide by
1075    * @return the quotient
1076    * @throws ArithmeticFunctionException
1077    *           with the code {@link ArithmeticFunctionException#DIVISION_BY_ZERO}
1078    *           if the divisor is zero
1079    */
1080   @NonNull
1081   public static IDecimalItem opNumericDivide(@NonNull INumericItem dividend, @NonNull INumericItem divisor) {
1082     return ((IDecimalItem) dividend).divide((IDecimalItem) divisor);
1083   }
1084 
1085   /**
1086    * Based on XPath 3.1 <a href=
1087    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-integer-divide">op:numeric-integer-divide</a>.
1088    *
1089    * @param dividend
1090    *          the number to be divided
1091    * @param divisor
1092    *          the number to divide by
1093    * @return the quotient
1094    */
1095   @NonNull
1096   public static IIntegerItem opNumericIntegerDivide(
1097       @NonNull INumericItem dividend,
1098       @NonNull INumericItem divisor) {
1099     return performArithmeticOperation(
1100         dividend,
1101         divisor,
1102         IIntegerItem::integerDivide,
1103         IDecimalItem::integerDivide);
1104   }
1105 
1106   /**
1107    * Based on XPath 3.1 <a href=
1108    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-mod">op:numeric-mod</a>.
1109    *
1110    * @param dividend
1111    *          the number to be divided
1112    * @param divisor
1113    *          the number to divide by
1114    * @return the remainder
1115    */
1116   @NonNull
1117   public static INumericItem opNumericMod(@NonNull INumericItem dividend, @NonNull INumericItem divisor) {
1118     return performArithmeticOperation(
1119         dividend,
1120         divisor,
1121         IIntegerItem::mod,
1122         IDecimalItem::mod);
1123   }
1124 
1125   /**
1126    * Based on XPath 3.1 <a href=
1127    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-unary-minus">op:numeric-unary-minus</a>.
1128    *
1129    * @param item
1130    *          the number whose sign is to be reversed
1131    * @return the number with a reversed sign
1132    */
1133   @NonNull
1134   public static INumericItem opNumericUnaryMinus(
1135       @NonNull INumericItem item) {
1136     INumericItem retval;
1137     if (item instanceof IIntegerItem) {
1138       retval = ((IIntegerItem) item).negate();
1139     } else if (item instanceof IDecimalItem) {
1140       retval = ((IDecimalItem) item).negate();
1141     } else {
1142       throw new InvalidTypeMetapathException(
1143           item,
1144           String.format("Unsupported numeric type '%s'.", item.getClass()));
1145     }
1146     return retval;
1147   }
1148 
1149   /**
1150    * Based on XPath 3.1 <a href=
1151    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-equal">op:numeric-equal</a>.
1152    *
1153    * @param arg1
1154    *          the first number to check for equality
1155    * @param arg2
1156    *          the second number to check for equality
1157    * @return {@code true} if the numbers are numerically equal or {@code false}
1158    *         otherwise
1159    */
1160   @NonNull
1161   public static IBooleanItem opNumericEqual(@Nullable INumericItem arg1, @Nullable INumericItem arg2) {
1162     IBooleanItem retval;
1163     if (arg1 == null || arg2 == null) {
1164       retval = IBooleanItem.FALSE;
1165     } else if (arg1 instanceof IIntegerItem && arg2 instanceof IIntegerItem) {
1166       retval = IBooleanItem.valueOf(arg1.asInteger().equals(arg2.asInteger()));
1167     } else {
1168       retval = IBooleanItem.valueOf(arg1.asDecimal().compareTo(arg2.asDecimal()) == 0);
1169     }
1170     return retval;
1171   }
1172 
1173   /**
1174    * Based on XPath 3.1 <a href=
1175    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-greater-than">op:numeric-greater-than</a>.
1176    *
1177    * @param arg1
1178    *          the first number to check
1179    * @param arg2
1180    *          the second number to check
1181    * @return {@code true} if the first number is greater than or equal to the
1182    *         second, or {@code false} otherwise
1183    */
1184   @NonNull
1185   public static IBooleanItem opNumericGreaterThan(@Nullable INumericItem arg1, @Nullable INumericItem arg2) {
1186     return opNumericLessThan(arg2, arg1);
1187   }
1188 
1189   /**
1190    * Based on XPath 3.1 <a href=
1191    * "https://www.w3.org/TR/xpath-functions-31/#func-numeric-less-than">op:numeric-less-than</a>.
1192    *
1193    * @param arg1
1194    *          the first number to check
1195    * @param arg2
1196    *          the second number to check
1197    * @return {@code true} if the first number is less than or equal to the second,
1198    *         or {@code false} otherwise
1199    */
1200   @NonNull
1201   public static IBooleanItem opNumericLessThan(@Nullable INumericItem arg1, @Nullable INumericItem arg2) {
1202     IBooleanItem retval;
1203     if (arg1 == null || arg2 == null) {
1204       retval = IBooleanItem.FALSE;
1205     } else if (arg1 instanceof IIntegerItem && arg2 instanceof IIntegerItem) {
1206       int result = arg1.asInteger().compareTo(arg2.asInteger());
1207       retval = IBooleanItem.valueOf(result < 0);
1208     } else {
1209       int result = arg1.asDecimal().compareTo(arg2.asDecimal());
1210       retval = IBooleanItem.valueOf(result < 0);
1211     }
1212     return retval;
1213   }
1214 
1215   /**
1216    * Based on XPath 3.1 <a href=
1217    * "https://www.w3.org/TR/xpath-functions-31/#func-boolean-equal">op:boolean-equal</a>.
1218    *
1219    * @param arg1
1220    *          the first boolean to check
1221    * @param arg2
1222    *          the second boolean to check
1223    * @return {@code true} if the first boolean is equal to the second, or
1224    *         {@code false} otherwise
1225    */
1226   @NonNull
1227   public static IBooleanItem opBooleanEqual(@Nullable IBooleanItem arg1, @Nullable IBooleanItem arg2) {
1228     boolean left = arg1 != null && arg1.toBoolean();
1229     boolean right = arg2 != null && arg2.toBoolean();
1230 
1231     return IBooleanItem.valueOf(left == right);
1232   }
1233 
1234   /**
1235    * Based on XPath 3.1 <a href=
1236    * "https://www.w3.org/TR/xpath-functions-31/#func-boolean-greater-than">op:boolean-greater-than</a>.
1237    *
1238    * @param arg1
1239    *          the first boolean to check
1240    * @param arg2
1241    *          the second boolean to check
1242    * @return {@code true} if the first argument is {@link IBooleanItem#TRUE} and
1243    *         the second is {@link IBooleanItem#FALSE}, or {@code false} otherwise
1244    */
1245   @NonNull
1246   public static IBooleanItem opBooleanGreaterThan(@Nullable IBooleanItem arg1, @Nullable IBooleanItem arg2) {
1247     return opBooleanLessThan(arg2, arg1);
1248   }
1249 
1250   /**
1251    * Based on XPath 3.1 <a href=
1252    * "https://www.w3.org/TR/xpath-functions-31/#func-boolean-less-than">op:boolean-less-than</a>.
1253    *
1254    * @param arg1
1255    *          the first boolean to check
1256    * @param arg2
1257    *          the second boolean to check
1258    * @return {@code true} if the first argument is {@link IBooleanItem#FALSE} and
1259    *         the second is {@link IBooleanItem#TRUE}, or {@code false} otherwise
1260    */
1261   @NonNull
1262   public static IBooleanItem opBooleanLessThan(@Nullable IBooleanItem arg1, @Nullable IBooleanItem arg2) {
1263     boolean left = arg1 != null && arg1.toBoolean();
1264     boolean right = arg2 != null && arg2.toBoolean();
1265 
1266     return IBooleanItem.valueOf(!left && right);
1267   }
1268 
1269   /**
1270    * Based on XPath 3.1 <a href=
1271    * "https://www.w3.org/TR/xpath-functions-31/#func-same-key">op:same-key</a>.
1272    *
1273    * @param k1
1274    *          the first key to compare
1275    * @param k2
1276    *          the second key to compare
1277    * @param dynamicContext
1278    *          used to get the implicit timezone from the evaluation context
1279    * @return {@code true} if the compared keys are the same, or {@code false}
1280    *         otherwise
1281    */
1282   public static boolean opSameKey(
1283       @NonNull IAnyAtomicItem k1,
1284       @NonNull IAnyAtomicItem k2,
1285       @NonNull DynamicContext dynamicContext) {
1286     boolean retval;
1287     if ((k1 instanceof IStringItem || k1 instanceof IAnyUriItem || k1 instanceof IUntypedAtomicItem)
1288         && (k2 instanceof IStringItem || k2 instanceof IAnyUriItem || k2 instanceof IUntypedAtomicItem)) {
1289       retval = k1.asString().equals(k2.asString());
1290     } else if (k1 instanceof IDecimalItem && k2 instanceof IDecimalItem) {
1291       retval = ((IDecimalItem) k1).asDecimal().equals(((IDecimalItem) k2).asDecimal());
1292     } else {
1293       retval = k1.deepEquals(k2, dynamicContext);
1294     }
1295     return retval;
1296   }
1297 }