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.RoundingMode;
10  
11  import dev.metaschema.core.datatype.adapter.DecimalAdapter;
12  import dev.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
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.item.atomic.impl.DecimalItemImpl;
17  import dev.metaschema.core.metapath.type.IAtomicOrUnionType;
18  import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
19  import dev.metaschema.core.util.ObjectUtils;
20  import edu.umd.cs.findbugs.annotations.NonNull;
21  
22  /**
23   * An atomic Metapath item containing a decimal data value.
24   */
25  public interface IDecimalItem extends INumericItem {
26    /**
27     * The decimal item with the value "0".
28     */
29    @NonNull
30    IDecimalItem ZERO = valueOf(ObjectUtils.notNull(BigDecimal.ZERO));
31  
32    /**
33     * Get the type information for this item.
34     *
35     * @return the type information
36     */
37    @NonNull
38    static IAtomicOrUnionType<IDecimalItem> type() {
39      return MetaschemaDataTypeProvider.DECIMAL.getItemType();
40    }
41  
42    @Override
43    default IAtomicOrUnionType<? extends IDecimalItem> getType() {
44      return type();
45    }
46  
47    /**
48     * Construct a new decimal item using the provided string {@code value}.
49     *
50     * @param value
51     *          a string representing a decimal value
52     * @return the new item
53     * @throws InvalidTypeMetapathException
54     *           if the given string is not a decimal value
55     */
56    @NonNull
57    static IDecimalItem valueOf(@NonNull String value) {
58      try {
59        return valueOf(MetaschemaDataTypeProvider.DECIMAL.parse(value));
60      } catch (IllegalArgumentException ex) {
61        throw new InvalidTypeMetapathException(
62            null,
63            String.format("Invalid decimal value '%s'. %s",
64                value,
65                ex.getLocalizedMessage()),
66            ex);
67      }
68    }
69  
70    /**
71     * Construct a new decimal item using the provided {@code value}.
72     *
73     * @param value
74     *          a long value
75     * @return the new item
76     */
77    @NonNull
78    static IDecimalItem valueOf(long value) {
79      return valueOf(ObjectUtils.notNull(BigDecimal.valueOf(value)));
80    }
81  
82    /**
83     * Construct a new decimal item using the provided {@code value}.
84     *
85     * @param value
86     *          a double value
87     * @return the new item
88     */
89    @NonNull
90    static IDecimalItem valueOf(double value) {
91      return valueOf(ObjectUtils.notNull(Double.toString(value)));
92    }
93  
94    /**
95     * Construct a new decimal item using the provided {@code value}.
96     *
97     * @param value
98     *          a double value
99     * @return the new item
100    */
101   @NonNull
102   static IDecimalItem valueOf(boolean value) {
103     return valueOf(DecimalItemImpl.toBigDecimal(value));
104   }
105 
106   /**
107    * Construct a new decimal item using the provided {@code value}.
108    *
109    * @param value
110    *          a decimal value
111    * @return the new item
112    */
113   @NonNull
114   static IDecimalItem valueOf(@NonNull BigDecimal value) {
115     return new DecimalItemImpl(value);
116   }
117 
118   /**
119    * Cast the provided type to this item type.
120    *
121    * @param item
122    *          the item to cast
123    * @return the original item if it is already this type, otherwise a new item
124    *         cast to this type
125    * @throws InvalidValueForCastFunctionException
126    *           if the provided {@code item} cannot be cast to this type
127    */
128   @NonNull
129   static IDecimalItem cast(@NonNull IAnyAtomicItem item) {
130     IDecimalItem retval;
131     if (item instanceof IDecimalItem) {
132       retval = (IDecimalItem) item;
133     } else if (item instanceof INumericItem) {
134       retval = valueOf(((INumericItem) item).asDecimal());
135     } else if (item instanceof IBooleanItem) {
136       retval = valueOf(((IBooleanItem) item).toBoolean());
137     } else {
138       try {
139         retval = valueOf(item.asString());
140       } catch (IllegalStateException | InvalidTypeMetapathException ex) {
141         // asString can throw IllegalStateException exception
142         throw new InvalidValueForCastFunctionException(ex);
143       }
144     }
145     return retval;
146   }
147 
148   @Override
149   default IDecimalItem castAsType(IAnyAtomicItem item) {
150     return cast(item);
151   }
152 
153   @Override
154   default boolean toEffectiveBoolean() {
155     return !BigDecimal.ZERO.equals(asDecimal());
156   }
157 
158   @SuppressWarnings("null")
159   @Override
160   default INumericItem abs() {
161     return new DecimalItemImpl(asDecimal().abs());
162   }
163 
164   @SuppressWarnings("null")
165   @Override
166   default IIntegerItem ceiling() {
167     return IIntegerItem.valueOf(asDecimal().setScale(0, RoundingMode.CEILING).toBigIntegerExact());
168   }
169 
170   @SuppressWarnings("null")
171   @Override
172   default IIntegerItem floor() {
173     return IIntegerItem.valueOf(asDecimal().setScale(0, RoundingMode.FLOOR).toBigIntegerExact());
174   }
175 
176   /**
177    * Convert this decimal item to a Java int, exactly. If the decimal is not in a
178    * valid int range, an exception is thrown.
179    *
180    * @return the int value
181    * @throws CastFunctionException
182    *           if the value does not fit in an int
183    */
184   @Override
185   default int toIntValueExact() {
186     try {
187       // asDecimal() yields a BigDecimal, so we can call intValueExact().
188       // Throw an exception if it does not fit in a 32-bit int.
189       return asDecimal().intValueExact();
190     } catch (ArithmeticException ex) {
191       throw new CastFunctionException(
192           CastFunctionException.INPUT_VALUE_TOO_LARGE,
193           this,
194           String.format("Decimal value '%s' is out of range for a Java int.", asString()),
195           ex);
196     }
197   }
198 
199   /**
200    * Create a new sum by adding this value to the provided addend value.
201    *
202    * @param addend
203    *          the second value to sum
204    * @return a new value resulting from adding this value to the provided addend
205    *         value
206    */
207   @NonNull
208   default IDecimalItem add(@NonNull IDecimalItem addend) {
209     BigDecimal addendLeft = asDecimal();
210     BigDecimal addendRight = addend.asDecimal();
211     return valueOf(ObjectUtils.notNull(addendLeft.add(addendRight)));
212   }
213 
214   /**
215    * Determine the difference by subtracting the provided subtrahend value from
216    * this minuend value.
217    *
218    * @param subtrahend
219    *          the value to subtract
220    * @return a new value resulting from subtracting the subtrahend from the
221    *         minuend
222    */
223   @NonNull
224   default IDecimalItem subtract(@NonNull IDecimalItem subtrahend) {
225     BigDecimal minuendDecimal = asDecimal();
226     BigDecimal subtrahendDecimal = subtrahend.asDecimal();
227     return valueOf(ObjectUtils.notNull(minuendDecimal.subtract(subtrahendDecimal, DecimalAdapter.mathContext())));
228   }
229 
230   /**
231    * Multiply this multiplicand value by the provided multiplier value.
232    *
233    * @param multiplier
234    *          the value to multiply by
235    * @return a new value resulting from multiplying the multiplicand by the
236    *         multiplier
237    */
238   @NonNull
239   default IDecimalItem multiply(@NonNull IDecimalItem multiplier) {
240     return valueOf(ObjectUtils.notNull(asDecimal().multiply(multiplier.asDecimal(), DecimalAdapter.mathContext())));
241   }
242 
243   /**
244    * Divide this dividend value by the provided divisor value.
245    *
246    * @param divisor
247    *          the value to divide by
248    * @return a new value resulting from dividing the dividend by the divisor
249    * @throws ArithmeticFunctionException
250    *           with the code {@link ArithmeticFunctionException#DIVISION_BY_ZERO}
251    *           if the divisor is zero
252    */
253   @NonNull
254   default IDecimalItem divide(@NonNull IDecimalItem divisor) {
255     // create a decimal result
256     BigDecimal divisorDecimal = divisor.asDecimal();
257 
258     if (BigDecimal.ZERO.compareTo(divisorDecimal) == 0) {
259       throw new ArithmeticFunctionException(ArithmeticFunctionException.DIVISION_BY_ZERO,
260           ArithmeticFunctionException.DIVISION_BY_ZERO_MESSAGE);
261     }
262     return valueOf(ObjectUtils.notNull(asDecimal().divide(divisorDecimal, DecimalAdapter.mathContext())));
263   }
264 
265   /**
266    * Divide this dividend value by the provided divisor value using integer
267    * division.
268    *
269    * @param divisor
270    *          the value to divide by
271    * @return a new value resulting from dividing the dividend by the divisor
272    */
273   @Override
274   @NonNull
275   default IIntegerItem integerDivide(INumericItem divisor) {
276     // create a decimal result
277     BigDecimal decimalDivisor = divisor.asDecimal();
278 
279     if (BigDecimal.ZERO.compareTo(decimalDivisor) == 0) {
280       throw new ArithmeticFunctionException(ArithmeticFunctionException.DIVISION_BY_ZERO,
281           ArithmeticFunctionException.DIVISION_BY_ZERO_MESSAGE);
282     }
283 
284     BigDecimal decimalDividend = asDecimal();
285     return IIntegerItem.valueOf(
286         ObjectUtils.notNull(decimalDividend
287             .divideToIntegralValue(decimalDivisor, DecimalAdapter.mathContext()).toBigInteger()));
288   }
289 
290   /**
291    * Compute the remainder when dividing this dividend value by the provided
292    * divisor value.
293    *
294    * @param divisor
295    *          the value to divide by
296    * @return a new value containing the remainder resulting from dividing the
297    *         dividend by the divisor
298    */
299   @Override
300   @NonNull
301   default IDecimalItem mod(INumericItem divisor) {
302     // create a decimal result
303     BigDecimal decimalDivisor = divisor.asDecimal();
304 
305     if (BigDecimal.ZERO.compareTo(decimalDivisor) == 0) {
306       throw new ArithmeticFunctionException(ArithmeticFunctionException.DIVISION_BY_ZERO,
307           ArithmeticFunctionException.DIVISION_BY_ZERO_MESSAGE);
308     }
309     return valueOf(ObjectUtils.notNull(asDecimal().remainder(decimalDivisor, DecimalAdapter.mathContext())));
310   }
311 
312   @Override
313   default IDecimalItem negate() {
314     return valueOf(ObjectUtils.notNull(asDecimal().negate(DecimalAdapter.mathContext())));
315   }
316 
317   /**
318    * Compares this value with the argument.
319    *
320    * @param item
321    *          the item to compare with this value
322    * @return a negative integer, zero, or a positive integer if this value is less
323    *         than, equal to, or greater than the {@code item}.
324    */
325   default int compareTo(@NonNull IDecimalItem item) {
326     return asDecimal().compareTo(item.asDecimal());
327   }
328 }