1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.item.atomic;
7   
8   import java.math.BigDecimal;
9   import java.math.BigInteger;
10  import java.math.MathContext;
11  import java.math.RoundingMode;
12  
13  import dev.metaschema.core.metapath.function.ArithmeticFunctionException;
14  import dev.metaschema.core.metapath.function.CastFunctionException;
15  import dev.metaschema.core.metapath.function.InvalidValueForCastFunctionException;
16  import dev.metaschema.core.metapath.function.impl.OperationFunctions;
17  import dev.metaschema.core.metapath.type.IAtomicOrUnionType;
18  import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
19  import dev.metaschema.core.metapath.type.impl.TypeConstants;
20  import dev.metaschema.core.util.ObjectUtils;
21  import edu.umd.cs.findbugs.annotations.NonNull;
22  
23  /**
24   * Represents an atomic Metapath item containing a numeric data value, which can
25   * be either an integer or decimal. This interface provides operations for
26   * numeric type conversion, comparison, and mathematical operations commonly
27   * used in Metapath expressions.
28   *
29   * @see IIntegerItem
30   * @see IDecimalItem
31   */
32  public interface INumericItem extends IAnyAtomicItem {
33    /**
34     * Get the type information for this item.
35     *
36     * @return the type information
37     */
38    @NonNull
39    static IAtomicOrUnionType<INumericItem> type() {
40      return TypeConstants.NUMERIC_TYPE;
41    }
42  
43    /**
44     * Cast the provided type to this item type.
45     * <p>
46     * Per XPath 3.1, boolean values are cast as: {@code true} → 1, {@code false} →
47     * 0.
48     *
49     * @param item
50     *          the item to cast
51     * @return the original item if it is already this type, otherwise a new item
52     *         cast to this type
53     * @throws InvalidValueForCastFunctionException
54     *           if the provided {@code item} cannot be cast to this type
55     */
56    @NonNull
57    static INumericItem cast(@NonNull IAnyAtomicItem item) {
58      INumericItem retval;
59      if (item instanceof INumericItem) {
60        retval = (INumericItem) item;
61      } else if (item instanceof IBooleanItem) {
62        // XPath 3.1: boolean true -> 1, false -> 0
63        retval = IIntegerItem.valueOf(((IBooleanItem) item).toBoolean());
64      } else {
65        try {
66          retval = IDecimalItem.valueOf(item.asString());
67        } catch (IllegalStateException | InvalidTypeMetapathException ex) {
68          // asString can throw IllegalStateException exception
69          throw new InvalidValueForCastFunctionException(ex);
70        }
71      }
72      return retval;
73    }
74  
75    /**
76     * Get this item's value as a decimal.
77     *
78     * @return the equivalent decimal value
79     */
80    @NonNull
81    BigDecimal asDecimal();
82  
83    /**
84     * Get this item's value as an integer.
85     *
86     * @return the equivalent integer value
87     */
88    @NonNull
89    BigInteger asInteger();
90  
91    /**
92     * Convert this numeric item to a Java int, exactly. If the value is not in a
93     * valid int range, an exception is thrown.
94     *
95     * @return the int value
96     * @throws CastFunctionException
97     *           if the value does not fit in an int
98     */
99    int toIntValueExact();
100 
101   /**
102    * Get the effective boolean value of this item based on
103    * <a href="https://www.w3.org/TR/xpath-31/#id-ebv">XPath 3.1</a>.
104    *
105    * @return the effective boolean value
106    */
107   boolean toEffectiveBoolean();
108 
109   @Override
110   INumericItem castAsType(IAnyAtomicItem item);
111 
112   /**
113    * Get the absolute value of the item.
114    *
115    * @return this item negated if this item is negative, or the item otherwise
116    */
117   @NonNull
118   INumericItem abs();
119 
120   /**
121    * Round the value to the whole number closest to positive infinity.
122    *
123    * @return the rounded value
124    */
125   @NonNull
126   IIntegerItem ceiling();
127 
128   /**
129    * Round the value to the whole number closest to negative infinity.
130    *
131    * @return the rounded value
132    */
133   @NonNull
134   IIntegerItem floor();
135 
136   /**
137    * Round the item's value with zero precision.
138    * <p>
139    * This is the same as calling {@link #round(IIntegerItem)} with a precision of
140    * {@code 0}.
141    *
142    * @return the rounded value
143    */
144   @NonNull
145   default INumericItem round() {
146     return round(IIntegerItem.ZERO);
147   }
148 
149   /**
150    * Round the item's value with the specified precision.
151    * <p>
152    * This is the same as calling {@link #round(IIntegerItem)} with a precision of
153    * {@code 0}.
154    *
155    * @param precisionItem
156    *          the precision indicating the number of digits to round to before
157    *          (negative value} or after (positive value) the decimal point.
158    * @return the rounded value
159    */
160   @NonNull
161   default INumericItem round(@NonNull IIntegerItem precisionItem) {
162     int precision;
163     try {
164       precision = precisionItem.toIntValueExact();
165     } catch (CastFunctionException ex) {
166       throw new ArithmeticFunctionException(ArithmeticFunctionException.OVERFLOW_UNDERFLOW_ERROR,
167           "Numeric operation overflow/underflow.", ex);
168     }
169     return precision >= 0
170         ? roundWithPositivePrecision(precision)
171         : roundWithNegativePrecision(precision);
172   }
173 
174   @NonNull
175   private INumericItem roundWithPositivePrecision(int precision) {
176     INumericItem retval;
177     if (this instanceof IIntegerItem) {
178       retval = this;
179     } else {
180       BigDecimal value = asDecimal();
181       BigDecimal rounded = value.signum() == -1
182           ? value.round(new MathContext(precision + value.precision() - value.scale(), RoundingMode.HALF_DOWN))
183           : value.round(new MathContext(precision + value.precision() - value.scale(), RoundingMode.HALF_UP));
184       retval = castAsType(IDecimalItem.valueOf(ObjectUtils.notNull(rounded)));
185     }
186     return retval;
187   }
188 
189   /**
190    * Rounds a number to the specified negative precision by: 1. Computing the
191    * divisor (10^|precision|) 2. If the absolute value is less than the divisor,
192    * returns 0 3. Otherwise, rounds to the nearest multiple of the divisor
193    *
194    * @param precision
195    *          the negative precision to round to
196    * @return the rounded value
197    */
198   @NonNull
199   private INumericItem roundWithNegativePrecision(int precision) {
200     BigInteger value = asInteger();
201     BigInteger divisor = BigInteger.TEN.pow(0 - precision);
202 
203     INumericItem retval;
204     if (divisor.compareTo(value.abs()) > 0) {
205       retval = IIntegerItem.ZERO;
206     } else {
207       BigInteger remainder = value.mod(divisor);
208       BigInteger lessRemainder = value.subtract(remainder);
209       BigInteger halfDivisor = divisor.divide(BigInteger.TWO);
210       BigInteger roundedValue = remainder.compareTo(halfDivisor) >= 0
211           ? lessRemainder.add(divisor)
212           : lessRemainder;
213       retval = IIntegerItem.valueOf(ObjectUtils.notNull(roundedValue));
214     }
215     return retval;
216   }
217 
218   /**
219    * Create a new sum by adding this value to the provided addend value.
220    *
221    * @param addend
222    *          the second value to sum
223    * @return a new value resulting from adding this value to the provided addend
224    *         value
225    */
226   @NonNull
227   default INumericItem add(@NonNull INumericItem addend) {
228     return OperationFunctions.opNumericAdd(this, addend);
229   }
230 
231   /**
232    * Determine the difference by subtracting the provided subtrahend value from
233    * this minuend value.
234    *
235    * @param subtrahend
236    *          the value to subtract
237    * @return a new value resulting from subtracting the subtrahend from the
238    *         minuend
239    */
240   @NonNull
241   default INumericItem subtract(@NonNull INumericItem subtrahend) {
242     return OperationFunctions.opNumericSubtract(this, subtrahend);
243   }
244 
245   /**
246    * Multiply this multiplicand value by the provided multiplier value.
247    *
248    * @param multiplier
249    *          the value to multiply by
250    * @return a new value resulting from multiplying the multiplicand by the
251    *         multiplier
252    */
253   @NonNull
254   default INumericItem multiply(@NonNull INumericItem multiplier) {
255     return OperationFunctions.opNumericMultiply(this, multiplier);
256   }
257 
258   /**
259    * Divide this dividend value by the provided divisor value.
260    *
261    * @param divisor
262    *          the value to divide by
263    * @return a new value resulting from dividing the dividend by the divisor
264    */
265   @NonNull
266   default INumericItem divide(@NonNull INumericItem divisor) {
267     return OperationFunctions.opNumericDivide(this, divisor);
268   }
269 
270   /**
271    * Divide this dividend value by the provided divisor value using integer
272    * division.
273    *
274    * @param divisor
275    *          the value to divide by
276    * @return a new value resulting from dividing the dividend by the divisor
277    */
278   @NonNull
279   default IIntegerItem integerDivide(@NonNull INumericItem divisor) {
280     return OperationFunctions.opNumericIntegerDivide(this, divisor);
281   }
282 
283   /**
284    * Compute the remainder when dividing this dividend value by the provided
285    * divisor value.
286    *
287    * @param divisor
288    *          the value to divide by
289    * @return a new value containing the remainder resulting from dividing the
290    *         dividend by the divisor
291    */
292   @NonNull
293   default INumericItem mod(@NonNull INumericItem divisor) {
294     return OperationFunctions.opNumericMod(this, divisor);
295   }
296 
297   /**
298    * Reverse the sign of this value.
299    *
300    * @return a new value with the sign reversed
301    */
302   @NonNull
303   INumericItem negate();
304 }