001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.bson;
018
019import java.math.*;
020
021/**
022 * IEEE 754-2008 Decimal128 BID (Binary Integer Decimal) encoding for BSON.
023 *
024 * <p>
025 * Supports conversion between <code>BigDecimal</code> and the 16-byte BSON Decimal128 format.
026 * Used by {@link BsonSerializer} and {@link BsonParser} for decimal values.
027 *
028 * <h5 class='section'>See Also:</h5><ul>
029 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Bson">BSON</a>
030 *    <li class='link'><a class="doclink" href="https://bsonspec.org/spec.html">BSON Specification</a>
031 * </ul>
032 */
033@SuppressWarnings({
034   "java:S115" // NaN_MASK uses industry-standard NaN casing (Not-a-Number) rather than all-caps NANMASK
035})
036public final class BsonDecimal128 {
037
038   private static final MathContext DECIMAL128 = new MathContext(34);
039   private static final long SIGN_BIT_MASK = 1L << 63;
040   private static final long INFINITY_MASK = 0x7800000000000000L;
041   private static final long NaN_MASK = 0x7c00000000000000L;
042   private static final int MIN_EXPONENT = -6176;
043   private static final int MAX_EXPONENT = 6111;
044   private static final int EXPONENT_OFFSET = 6176;
045   private static final int MAX_BIT_LENGTH = 113;
046   private static final BigInteger TEN = BigInteger.TEN;
047   private static final BigInteger ZERO = BigInteger.ZERO;
048   private static final BigInteger ONE = BigInteger.ONE;
049
050   private final long high;
051   private final long low;
052
053   private BsonDecimal128(long high, long low) {
054      this.high = high;
055      this.low = low;
056   }
057
058   /**
059    * Creates a Decimal128 from the given high and low 64-bit values (IEEE 754-2008 BID encoding).
060    *
061    * @param high The high-order 64 bits.
062    * @param low The low-order 64 bits.
063    * @return The Decimal128 instance.
064    */
065   public static BsonDecimal128 fromIEEE754BIDEncoding(long high, long low) {
066      return new BsonDecimal128(high, low);
067   }
068
069   /**
070    * Creates a Decimal128 from the given BigDecimal.
071    *
072    * @param value The decimal value.
073    *    <br>Must not be <jk>null</jk>.
074    * @return The Decimal128 instance.
075    * @throws NumberFormatException If the value is out of Decimal128 range.
076    */
077   public static BsonDecimal128 fromBigDecimal(BigDecimal value) {
078      return new BsonDecimal128(value, value.signum() == -1);
079   }
080
081   private BsonDecimal128(BigDecimal initialValue, boolean isNegative) {
082      var value = clampAndRound(initialValue);
083      long localHigh = 0;
084      long localLow = 0;
085
086      int exponent = -value.scale();
087      if (exponent < MIN_EXPONENT || exponent > MAX_EXPONENT)
088         throw new NumberFormatException("Exponent out of Decimal128 range: " + exponent);
089
090      if (value.unscaledValue().bitLength() > MAX_BIT_LENGTH)
091         throw new NumberFormatException("Significand out of Decimal128 range");
092
093      var significand = value.unscaledValue().abs();
094      int bitLength = significand.bitLength();
095
096      for (var i = 0; i < Math.min(64, bitLength); i++)
097         if (significand.testBit(i))
098            localLow |= 1L << i;
099
100      for (var i = 64; i < bitLength; i++)
101         if (significand.testBit(i))
102            localHigh |= 1L << (i - 64);
103
104      long biasedExponent = (long)exponent + EXPONENT_OFFSET;
105      localHigh |= biasedExponent << 49;
106
107      if (value.signum() == -1 || isNegative)
108         localHigh |= SIGN_BIT_MASK;
109
110      this.high = localHigh;
111      this.low = localLow;
112   }
113
114   private static BigDecimal clampAndRound(BigDecimal initialValue) {
115      BigDecimal value;
116      if (-initialValue.scale() > MAX_EXPONENT) {
117         int diff = -initialValue.scale() - MAX_EXPONENT;
118         if (initialValue.unscaledValue().equals(ZERO))
119            value = new BigDecimal(initialValue.unscaledValue(), -MAX_EXPONENT);
120         else if (diff + initialValue.precision() > 34)
121            throw new NumberFormatException("Exponent out of Decimal128 range: " + initialValue);
122         else {
123            var multiplier = TEN.pow(diff);
124            value = new BigDecimal(initialValue.unscaledValue().multiply(multiplier), initialValue.scale() + diff);
125         }
126      } else if (-initialValue.scale() < MIN_EXPONENT) {
127         int diff = initialValue.scale() + MIN_EXPONENT;
128         int undiscardedPrecision = ensureExactRounding(initialValue, diff);
129         var divisor = undiscardedPrecision == 0 ? ONE : TEN.pow(diff);
130         value = new BigDecimal(initialValue.unscaledValue().divide(divisor), initialValue.scale() - diff);
131      } else {
132         value = initialValue.round(DECIMAL128);
133         int extraPrecision = initialValue.precision() - value.precision();
134         if (extraPrecision > 0)
135            ensureExactRounding(initialValue, extraPrecision);
136      }
137      return value;
138   }
139
140   private static int ensureExactRounding(BigDecimal initialValue, int extraPrecision) {
141      var significand = initialValue.unscaledValue().abs().toString();
142      int undiscardedPrecision = Math.max(0, significand.length() - extraPrecision);
143      for (var i = undiscardedPrecision; i < significand.length(); i++)
144         if (significand.charAt(i) != '0')
145            throw new NumberFormatException("Conversion to Decimal128 would require inexact rounding: " + initialValue);
146      return undiscardedPrecision;
147   }
148
149   /**
150    * Returns the high-order 64 bits.
151    *
152    * @return The high bits.
153    */
154   public long getHigh() {
155      return high;
156   }
157
158   /**
159    * Returns the low-order 64 bits.
160    *
161    * @return The low bits.
162    */
163   public long getLow() {
164      return low;
165   }
166
167   /**
168    * Converts to BigDecimal.
169    *
170    * @return The equivalent BigDecimal.
171    * @throws ArithmeticException If the value is NaN, Infinity, or negative zero.
172    */
173   public BigDecimal toBigDecimal() {
174      if (isNaN())
175         throw new ArithmeticException("NaN cannot be converted to BigDecimal");
176      if (isInfinite())
177         throw new ArithmeticException("Infinity cannot be converted to BigDecimal");
178
179      var bd = toBigDecimalNoNegativeZeroCheck();
180      if (isNegative() && bd.signum() == 0)
181         throw new ArithmeticException("Negative zero cannot be converted to BigDecimal");
182      return bd;
183   }
184
185   /**
186    * Converts to BigDecimal, normalizing the special IEEE-754 values that have no BigDecimal representation.
187    *
188    * <p>
189    * Unlike {@link #toBigDecimal()}, this never throws: <c>NaN</c> and <c>±Infinity</c> (both valid on the BSON
190    * wire and emitted by MongoDB) map to <jk>null</jk>, and negative zero maps to a finite zero.  Used by the
191    * parser so adversarial/MongoDB-sourced special decimals fail soft instead of aborting the whole parse with an
192    * unchecked {@link ArithmeticException}.
193    *
194    * @return The equivalent BigDecimal, or <jk>null</jk> if the value is NaN or Infinity.
195    */
196   public BigDecimal toBigDecimalOrNull() {
197      if (isNaN() || isInfinite())
198         return null;
199      return toBigDecimalNoNegativeZeroCheck();
200   }
201
202   private boolean isNaN() {
203      return (high & NaN_MASK) == NaN_MASK;
204   }
205
206   private boolean isInfinite() {
207      return (high & INFINITY_MASK) == INFINITY_MASK;
208   }
209
210   private boolean isNegative() {
211      return (high & SIGN_BIT_MASK) == SIGN_BIT_MASK;
212   }
213
214   private boolean twoHighestCombinationBitsAreSet() {
215      return (high & 3L << 61) == 3L << 61;
216   }
217
218   private int getExponent() {
219      if (twoHighestCombinationBitsAreSet())
220         return (int)((high & 0x1fffe00000000000L) >>> 47) - EXPONENT_OFFSET;
221      return (int)((high & 0x7fff800000000000L) >>> 49) - EXPONENT_OFFSET;
222   }
223
224   private BigDecimal toBigDecimalNoNegativeZeroCheck() {
225      int scale = -getExponent();
226      if (twoHighestCombinationBitsAreSet())
227         return BigDecimal.valueOf(0, scale);
228      return new BigDecimal(new BigInteger(isNegative() ? -1 : 1, getSignificandBytes()), scale);
229   }
230
231   private byte[] getSignificandBytes() {
232      var bytes = new byte[15];
233      long mask = 0xFF;
234      for (var i = 14; i >= 7; i--) {
235         bytes[i] = (byte)((low & mask) >>> ((14 - i) << 3));
236         mask <<= 8;
237      }
238      mask = 0xFF;
239      for (var i = 6; i >= 1; i--) {
240         bytes[i] = (byte)((high & mask) >>> ((6 - i) << 3));
241         mask <<= 8;
242      }
243      bytes[0] = (byte)((high & 0x0001000000000000L) >>> 48);
244      return bytes;
245   }
246
247}