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 static dev.metaschema.core.metapath.TestUtils.bool;
9   import static dev.metaschema.core.metapath.TestUtils.date;
10  import static dev.metaschema.core.metapath.TestUtils.dateTime;
11  import static dev.metaschema.core.metapath.TestUtils.dayTimeDuration;
12  import static dev.metaschema.core.metapath.TestUtils.decimal;
13  import static dev.metaschema.core.metapath.TestUtils.duration;
14  import static dev.metaschema.core.metapath.TestUtils.integer;
15  import static dev.metaschema.core.metapath.TestUtils.time;
16  import static dev.metaschema.core.metapath.TestUtils.yearMonthDuration;
17  import static org.assertj.core.api.Assertions.assertThat;
18  import static org.assertj.core.api.Assertions.from;
19  import static org.junit.jupiter.api.Assertions.assertAll;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertThrows;
22  
23  import org.junit.jupiter.api.DisplayName;
24  import org.junit.jupiter.api.Nested;
25  import org.junit.jupiter.api.Test;
26  import org.junit.jupiter.api.TestInstance;
27  import org.junit.jupiter.params.ParameterizedTest;
28  import org.junit.jupiter.params.provider.Arguments;
29  import org.junit.jupiter.params.provider.MethodSource;
30  
31  import java.util.stream.Stream;
32  
33  import dev.metaschema.core.metapath.DynamicContext;
34  import dev.metaschema.core.metapath.function.ArithmeticFunctionException;
35  import dev.metaschema.core.metapath.function.CastFunctionException;
36  import dev.metaschema.core.metapath.function.DateTimeFunctionException;
37  import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
38  import dev.metaschema.core.metapath.item.atomic.IDateItem;
39  import dev.metaschema.core.metapath.item.atomic.IDateTimeItem;
40  import dev.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
41  import dev.metaschema.core.metapath.item.atomic.IDecimalItem;
42  import dev.metaschema.core.metapath.item.atomic.IDurationItem;
43  import dev.metaschema.core.metapath.item.atomic.IIntegerItem;
44  import dev.metaschema.core.metapath.item.atomic.INumericItem;
45  import dev.metaschema.core.metapath.item.atomic.ITimeItem;
46  import dev.metaschema.core.metapath.item.atomic.IYearMonthDurationItem;
47  import edu.umd.cs.findbugs.annotations.NonNull;
48  import edu.umd.cs.findbugs.annotations.Nullable;
49  
50  class OperationFunctionsTest {
51    @Nested
52    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
53    @DisplayName("Functions and operators on numerics")
54    class Numeric {
55      private Stream<Arguments> provideValuesOpNumericAdd() {
56        return Stream.of(
57            Arguments.of(integer(2), integer(1), integer(1)),
58            Arguments.of(decimal("2.0"), decimal("1.0"), integer(1)),
59            Arguments.of(decimal("2.0"), integer(1), decimal("1.0")),
60            Arguments.of(decimal("2.0"), decimal("1.0"), decimal("1.0")));
61      }
62  
63      @ParameterizedTest
64      @MethodSource("provideValuesOpNumericAdd")
65      @DisplayName("op:numeric-add")
66      void testOpNumericAdd(
67          @NonNull INumericItem expected,
68          @NonNull INumericItem addend1,
69          @NonNull INumericItem addend2) {
70        assertEquals(expected, OperationFunctions.opNumericAdd(addend1, addend2));
71      }
72  
73      private Stream<Arguments> provideValuesOpNumericSubtract() {
74        return Stream.of(
75            Arguments.of(integer(0), integer(1), integer(1)),
76            Arguments.of(decimal("0"), decimal("1.0"), integer(1)),
77            Arguments.of(decimal("0"), integer(1), decimal("1.0")),
78            Arguments.of(decimal("0"), decimal("1.0"), decimal("1.0")));
79      }
80  
81      @ParameterizedTest
82      @MethodSource("provideValuesOpNumericSubtract")
83      @DisplayName("op:numeric-subtract")
84      void testOpNumericSubtract(
85          @NonNull INumericItem expected,
86          @NonNull INumericItem minuend,
87          @NonNull INumericItem subtrahend) {
88        assertEquals(expected, OperationFunctions.opNumericSubtract(minuend, subtrahend));
89      }
90  
91      private Stream<Arguments> provideValuesOpNumericMultiply() {
92        return Stream.of(
93            Arguments.of(integer(0), integer(1), integer(0)),
94            Arguments.of(integer(1), integer(1), integer(1)),
95            Arguments.of(integer(2), integer(1), integer(2)),
96            Arguments.of(decimal("1.0"), decimal("1.0"), integer(1)),
97            Arguments.of(decimal("1.0"), integer(1), decimal("1.0")),
98            Arguments.of(decimal("1.0"), decimal("1.0"), decimal("1.0")));
99      }
100 
101     @ParameterizedTest
102     @MethodSource("provideValuesOpNumericMultiply")
103     @DisplayName("op:numeric-multiply")
104     void testOpNumericMultiply(
105         @NonNull INumericItem expected,
106         @NonNull INumericItem multiplicand,
107         @NonNull INumericItem multiplier) {
108       assertEquals(expected, OperationFunctions.opNumericMultiply(multiplicand, multiplier));
109     }
110 
111     @Nested
112     @DisplayName("op:numeric-divide")
113     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
114     class NumericDivide {
115       private Stream<Arguments> provideValuesOpNumericDivide() {
116         return Stream.of(
117             Arguments.of(decimal("0"), integer(0), integer(1)),
118             Arguments.of(decimal("1"), integer(1), integer(1)),
119             Arguments.of(decimal("0.5"), integer(1), integer(2)),
120             Arguments.of(decimal("1.0"), decimal("1.0"), integer(1)),
121             Arguments.of(decimal("1.0"), integer(1), decimal("1.0")),
122             Arguments.of(decimal("1.0"), decimal("1.0"), decimal("1.0")));
123       }
124 
125       @ParameterizedTest
126       @MethodSource("provideValuesOpNumericDivide")
127       @DisplayName("op:numeric-divide - known good")
128       void testOpNumericDivide(
129           @NonNull INumericItem expected,
130           @NonNull INumericItem dividend,
131           @NonNull INumericItem divisor) {
132         assertEquals(expected, OperationFunctions.opNumericDivide(dividend, divisor));
133       }
134 
135       private Stream<Arguments> provideValuesOpNumericDivideByZero() {
136         return Stream.of(
137             Arguments.of(integer(0), integer(0)),
138             Arguments.of(decimal("1.0"), integer(0)),
139             Arguments.of(integer(1), decimal("0.0")),
140             Arguments.of(integer(1), decimal("0")));
141       }
142 
143       @ParameterizedTest
144       @MethodSource("provideValuesOpNumericDivideByZero")
145       @DisplayName("op:numeric-divide - by zero")
146       void testOpNumericDivideByZero(
147           @NonNull INumericItem dividend,
148           @NonNull INumericItem divisor) {
149         ArithmeticFunctionException thrown = assertThrows(ArithmeticFunctionException.class, () -> {
150           OperationFunctions.opNumericDivide(dividend, divisor);
151         });
152         assertThat(thrown)
153             .isExactlyInstanceOf(ArithmeticFunctionException.class)
154             .returns(
155                 ArithmeticFunctionException.DIVISION_BY_ZERO,
156                 from(ex -> ex.getErrorCode().getCode()))
157             .hasNoCause();
158       }
159     }
160 
161     @Nested
162     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
163     @DisplayName("op:numeric-integer-divide")
164     class NumericIntegerDivide {
165       private Stream<Arguments> provideValuesOpNumericIntegerDivide() {
166         return Stream.of(
167             Arguments.of(integer(3), integer(10), integer(3)),
168             Arguments.of(integer(-1), integer(3), integer(-2)),
169             Arguments.of(integer(-1), integer(-3), integer(2)),
170             Arguments.of(integer(3), decimal("9.0"), integer(3)),
171             Arguments.of(integer(-1), decimal("-3.5"), integer(3)),
172             Arguments.of(integer(0), decimal("3.0"), integer(4)),
173             Arguments.of(integer(5), decimal("3.1E1"), integer(6)),
174             Arguments.of(integer(4), decimal("3.1E1"), integer(7)));
175       }
176 
177       @ParameterizedTest
178       @MethodSource("provideValuesOpNumericIntegerDivide")
179       @DisplayName("op:numeric-integer-divide - by zero")
180       void testOpNumericIntegerDivide(
181           @NonNull IIntegerItem expected,
182           @NonNull INumericItem dividend,
183           @NonNull INumericItem divisor) {
184         assertEquals(expected, OperationFunctions.opNumericIntegerDivide(dividend, divisor));
185       }
186 
187       private Stream<Arguments> provideValuesOpNumericIntegerDivideByZero() {
188         return Stream.of(
189             Arguments.of(integer(0), integer(0)),
190             Arguments.of(decimal("1.0"), integer(0)),
191             Arguments.of(integer(1), decimal("0.0")),
192             Arguments.of(integer(1), decimal("0")));
193       }
194 
195       @ParameterizedTest
196       @MethodSource("provideValuesOpNumericIntegerDivideByZero")
197       void testOpNumericIntegerDivideByZero(
198           @NonNull INumericItem dividend,
199           @NonNull INumericItem divisor) {
200         ArithmeticFunctionException thrown = assertThrows(ArithmeticFunctionException.class, () -> {
201           OperationFunctions.opNumericIntegerDivide(dividend, divisor);
202         });
203         assertThat(thrown)
204             .isExactlyInstanceOf(ArithmeticFunctionException.class)
205             .returns(
206                 ArithmeticFunctionException.DIVISION_BY_ZERO,
207                 from(ex -> ex.getErrorCode().getCode()))
208             .hasNoCause();
209       }
210     }
211 
212     private Stream<Arguments> provideValuesOpNumericMod() {
213       return Stream.of(
214           Arguments.of(integer(1), integer(10), integer(3)),
215           Arguments.of(decimal(0), integer(6), integer(-2)),
216           Arguments.of(decimal(3.0E0), decimal(1.23E2), decimal(0.6E1)),
217           Arguments.of(integer(2), integer(5), integer(3)),
218           Arguments.of(integer(0), integer(6), integer(-2)),
219           Arguments.of(decimal("0.9"), decimal("4.5"), decimal("1.2")),
220           Arguments.of(integer(3), integer(123), integer(6)));
221     }
222 
223     @ParameterizedTest
224     @MethodSource("provideValuesOpNumericMod")
225     @DisplayName("op:numeric-mod")
226     void testOpNumericMod(@Nullable INumericItem expected, @NonNull INumericItem dividend,
227         @NonNull INumericItem divisor) {
228       assertEquals(expected, OperationFunctions.opNumericMod(dividend, divisor));
229     }
230 
231     private Stream<Arguments> provideValuesOpNumericModByZero() {
232       return Stream.of(
233           Arguments.of(integer(0), integer(0)),
234           Arguments.of(decimal("1.0"), integer(0)),
235           Arguments.of(integer(1), decimal("0.0")),
236           Arguments.of(integer(1), decimal("0")));
237     }
238 
239     @ParameterizedTest
240     @MethodSource("provideValuesOpNumericModByZero")
241     @DisplayName("op:numeric-mod - by zero")
242     void testOpNumericModByZero(
243         @NonNull INumericItem dividend,
244         @NonNull INumericItem divisor) {
245       ArithmeticFunctionException thrown = assertThrows(ArithmeticFunctionException.class, () -> {
246         OperationFunctions.opNumericMod(dividend, divisor);
247       });
248       assertThat(thrown)
249           .isExactlyInstanceOf(ArithmeticFunctionException.class)
250           .returns(
251               ArithmeticFunctionException.DIVISION_BY_ZERO,
252               from(ex -> ex.getErrorCode().getCode()))
253           .hasNoCause();
254     }
255 
256     private Stream<Arguments> provideValuesOpNumericUnaryMinus() {
257       return Stream.of(
258           Arguments.of(integer(-10), integer(10)),
259           Arguments.of(integer(3), integer(-3)),
260           Arguments.of(decimal(0), integer(-0)),
261           Arguments.of(decimal(3.0E0), decimal(-3.0E0)),
262           Arguments.of(decimal("-0.9"), decimal("0.9")));
263     }
264 
265     @ParameterizedTest
266     @MethodSource("provideValuesOpNumericUnaryMinus")
267     @DisplayName("op:numeric-unary-minus")
268     void testOpNumericUnaryMinus(@Nullable INumericItem expected, @NonNull INumericItem item) {
269       assertEquals(expected, OperationFunctions.opNumericUnaryMinus(item));
270     }
271   }
272 
273   @Nested
274   @DisplayName("Comparison operators on numeric values")
275   @TestInstance(TestInstance.Lifecycle.PER_CLASS)
276   class NumericComparison {
277 
278     private Stream<Arguments> provideValuesOpNumericEqual() {
279       return Stream.of(
280           Arguments.of(bool(false), null, integer(1)),
281           Arguments.of(bool(false), integer(1), null),
282           Arguments.of(bool(false), null, null),
283           Arguments.of(bool(true), integer(1), integer(1)),
284           Arguments.of(bool(true), integer(0), integer(0)),
285           Arguments.of(bool(true), decimal(0), integer(-0)),
286           Arguments.of(bool(true), decimal(3.0E0), decimal(3.0E0)),
287           Arguments.of(bool(false), integer(-10), integer(10)),
288           Arguments.of(bool(false), integer(3), integer(-3)),
289           Arguments.of(bool(false), decimal(3.0E0), decimal(-3.0E0)),
290           Arguments.of(bool(false), decimal("-0.9"), decimal("0.9")));
291     }
292 
293     @ParameterizedTest
294     @MethodSource("provideValuesOpNumericEqual")
295     @DisplayName("op:numeric-equal")
296     void testOpNumericUnaryMinus(
297         @NonNull IBooleanItem expected,
298         @Nullable INumericItem item1,
299         @Nullable INumericItem item2) {
300       assertEquals(expected, OperationFunctions.opNumericEqual(item1, item2));
301     }
302 
303     private Stream<Arguments> provideValuesOpNumericLessThan() {
304       return Stream.of(
305           Arguments.of(bool(false), null, integer(1)),
306           Arguments.of(bool(false), integer(1), null),
307           Arguments.of(bool(false), null, null),
308           Arguments.of(bool(false), integer(1), integer(1)),
309           Arguments.of(bool(false), integer(0), integer(0)),
310           Arguments.of(bool(false), decimal(0), integer(-0)),
311           Arguments.of(bool(false), decimal(3.0E0), decimal(3.0E0)),
312           Arguments.of(bool(true), integer(-10), integer(10)),
313           Arguments.of(bool(false), integer(3), integer(-3)),
314           Arguments.of(bool(false), decimal(3.0E0), decimal(-3.0E0)),
315           Arguments.of(bool(true), decimal("-0.9"), decimal("0.9")));
316     }
317 
318     @ParameterizedTest
319     @MethodSource("provideValuesOpNumericLessThan")
320     @DisplayName("op:numeric-less-than and op:numeric-greater-than")
321     void testOpNumericNumericLessThan(
322         @NonNull IBooleanItem expected,
323         @Nullable INumericItem item1,
324         @Nullable INumericItem item2) {
325       assertAll(
326           () -> assertEquals(expected, OperationFunctions.opNumericLessThan(item1, item2)),
327           () -> assertEquals(expected, OperationFunctions.opNumericGreaterThan(item2, item1)));
328     }
329   }
330 
331   @Nested
332   @DisplayName("Functions and operators on Boolean values")
333   @TestInstance(TestInstance.Lifecycle.PER_CLASS)
334   class Boolean {
335 
336     @Nested
337     @DisplayName("Operators on Boolean values")
338     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
339     class BooleanComparison {
340       private Stream<Arguments> provideValuesOpBooleanEqual() {
341         return Stream.of(
342             Arguments.of(bool(true), bool(true), bool(true)),
343             Arguments.of(bool(false), bool(true), bool(false)),
344             Arguments.of(bool(false), bool(false), bool(true)),
345             Arguments.of(bool(true), bool(false), bool(false)));
346       }
347 
348       @ParameterizedTest
349       @MethodSource("provideValuesOpBooleanEqual")
350       @DisplayName("op:boolean-equal")
351       void testOpBooleanEqual(
352           @Nullable IBooleanItem expected,
353           @NonNull IBooleanItem item1,
354           @NonNull IBooleanItem item2) {
355         assertEquals(expected, OperationFunctions.opBooleanEqual(item1, item2));
356       }
357 
358       private Stream<Arguments> provideValuesOpBooleanLessThan() {
359         return Stream.of(
360             Arguments.of(bool(false), bool(true), bool(true)),
361             Arguments.of(bool(false), bool(true), bool(false)),
362             Arguments.of(bool(true), bool(false), bool(true)),
363             Arguments.of(bool(false), bool(false), bool(false)));
364       }
365 
366       @ParameterizedTest
367       @MethodSource("provideValuesOpBooleanLessThan")
368       @DisplayName("op:boolean-less-than")
369       void testOpBooleanLessThan(
370           @Nullable IBooleanItem expected,
371           @NonNull IBooleanItem item1,
372           @NonNull IBooleanItem item2) {
373         assertEquals(expected, OperationFunctions.opBooleanLessThan(item1, item2));
374       }
375 
376       private Stream<Arguments> provideValuesOpBooleanGreaterThan() {
377         return Stream.of(
378             Arguments.of(bool(false), bool(true), bool(true)),
379             Arguments.of(bool(true), bool(true), bool(false)),
380             Arguments.of(bool(false), bool(false), bool(true)),
381             Arguments.of(bool(false), bool(false), bool(false)));
382       }
383 
384       @ParameterizedTest
385       @MethodSource("provideValuesOpBooleanGreaterThan")
386       @DisplayName("op:boolean-greater-than")
387       void testOpBooleanGreaterThan(
388           @Nullable IBooleanItem expected,
389           @NonNull IBooleanItem item1,
390           @NonNull IBooleanItem item2) {
391         assertEquals(expected, OperationFunctions.opBooleanGreaterThan(item1, item2));
392       }
393     }
394   }
395 
396   @Nested
397   @DisplayName("Functions and operators on durations")
398   @TestInstance(TestInstance.Lifecycle.PER_CLASS)
399   class Duration {
400     @Nested
401     @DisplayName("Comparison operators on durations")
402     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
403     class DurationComparison {
404       private Stream<Arguments> provideValuesDurationEqual() {
405         return Stream.of(
406             Arguments.of(bool(true), duration("P1Y"), duration("P12M")),
407             Arguments.of(bool(true), duration("PT24H"), duration("P1D")),
408             Arguments.of(bool(false), duration("P1Y"), duration("P365D")),
409             Arguments.of(bool(true), duration("P0Y"), duration("P0D")),
410             Arguments.of(bool(true), yearMonthDuration("P0Y"), dayTimeDuration("P0D")),
411             Arguments.of(bool(false), yearMonthDuration("P1Y"), dayTimeDuration("P365D")),
412             Arguments.of(bool(true), yearMonthDuration("P2Y"), yearMonthDuration("P24M")),
413             Arguments.of(bool(true), dayTimeDuration("P10D"), dayTimeDuration("PT240H")),
414             Arguments.of(bool(true), duration("P2Y0M0DT0H0M0S"), yearMonthDuration("P24M")),
415             Arguments.of(bool(true), duration("P0Y0M10D"), dayTimeDuration("PT240H")));
416       }
417 
418       @ParameterizedTest
419       @DisplayName("op:duration-equal")
420       @MethodSource("provideValuesDurationEqual")
421       void testOpDurationEqual(
422           @NonNull IBooleanItem expected,
423           @NonNull IDurationItem arg1,
424           @NonNull IDurationItem arg2) {
425         assertEquals(expected, OperationFunctions.opDurationEqual(arg1, arg2));
426       }
427 
428       private Stream<Arguments> provideValuesYearMonthDurationLessAndGreaterThan() {
429         return Stream.of(
430             Arguments.of(bool(false), yearMonthDuration("P1Y"), yearMonthDuration("P11M")),
431             Arguments.of(bool(true), yearMonthDuration("P2Y"), yearMonthDuration("P25M")),
432             Arguments.of(bool(false), yearMonthDuration("P2Y"), yearMonthDuration("P1Y9M")),
433             Arguments.of(bool(true), yearMonthDuration("P1Y"), yearMonthDuration("P15M")),
434             Arguments.of(bool(false), yearMonthDuration("P3Y"), yearMonthDuration("P1Y15M")),
435             Arguments.of(bool(true), yearMonthDuration("P3Y"), yearMonthDuration("P1Y72M")),
436             Arguments.of(bool(false), yearMonthDuration("P1Y"), yearMonthDuration("P12M")),
437             Arguments.of(bool(false), yearMonthDuration("P2Y"), yearMonthDuration("P24M")));
438       }
439 
440       @ParameterizedTest
441       @DisplayName("op:yearMonthDuration-less-than and op:yearMonthDuration-greater-than")
442       @MethodSource("provideValuesYearMonthDurationLessAndGreaterThan")
443       void testOpYearMonthDurationLessAndGreaterThan(
444           @NonNull IBooleanItem expected,
445           @NonNull IYearMonthDurationItem item1,
446           @NonNull IYearMonthDurationItem item2) {
447         assertAll(
448             () -> assertEquals(expected, OperationFunctions.opYearMonthDurationLessThan(item1, item2)),
449             () -> assertEquals(expected, OperationFunctions.opYearMonthDurationGreaterThan(item2, item1)));
450 
451       }
452 
453       private Stream<Arguments> provideValuesDayTimeDurationLessAndGreaterThan() {
454         return Stream.of(
455             Arguments.of(bool(false), dayTimeDuration("P1D"), dayTimeDuration("PT24H")),
456             Arguments.of(bool(true), dayTimeDuration("P2D"), dayTimeDuration("P1DT25H")),
457             Arguments.of(bool(true), dayTimeDuration("P2D"), dayTimeDuration("P1DT86401S")),
458             Arguments.of(bool(true), dayTimeDuration("P1D"), dayTimeDuration("P2D")),
459             Arguments.of(bool(true), dayTimeDuration("P3D"), dayTimeDuration("PT4321M")),
460             Arguments.of(bool(false), dayTimeDuration("P1D"), dayTimeDuration("P1D")),
461             Arguments.of(bool(false), dayTimeDuration("PT1H"), dayTimeDuration("PT1H")),
462             Arguments.of(bool(false), dayTimeDuration("PT1M"), dayTimeDuration("PT1M")),
463             Arguments.of(bool(false), dayTimeDuration("PT1S"), dayTimeDuration("PT1S")),
464             Arguments.of(bool(true), dayTimeDuration("PT1S"), dayTimeDuration("PT2S")));
465       }
466 
467       @ParameterizedTest
468       @DisplayName("op:dayTimeDuration-less-than and op:dayTimeDuration-greater-than")
469       @MethodSource("provideValuesDayTimeDurationLessAndGreaterThan")
470       void testOpDayTimeDurationLessAndGreaterThan(
471           @NonNull IBooleanItem expected,
472           @NonNull IDayTimeDurationItem item1,
473           @NonNull IDayTimeDurationItem item2) {
474         assertAll(
475             () -> assertEquals(expected, OperationFunctions.opDayTimeDurationLessThan(item1, item2)),
476             () -> assertEquals(expected, OperationFunctions.opDayTimeDurationGreaterThan(item2, item1)));
477       }
478     }
479 
480     @Nested
481     @DisplayName("Arithmetic operators on durations")
482     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
483     class DurationArithmetic {
484       @Nested
485       class YearMonthDuration {
486 
487         @Test
488         @DisplayName("op:add-yearMonthDurations: known good")
489         void testOpAddYearMonthDurations() {
490           assertEquals(
491               IYearMonthDurationItem.valueOf("P6Y2M"),
492               OperationFunctions.opAddYearMonthDurations(
493                   IYearMonthDurationItem.valueOf("P2Y11M"),
494                   IYearMonthDurationItem.valueOf("P3Y3M")));
495         }
496 
497         @Test
498         @DisplayName("op:add-yearMonthDurations: overflow")
499         void testOpAddYearMonthDurationsOverflow() {
500           DateTimeFunctionException thrown = assertThrows(DateTimeFunctionException.class, () -> {
501             OperationFunctions.opAddYearMonthDurations(
502                 IYearMonthDurationItem.valueOf("P" + Integer.MAX_VALUE + "Y"),
503                 IYearMonthDurationItem.valueOf("P" + Integer.MAX_VALUE + "Y"));
504           });
505           assertThat(thrown)
506               .isExactlyInstanceOf(DateTimeFunctionException.class)
507               .returns(
508                   DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR,
509                   from(ex -> ex.getErrorCode().getCode()))
510               .cause()
511               .isExactlyInstanceOf(ArithmeticException.class);
512         }
513 
514         @Test
515         @DisplayName("op:subtract-yearMonthDurations: known good")
516         void testOpSubtractYearMonthDurations() {
517           assertEquals(
518               IYearMonthDurationItem.valueOf("-P4M"),
519               OperationFunctions.opSubtractYearMonthDurations(
520                   IYearMonthDurationItem.valueOf("P2Y11M"),
521                   IYearMonthDurationItem.valueOf("P3Y3M")));
522         }
523 
524         @Test
525         @DisplayName("op:subtract-yearMonthDurations: overflow")
526         void testOpSubtractYearMonthDurationsOverflow() {
527           DateTimeFunctionException thrown = assertThrows(DateTimeFunctionException.class, () -> {
528             OperationFunctions.opSubtractYearMonthDurations(
529                 IYearMonthDurationItem.valueOf("-P" + Integer.MAX_VALUE + "Y"),
530                 IYearMonthDurationItem.valueOf("P" + Integer.MAX_VALUE + "Y"));
531           });
532           assertThat(thrown)
533               .isExactlyInstanceOf(DateTimeFunctionException.class)
534               .returns(
535                   DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR,
536                   from(ex -> ex.getErrorCode().getCode()))
537               .cause()
538               .isExactlyInstanceOf(ArithmeticException.class);
539         }
540 
541         @Test
542         @DisplayName("op:multiply-yearMonthDurations: known good")
543         void testOpMultiplyYearMonthDuration() {
544           assertAll(
545               () -> assertEquals(
546                   IYearMonthDurationItem.valueOf("P6Y9M"),
547                   OperationFunctions.opMultiplyYearMonthDuration(
548                       IYearMonthDurationItem.valueOf("P2Y11M"),
549                       IDecimalItem.valueOf("2.3"))),
550               () -> assertEquals(
551                   IYearMonthDurationItem.valueOf("P0M"),
552                   OperationFunctions.opMultiplyYearMonthDuration(
553                       IYearMonthDurationItem.valueOf("P1Y"),
554                       IDecimalItem.valueOf("0"))),
555               () -> assertEquals(
556                   IYearMonthDurationItem.valueOf("-P2Y"),
557                   OperationFunctions.opMultiplyYearMonthDuration(
558                       IYearMonthDurationItem.valueOf("P1Y"),
559                       IDecimalItem.valueOf("-2"))));
560         }
561 
562         @Test
563         @DisplayName("op:multiply-yearMonthDurations: overflow")
564         void testOpMultiplyYearMonthDurationsOverflow() {
565           DateTimeFunctionException thrown = assertThrows(DateTimeFunctionException.class, () -> {
566             OperationFunctions.opMultiplyYearMonthDuration(
567                 IYearMonthDurationItem.valueOf("P" + Integer.MAX_VALUE + "Y"),
568                 IDecimalItem.valueOf("2.5"));
569           });
570           assertThat(thrown)
571               .isExactlyInstanceOf(DateTimeFunctionException.class)
572               .returns(
573                   DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR,
574                   from(ex -> ex.getErrorCode().getCode()))
575               .cause()
576               .isExactlyInstanceOf(CastFunctionException.class)
577               .returns(
578                   CastFunctionException.INPUT_VALUE_TOO_LARGE,
579                   from(ex -> ex instanceof CastFunctionException
580                       ? ((CastFunctionException) ex).getErrorCode().getCode()
581                       : null));
582         }
583 
584         @Test
585         @DisplayName("op:divide-yearMonthDurations: known good")
586         void testOpDivideYearMonthDuration() {
587           assertEquals(
588               IYearMonthDurationItem.valueOf("P1Y11M"),
589               OperationFunctions.opDivideYearMonthDuration(
590                   IYearMonthDurationItem.valueOf("P2Y11M"),
591                   IDecimalItem.valueOf("1.5")));
592         }
593 
594         @Test
595         @DisplayName("op:divide-yearMonthDuration-by-yearMonthDuration: known good")
596         void testOpDivideYearMonthDurationByYearMonthDuration() {
597           assertEquals(
598               IDecimalItem.valueOf("-2.5"),
599               OperationFunctions.opDivideYearMonthDurationByYearMonthDuration(
600                   IYearMonthDurationItem.valueOf("P3Y4M"),
601                   IYearMonthDurationItem.valueOf("-P1Y4M")));
602         }
603       }
604 
605       @Nested
606       class DayTimeDuration {
607 
608         @Test
609         @DisplayName("op:add-dayTimeDurations: known good")
610         void testOpAddDayTimeDurations() {
611           assertEquals(
612               IDayTimeDurationItem.valueOf("P8DT5M"),
613               OperationFunctions.opAddDayTimeDurations(
614                   IDayTimeDurationItem.valueOf("P2DT12H5M"),
615                   IDayTimeDurationItem.valueOf("P5DT12H")));
616         }
617 
618         @Test
619         @DisplayName("op:add-dayTimeDurations: overflow")
620         void testOpAddDayTimeDurationsOverflow() {
621           DateTimeFunctionException thrown = assertThrows(DateTimeFunctionException.class, () -> {
622             OperationFunctions.opAddDayTimeDurations(
623                 // subtracting 807 ensures the long doesn't overflow
624                 IDayTimeDurationItem.valueOf("PT" + (Long.MAX_VALUE - 807) + "S"),
625                 IDayTimeDurationItem.valueOf("PT" + (Long.MAX_VALUE - 807) + "S"));
626           });
627           assertThat(thrown)
628               .isExactlyInstanceOf(DateTimeFunctionException.class)
629               .hasCauseExactlyInstanceOf(ArithmeticException.class)
630               .returns(
631                   DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR,
632                   from(ex -> ex.getErrorCode().getCode()));
633         }
634 
635         @Test
636         @DisplayName("op:subtract-dayTimeDurations: known good")
637         void testOpSubtractDayTimeDurations() {
638           assertEquals(
639               IDayTimeDurationItem.valueOf("P1DT1H30M"),
640               OperationFunctions.opSubtractDayTimeDurations(
641                   IDayTimeDurationItem.valueOf("P2DT12H"),
642                   IDayTimeDurationItem.valueOf("P1DT10H30M")));
643         }
644 
645         @Test
646         @DisplayName("op:subtract-dayTimeDurations: overflow")
647         void testOpSubtractDayTimeDurationsOverflow() {
648           DateTimeFunctionException thrown = assertThrows(DateTimeFunctionException.class, () -> {
649             OperationFunctions.opSubtractDayTimeDurations(
650                 IDayTimeDurationItem.valueOf("-PT" + (Long.MAX_VALUE - 807) + "S"),
651                 IDayTimeDurationItem.valueOf("PT" + (Long.MAX_VALUE - 807) + "S"));
652           });
653           assertThat(thrown)
654               .isExactlyInstanceOf(DateTimeFunctionException.class)
655               .hasCauseExactlyInstanceOf(ArithmeticException.class)
656               .returns(
657                   DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR,
658                   from(ex -> ex.getErrorCode().getCode()));
659         }
660 
661         @Test
662         @DisplayName("op:multiply-dayTimeDuration: known good")
663         void testOpMultiplyDayTimeDuration() {
664           assertEquals(
665               IDayTimeDurationItem.valueOf("PT4H33M"),
666               OperationFunctions.opMultiplyDayTimeDuration(
667                   IDayTimeDurationItem.valueOf("PT2H10M"),
668                   IDecimalItem.valueOf("2.1")));
669         }
670 
671         @Test
672         @DisplayName("op:multiply-dayTimeDuration: overflow")
673         void testOpMultiplyDayTimeDurationsOverflow() {
674           DateTimeFunctionException thrown = assertThrows(DateTimeFunctionException.class, () -> {
675             OperationFunctions.opMultiplyDayTimeDuration(
676                 IDayTimeDurationItem.valueOf("PT" + Long.MAX_VALUE / 2 + "S"),
677                 IDecimalItem.valueOf("5"));
678           });
679           assertThat(thrown)
680               .isExactlyInstanceOf(DateTimeFunctionException.class)
681               .returns(
682                   DateTimeFunctionException.DURATION_OVERFLOW_UNDERFLOW_ERROR,
683                   from(ex -> ex.getErrorCode().getCode()))
684               .cause()
685               .isExactlyInstanceOf(CastFunctionException.class)
686               .returns(
687                   CastFunctionException.INPUT_VALUE_TOO_LARGE,
688                   from(ex -> ex instanceof CastFunctionException
689                       ? ((CastFunctionException) ex).getErrorCode().getCode()
690                       : null));
691         }
692 
693         @Test
694         @DisplayName("op:divide-dayTimeDuration: known good")
695         void testOpDivideDayTimeDuration() {
696           assertEquals(
697               IDayTimeDurationItem.valueOf("PT17H40M7S"),
698               OperationFunctions.opDivideDayTimeDuration(
699                   IDayTimeDurationItem.valueOf("P1DT2H30M10.5S"),
700                   IDecimalItem.valueOf("1.5")));
701         }
702 
703         @Test
704         @DisplayName("op:divide-dayTimeDuration-by-dayTimeDuration: known good")
705         void testOpDivideDayTimeDurationByDayTimeDuration() {
706           assertAll(
707               () -> assertEquals(
708                   IDecimalItem.valueOf("1.437834967320261"),
709                   OperationFunctions.opDivideDayTimeDurationByDayTimeDuration(
710                       IDayTimeDurationItem.valueOf("P2DT53M11S"),
711                       IDayTimeDurationItem.valueOf("P1DT10H"))),
712               () -> assertEquals(
713                   IDecimalItem.valueOf("175991.0"),
714                   OperationFunctions.opDivideDayTimeDurationByDayTimeDuration(
715                       IDayTimeDurationItem.valueOf("P2DT53M11S"),
716                       IDayTimeDurationItem.valueOf("PT1S"))));
717         }
718       }
719     }
720   }
721 
722   @Nested
723   @DisplayName("Functions and operators on dates and times")
724   @TestInstance(TestInstance.Lifecycle.PER_CLASS)
725   class DateTime {
726     @Nested
727     @DisplayName("Functions and operators on dates and times")
728     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
729     class DateTimeOperators {
730       private Stream<Arguments> provideValuesDateTimeCompare() {
731         return Stream.of(
732             Arguments.of(
733                 bool(true),
734                 bool(false),
735                 IDateTimeItem.valueOf("2002-04-02T12:00:00-01:00"),
736                 IDateTimeItem.valueOf("2002-04-02T17:00:00+04:00")),
737             Arguments.of(
738                 bool(true),
739                 bool(false),
740                 IDateTimeItem.valueOf("2002-04-02T12:00:00"),
741                 IDateTimeItem.valueOf("2002-04-02T23:00:00+06:00")),
742             Arguments.of(
743                 bool(false),
744                 bool(true),
745                 IDateTimeItem.valueOf("2002-04-02T12:00:00"),
746                 IDateTimeItem.valueOf("2002-04-02T17:00:00")),
747             Arguments.of(
748                 bool(true),
749                 bool(false),
750                 IDateTimeItem.valueOf("2002-04-02T12:00:00"),
751                 IDateTimeItem.valueOf("2002-04-02T12:00:00")),
752             Arguments.of(
753                 bool(true),
754                 bool(false),
755                 IDateTimeItem.valueOf("2002-04-02T23:00:00-04:00"),
756                 IDateTimeItem.valueOf("2002-04-03T02:00:00-01:00")),
757             Arguments.of(
758                 bool(true),
759                 bool(false),
760                 IDateTimeItem.valueOf("1999-12-31T24:00:00"),
761                 IDateTimeItem.valueOf("2000-01-01T00:00:00")),
762             Arguments.of(
763                 bool(false),
764                 bool(false),
765                 IDateTimeItem.valueOf("2005-04-04T24:00:00"),
766                 IDateTimeItem.valueOf("2005-04-04T00:00:00")));
767       }
768 
769       @ParameterizedTest
770       @DisplayName("op:dateTime-equal")
771       @MethodSource("provideValuesDateTimeCompare")
772       void testOpDateTimeEqual(
773           @NonNull IBooleanItem expectedEqual,
774           @SuppressWarnings("unused") @NonNull IBooleanItem expectedLessThan,
775           @NonNull IDateTimeItem arg1,
776           @NonNull IDateTimeItem arg2) {
777         DynamicContext dynamicContext = new DynamicContext();
778         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
779         assertEquals(expectedEqual, OperationFunctions.opDateTimeEqual(arg1, arg2, dynamicContext));
780       }
781 
782       @ParameterizedTest
783       @DisplayName("op:dateTime-less-than and op:dateTime-greater-than")
784       @MethodSource("provideValuesDateTimeCompare")
785       void testOpDateTimeLessGreaterThan(
786           @SuppressWarnings("unused") @NonNull IBooleanItem expectedEqual,
787           @NonNull IBooleanItem expectedLessThan,
788           @NonNull IDateTimeItem item1,
789           @NonNull IDateTimeItem item2) {
790         DynamicContext dynamicContext = new DynamicContext();
791         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
792         assertAll(
793             () -> assertEquals(
794                 expectedLessThan,
795                 OperationFunctions.opDateTimeLessThan(item1, item2, dynamicContext)),
796             () -> assertEquals(
797                 expectedLessThan,
798                 OperationFunctions.opDateTimeGreaterThan(item2, item1, dynamicContext)));
799       }
800 
801       private Stream<Arguments> provideValuesDateCompare() {
802         return Stream.of(
803             Arguments.of(
804                 bool(false),
805                 bool(false),
806                 date("2004-12-25Z"),
807                 date("2004-12-25+07:00")),
808             Arguments.of(
809                 bool(true),
810                 bool(false),
811                 date("2004-12-25-12:00"),
812                 date("2004-12-26+12:00")));
813       }
814 
815       @ParameterizedTest
816       @DisplayName("op:date-equal")
817       @MethodSource("provideValuesDateCompare")
818       void testOpDateEqual(
819           @NonNull IBooleanItem expectedEqual,
820           @SuppressWarnings("unused") @NonNull IBooleanItem expectedLessThan,
821           @NonNull IDateItem arg1,
822           @NonNull IDateItem arg2) {
823         DynamicContext dynamicContext = new DynamicContext();
824         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
825         assertEquals(expectedEqual, OperationFunctions.opDateEqual(arg1, arg2, dynamicContext));
826       }
827 
828       @ParameterizedTest
829       @DisplayName("op:date-less-than and op:date-greater-than")
830       @MethodSource("provideValuesDateCompare")
831       void testOpDateLessGreaterThan(
832           @SuppressWarnings("unused") @NonNull IBooleanItem expectedEqual,
833           @NonNull IBooleanItem expectedLessThan,
834           @NonNull IDateItem item1,
835           @NonNull IDateItem item2) {
836         DynamicContext dynamicContext = new DynamicContext();
837         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
838         assertAll(
839             () -> assertEquals(
840                 expectedLessThan,
841                 OperationFunctions.opDateLessThan(item1, item2, dynamicContext)),
842             () -> assertEquals(
843                 expectedLessThan,
844                 OperationFunctions.opDateGreaterThan(item2, item1, dynamicContext)));
845       }
846 
847       private Stream<Arguments> provideValuesTimeCompare() {
848         return Stream.of(
849             Arguments.of(
850                 bool(false),
851                 bool(true),
852                 time("08:00:00+09:00"), // 1972-12-30T23:00:00Z
853                 time("17:00:00-06:00")), // 1972-12-31T23:00:00Z
854             Arguments.of(
855                 bool(true),
856                 bool(false),
857                 time("21:30:00+10:30"),
858                 time("06:00:00-05:00")),
859             Arguments.of(
860                 bool(true),
861                 bool(false),
862                 time("24:00:00+01:00"),
863                 time("00:00:00+01:00")),
864             Arguments.of(
865                 bool(true),
866                 bool(false),
867                 time("12:00:00"),
868                 time("23:00:00+06:00")),
869             Arguments.of(
870                 bool(false),
871                 bool(true),
872                 time("11:00:00"),
873                 time("17:00:00Z")),
874             Arguments.of(
875                 bool(false),
876                 bool(false),
877                 time("23:59:59"),
878                 time("24:00:00")));
879       }
880 
881       @ParameterizedTest
882       @DisplayName("op:time-equal")
883       @MethodSource("provideValuesTimeCompare")
884       void testOpTimeEqual(
885           @NonNull IBooleanItem expectedEqual,
886           @SuppressWarnings("unused") @NonNull IBooleanItem expectedLessThan,
887           @NonNull ITimeItem arg1,
888           @NonNull ITimeItem arg2) {
889         DynamicContext dynamicContext = new DynamicContext();
890         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
891         assertEquals(expectedEqual, OperationFunctions.opTimeEqual(arg1, arg2, dynamicContext));
892       }
893 
894       @ParameterizedTest
895       @DisplayName("op:time-less-than and op:time-greater-than")
896       @MethodSource("provideValuesTimeCompare")
897       void testOpTimeLessGreaterThan(
898           @SuppressWarnings("unused") @NonNull IBooleanItem expectedEqual,
899           @NonNull IBooleanItem expectedLessThan,
900           @NonNull ITimeItem item1,
901           @NonNull ITimeItem item2) {
902         DynamicContext dynamicContext = new DynamicContext();
903         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
904         assertAll(
905             () -> assertEquals(
906                 expectedLessThan,
907                 OperationFunctions.opTimeLessThan(item1, item2, dynamicContext)),
908             () -> assertEquals(
909                 expectedLessThan,
910                 OperationFunctions.opTimeGreaterThan(item2, item1, dynamicContext)));
911       }
912 
913       // TODO: op:gYearMonth-equal
914       // TODO: op:gYear-equal
915       // TODO: op:gMonthDay-equal
916       // TODO: op:gMonth-equal
917       // TODO: op:gDay-equal
918     }
919 
920     @Nested
921     @DisplayName("Arithmetic operators on durations, dates and times")
922     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
923     class DateTimeArithmetic {
924       private Stream<Arguments> provideValuesSubtractDateTimes() {
925         return Stream.of(
926             Arguments.of(
927                 dayTimeDuration("P337DT2H12M"),
928                 dateTime("2000-10-30T06:12:00"),
929                 dateTime("1999-11-28T09:00:00Z")));
930       }
931 
932       @ParameterizedTest
933       @DisplayName("op:subtract-dateTimes")
934       @MethodSource("provideValuesSubtractDateTimes")
935       void testOpSubtractDateTimes(
936           @NonNull IDayTimeDurationItem expected,
937           @NonNull IDateTimeItem item1,
938           @NonNull IDateTimeItem item2) {
939         DynamicContext dynamicContext = new DynamicContext();
940         dynamicContext.setImplicitTimeZone(IDayTimeDurationItem.valueOf("-PT5H"));
941         assertEquals(expected, OperationFunctions.opSubtractDateTimes(item1, item2, dynamicContext));
942       }
943 
944       private Stream<Arguments> provideValuesSubtractDates() {
945         return Stream.of(
946             Arguments.of(
947                 dayTimeDuration("P337D"),
948                 dayTimeDuration("PT0H"),
949                 date("2000-10-30"),
950                 date("1999-11-28")),
951             Arguments.of(
952                 dayTimeDuration("P336DT19H"),
953                 dayTimeDuration("PT5H"),
954                 date("2000-10-30"),
955                 date("1999-11-28Z")),
956             Arguments.of(
957                 dayTimeDuration("P5DT7H"),
958                 dayTimeDuration("PT5H"),
959                 date("2000-10-15-05:00"),
960                 date("2000-10-10+02:00")));
961       }
962 
963       @ParameterizedTest
964       @DisplayName("op:subtract-dates")
965       @MethodSource("provideValuesSubtractDates")
966       void testOpSubtractDates(
967           @NonNull IDayTimeDurationItem expected,
968           @NonNull IDayTimeDurationItem offset,
969           @NonNull IDateItem item1,
970           @NonNull IDateItem item2) {
971         DynamicContext dynamicContext = new DynamicContext();
972         dynamicContext.setImplicitTimeZone(offset);
973         assertEquals(expected, OperationFunctions.opSubtractDates(item1, item2, dynamicContext));
974       }
975 
976       private Stream<Arguments> provideValuesSubtractTimes() {
977         return Stream.of(
978             Arguments.of(
979                 dayTimeDuration("PT2H12M"),
980                 dayTimeDuration("-PT5H"),
981                 time("11:12:00Z"),
982                 time("04:00:00")),
983             Arguments.of(
984                 dayTimeDuration("PT0S"),
985                 dayTimeDuration("-PT5H"),
986                 time("11:00:00-05:00"),
987                 time("21:30:00+05:30")),
988             Arguments.of(
989                 dayTimeDuration("P1D"),
990                 dayTimeDuration("-PT5H"),
991                 time("17:00:00-06:00"),
992                 time("08:00:00+09:00")),
993             Arguments.of(
994                 dayTimeDuration("-PT23H59M59S"),
995                 dayTimeDuration("-PT5H"),
996                 time("24:00:00"),
997                 time("23:59:59")));
998       }
999 
1000       @ParameterizedTest
1001       @DisplayName("op:subtract-times")
1002       @MethodSource("provideValuesSubtractTimes")
1003       void testOpSubtractTimes(
1004           @NonNull IDayTimeDurationItem expected,
1005           @NonNull IDayTimeDurationItem offset,
1006           @NonNull ITimeItem item1,
1007           @NonNull ITimeItem item2) {
1008         DynamicContext dynamicContext = new DynamicContext();
1009         dynamicContext.setImplicitTimeZone(offset);
1010         assertEquals(expected, OperationFunctions.opSubtractTimes(item1, item2, dynamicContext));
1011       }
1012 
1013       private Stream<Arguments> provideValuesAddYearMonthDurationToDateTime() {
1014         return Stream.of(
1015             Arguments.of(
1016                 dateTime("2001-12-30T11:12:00"),
1017                 dateTime("2000-10-30T11:12:00"),
1018                 yearMonthDuration("P1Y2M")));
1019       }
1020 
1021       @ParameterizedTest
1022       @DisplayName("op:add-yearMonthDuration-to-dateTime")
1023       @MethodSource("provideValuesAddYearMonthDurationToDateTime")
1024       void testOpAddYearMonthDurationToDateTime(
1025           @NonNull IDateTimeItem expected,
1026           @NonNull IDateTimeItem item1,
1027           @NonNull IYearMonthDurationItem item2) {
1028         assertEquals(expected, OperationFunctions.opAddYearMonthDurationToDateTime(item1, item2));
1029       }
1030 
1031       private Stream<Arguments> provideValuesAddDayTimeDurationToDateTime() {
1032         return Stream.of(
1033             Arguments.of(
1034                 dateTime("2000-11-02T12:27:00"),
1035                 dateTime("2000-10-30T11:12:00"),
1036                 dayTimeDuration("P3DT1H15M")));
1037       }
1038 
1039       @ParameterizedTest
1040       @DisplayName("op:add-dayTimeDuration-to-dateTime")
1041       @MethodSource("provideValuesAddDayTimeDurationToDateTime")
1042       void testOpAddDayTimeDurationToDateTime(
1043           @NonNull IDateTimeItem expected,
1044           @NonNull IDateTimeItem item1,
1045           @NonNull IDayTimeDurationItem item2) {
1046         assertEquals(expected, OperationFunctions.opAddDayTimeDurationToDateTime(item1, item2));
1047       }
1048 
1049       private Stream<Arguments> provideValuesSubtractYearMonthDurationFromDateTime() {
1050         return Stream.of(
1051             Arguments.of(
1052                 dateTime("1999-08-30T11:12:00"),
1053                 dateTime("2000-10-30T11:12:00"),
1054                 yearMonthDuration("P1Y2M")));
1055       }
1056 
1057       @ParameterizedTest
1058       @DisplayName("op:subtract-yearMonthDuration-from-dateTime")
1059       @MethodSource("provideValuesSubtractYearMonthDurationFromDateTime")
1060       void testOpSubtractYearMonthDurationFromDateTime(
1061           @NonNull IDateTimeItem expected,
1062           @NonNull IDateTimeItem item1,
1063           @NonNull IYearMonthDurationItem item2) {
1064         assertEquals(expected, OperationFunctions.opSubtractYearMonthDurationFromDateTime(item1, item2));
1065       }
1066 
1067       private Stream<Arguments> provideValuesSubtractDayTimeDurationFromDateTime() {
1068         return Stream.of(
1069             Arguments.of(
1070                 dateTime("2000-10-27T09:57:00"),
1071                 dateTime("2000-10-30T11:12:00"),
1072                 dayTimeDuration("P3DT1H15M")));
1073       }
1074 
1075       @ParameterizedTest
1076       @DisplayName("op:subtract-dayTimeDuration-from-dateTime")
1077       @MethodSource("provideValuesSubtractDayTimeDurationFromDateTime")
1078       void testOpSubtractDayTimeDurationFromDateTime(
1079           @NonNull IDateTimeItem expected,
1080           @NonNull IDateTimeItem item1,
1081           @NonNull IDayTimeDurationItem item2) {
1082         assertEquals(expected, OperationFunctions.opSubtractDayTimeDurationFromDateTime(item1, item2));
1083       }
1084 
1085       // TODO: op:add-yearMonthDuration-to-date
1086       // TODO: op:add-dayTimeDuration-to-date
1087       // TODO: op:subtract-yearMonthDuration-from-date
1088       // TODO: op:subtract-dayTimeDuration-from-date
1089 
1090       private Stream<Arguments> provideValuesAddYearMonthDurationToDate() {
1091         return Stream.of(
1092             Arguments.of(
1093                 date("2001-12-30"),
1094                 date("2000-10-30"),
1095                 yearMonthDuration("P1Y2M")));
1096       }
1097 
1098       @ParameterizedTest
1099       @DisplayName("op:add-yearMonthDuration-to-date")
1100       @MethodSource("provideValuesAddYearMonthDurationToDate")
1101       void testOpAddYearMonthDurationToDate(
1102           @NonNull IDateItem expected,
1103           @NonNull IDateItem item1,
1104           @NonNull IYearMonthDurationItem item2) {
1105         assertEquals(expected, OperationFunctions.opAddYearMonthDurationToDate(item1, item2));
1106       }
1107 
1108       private Stream<Arguments> provideValuesAddDayTimeDurationToDate() {
1109         return Stream.of(
1110             Arguments.of(
1111                 date("2004-11-01Z"),
1112                 date("2004-10-30Z"),
1113                 dayTimeDuration("P2DT2H30M0S")));
1114       }
1115 
1116       @ParameterizedTest
1117       @DisplayName("op:add-dayTimeDuration-to-date")
1118       @MethodSource("provideValuesAddDayTimeDurationToDate")
1119       void testOpAddDayTimeDurationToDate(
1120           @NonNull IDateItem expected,
1121           @NonNull IDateItem item1,
1122           @NonNull IDayTimeDurationItem item2) {
1123         assertEquals(expected, OperationFunctions.opAddDayTimeDurationToDate(item1, item2));
1124       }
1125 
1126       private Stream<Arguments> provideValuesSubtractYearMonthDurationFromDate() {
1127         return Stream.of(
1128             Arguments.of(
1129                 date("1999-08-30"),
1130                 date("2000-10-30"),
1131                 yearMonthDuration("P1Y2M")),
1132             Arguments.of(
1133                 date("1999-02-28Z"),
1134                 date("2000-02-29Z"),
1135                 yearMonthDuration("P1Y")),
1136             Arguments.of(
1137                 date("1999-09-30-05:00"),
1138                 date("2000-10-31-05:00"),
1139                 yearMonthDuration("P1Y1M")));
1140       }
1141 
1142       @ParameterizedTest
1143       @DisplayName("op:subtract-yearMonthDuration-from-date")
1144       @MethodSource("provideValuesSubtractYearMonthDurationFromDate")
1145       void testOpSubtractYearMonthDurationFromDate(
1146           @NonNull IDateItem expected,
1147           @NonNull IDateItem item1,
1148           @NonNull IYearMonthDurationItem item2) {
1149         assertEquals(expected, OperationFunctions.opSubtractYearMonthDurationFromDate(item1, item2));
1150       }
1151 
1152       private Stream<Arguments> provideValuesSubtractDayTimeDurationFromDate() {
1153         return Stream.of(
1154             Arguments.of(
1155                 date("2000-10-26"),
1156                 date("2000-10-30"),
1157                 dayTimeDuration("P3DT1H15M")));
1158       }
1159 
1160       @ParameterizedTest
1161       @DisplayName("op:subtract-dayTimeDuration-from-date")
1162       @MethodSource("provideValuesSubtractDayTimeDurationFromDate")
1163       void testOpSubtractDayTimeDurationFromDate(
1164           @NonNull IDateItem expected,
1165           @NonNull IDateItem item1,
1166           @NonNull IDayTimeDurationItem item2) {
1167         assertEquals(expected, OperationFunctions.opSubtractDayTimeDurationFromDate(item1, item2));
1168       }
1169 
1170       private Stream<Arguments> provideValuesAddDayTimeDurationToTime() {
1171         return Stream.of(
1172             Arguments.of(
1173                 time("12:27:00"),
1174                 time("11:12:00"),
1175                 dayTimeDuration("P3DT1H15M")),
1176             Arguments.of(
1177                 time("02:27:00+03:00"),
1178                 time("23:12:00+03:00"),
1179                 dayTimeDuration("P1DT3H15M")));
1180       }
1181 
1182       @ParameterizedTest
1183       @DisplayName("op:add-dayTimeDuration-to-time")
1184       @MethodSource("provideValuesAddDayTimeDurationToTime")
1185       void testOpAddDayTimeDurationToTime(
1186           @NonNull ITimeItem expected,
1187           @NonNull ITimeItem item1,
1188           @NonNull IDayTimeDurationItem item2) {
1189         assertEquals(expected, OperationFunctions.opAddDayTimeDurationToTime(item1, item2));
1190       }
1191 
1192       private Stream<Arguments> provideValuesSubtractDayTimeDurationFromTime() {
1193         return Stream.of(
1194             Arguments.of(
1195                 time("09:57:00"),
1196                 time("11:12:00"),
1197                 dayTimeDuration("P3DT1H15M")),
1198             Arguments.of(
1199                 time("22:10:00-05:00"),
1200                 time("08:20:00-05:00"),
1201                 dayTimeDuration("P23DT10H10M")));
1202       }
1203 
1204       @ParameterizedTest
1205       @DisplayName("op:subtract-dayTimeDuration-from-time")
1206       @MethodSource("provideValuesSubtractDayTimeDurationFromTime")
1207       void testOpSubtractDayTimeDurationFromTime(
1208           @NonNull ITimeItem expected,
1209           @NonNull ITimeItem item1,
1210           @NonNull IDayTimeDurationItem item2) {
1211         assertEquals(expected, OperationFunctions.opSubtractDayTimeDurationFromTime(item1, item2));
1212       }
1213     }
1214   }
1215 
1216   @Nested
1217   @DisplayName("Functions and operators related to QNames")
1218   @TestInstance(TestInstance.Lifecycle.PER_CLASS)
1219   class QName {
1220     // TODO: op:QName-equal
1221   }
1222 
1223   @Nested
1224   @DisplayName("Operators on base64Binary and hexBinary")
1225   @TestInstance(TestInstance.Lifecycle.PER_CLASS)
1226   class Binary {
1227     @Nested
1228     @DisplayName("Comparisons of base64Binary and hexBinary values")
1229     @TestInstance(TestInstance.Lifecycle.PER_CLASS)
1230     class BinaryOperators {
1231       // TODO: op:hexBinary-equal
1232       // TODO: op:hexBinary-less-than
1233       // TODO: op:hexBinary-greater-than
1234       // TODO: op:base64Binary-equal
1235       // TODO: op:base64Binary-less-than
1236       // TODO: op:base64Binary-greater-than
1237     }
1238   }
1239 }