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}