1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.cst.math;
7   
8   import java.util.Map;
9   
10  import dev.metaschema.core.metapath.DynamicContext;
11  import dev.metaschema.core.metapath.IExpression;
12  import dev.metaschema.core.metapath.function.FunctionUtils;
13  import dev.metaschema.core.metapath.item.ISequence;
14  import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
15  import dev.metaschema.core.metapath.item.atomic.INumericItem;
16  import edu.umd.cs.findbugs.annotations.NonNull;
17  import edu.umd.cs.findbugs.annotations.Nullable;
18  
19  /**
20   * An immutable binary expression that supports basic arithmetic evaluation.
21   * <p>
22   * The result type is determined through static analysis of the sub-expressions,
23   * which may result in a more specific type that is a sub-class of the base
24   * result type.
25   * <p>
26   * The arithmetic operation method
27   * {@link #operation(IAnyAtomicItem, IAnyAtomicItem, DynamicContext)} must be
28   * implemented by extending classes to provide the evaluation logic.
29   */
30  public abstract class AbstractBasicArithmeticExpression
31      extends AbstractArithmeticExpression<IAnyAtomicItem> {
32  
33    /**
34     * An expression that represents a basic arithmetic operation on two values.
35     *
36     * @param text
37     *          the parsed text of the expression
38     * @param left
39     *          the first item
40     * @param right
41     *          the second item
42     */
43    public AbstractBasicArithmeticExpression(
44        @NonNull String text,
45        @NonNull IExpression left,
46        @NonNull IExpression right) {
47      super(text, left, right, IAnyAtomicItem.class);
48    }
49  
50    @Override
51    public Class<IAnyAtomicItem> getBaseResultType() {
52      return IAnyAtomicItem.class;
53    }
54  
55    @Override
56    protected ISequence<? extends IAnyAtomicItem> evaluate(DynamicContext dynamicContext, ISequence<?> focus) {
57      IAnyAtomicItem leftItem = ISequence.of(getLeft().accept(dynamicContext, focus).atomize()).getFirstItem(true);
58      IAnyAtomicItem rightItem = ISequence.of(getRight().accept(dynamicContext, focus).atomize()).getFirstItem(true);
59  
60      return resultOrEmpty(leftItem, rightItem, dynamicContext);
61    }
62  
63    /**
64     * Setup the operation on two atomic items.
65     *
66     * @param leftItem
67     *          the first item
68     * @param rightItem
69     *          the second item
70     * @param dynamicContext
71     *          used to provide evaluation information, including the implicit
72     *          timezone
73     * @return the result of the operation or an empty {@link ISequence} if either
74     *         item is {@code null}
75     */
76    @NonNull
77    protected ISequence<? extends IAnyAtomicItem> resultOrEmpty(
78        @Nullable IAnyAtomicItem leftItem,
79        @Nullable IAnyAtomicItem rightItem,
80        @NonNull DynamicContext dynamicContext) {
81      ISequence<? extends IAnyAtomicItem> retval;
82      if (leftItem == null || rightItem == null) {
83        retval = ISequence.empty();
84      } else {
85        IAnyAtomicItem result = operation(leftItem, rightItem, dynamicContext);
86        retval = ISequence.of(result);
87      }
88      return retval;
89    }
90  
91    /**
92     * Performs the arithmetic operation using the two provided values.
93     *
94     * @param left
95     *          the first item
96     * @param right
97     *          the second item
98     * @param dynamicContext
99     *          used to provide evaluation information, including the implicit
100    *          timezone
101    * @return the result of the operation
102    */
103   @NonNull
104   protected IAnyAtomicItem operation(
105       @NonNull IAnyAtomicItem left,
106       @NonNull IAnyAtomicItem right,
107       @NonNull DynamicContext dynamicContext) {
108 
109     Map<
110         Class<? extends IAnyAtomicItem>,
111         Map<Class<? extends IAnyAtomicItem>, OperationStrategy>> strategies = getStrategies();
112     Class<? extends IAnyAtomicItem> leftClass = left.getClass();
113 
114     // Find matching strategy for minuend type
115     Map<Class<? extends IAnyAtomicItem>, OperationStrategy> typeStrategies = null;
116     for (Map.Entry<Class<? extends IAnyAtomicItem>,
117         Map<Class<? extends IAnyAtomicItem>, OperationStrategy>> entry : strategies.entrySet()) {
118       if (entry.getKey().isAssignableFrom(leftClass)) {
119         // this is a matching strategy map
120         typeStrategies = entry.getValue();
121         break;
122       }
123     }
124 
125     if (typeStrategies == null) {
126       return operationAsNumeric(
127           FunctionUtils.castToNumeric(left),
128           FunctionUtils.castToNumeric(right));
129     }
130 
131     // Find matching strategy for subtrahend type
132     Class<? extends IAnyAtomicItem> rightClass = right.getClass();
133     for (Map.Entry<Class<? extends IAnyAtomicItem>, OperationStrategy> entry : typeStrategies.entrySet()) {
134       if (entry.getKey().isAssignableFrom(rightClass)) {
135         // this is matching strategy, execute it
136         return entry.getValue().execute(left, right, dynamicContext);
137       }
138     }
139 
140     throw new UnsupportedOperationException(unsupportedMessage(
141         left.toSignature(),
142         right.toSignature()));
143   }
144 
145   /**
146    * Provides a mapping of the left class to a mapping of the right class and the
147    * strategy to use to compute the operation.
148    * <p>
149    * This mapping is used to lookup the strategy to use based on the left and
150    * right classes.
151    *
152    * @return the mapping
153    */
154   @NonNull
155   protected abstract Map<
156       Class<? extends IAnyAtomicItem>,
157       Map<Class<? extends IAnyAtomicItem>, OperationStrategy>> getStrategies();
158 
159   /**
160    * Generates an error message for unsupported operand types.
161    *
162    * @param left
163    *          the string representation of the left operand type
164    * @param right
165    *          the string representation of the right operand type
166    * @return the formatted error message
167    */
168   @NonNull
169   protected abstract String unsupportedMessage(@NonNull String left, @NonNull String right);
170 
171   /**
172    * Performs the arithmetic operation on numeric items.
173    *
174    * @param left
175    *          the first numeric item
176    * @param right
177    *          the second numeric item
178    * @return the result of the numeric operation
179    */
180   @NonNull
181   protected abstract INumericItem operationAsNumeric(@NonNull INumericItem left, @NonNull INumericItem right);
182 
183   /**
184    * Provides a callback for resolving arithmetic operations.
185    */
186   @FunctionalInterface
187   protected interface OperationStrategy {
188     /**
189      * Called to execute an arithmetic operation.
190      *
191      * @param left
192      *          the left side of the arithmetic operation
193      * @param right
194      *          the right side of the arithmetic operation
195      * @param dynamicContext
196      *          the evaluation dynamic context
197      * @return the arithmetic result
198      */
199     @NonNull
200     IAnyAtomicItem execute(
201         @NonNull IAnyAtomicItem left,
202         @NonNull IAnyAtomicItem right,
203         @NonNull DynamicContext dynamicContext);
204   }
205 }