001/* 002 * The MIT License (MIT) 003 * 004 * Copyright (c) 2015-2023 decimal4j (tools4j), Marco Terzer 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in all 014 * copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024package org.decimal4j.util; 025 026import org.decimal4j.api.DecimalArithmetic; 027import org.decimal4j.scale.ScaleMetrics; 028import org.decimal4j.scale.Scales; 029 030import java.math.RoundingMode; 031import java.util.Objects; 032 033/** 034 * DoubleRounder Utility <b>(Deprecated)</b>. 035 * <p> 036 * DoubleRounder sometimes returns counter-intuitive results. The reason is that it performs mathematically 037 * correct rounding. For instance <code>DoubleRounder.round(256.025d, 2)</code> will be rounded down to 038 * <code>256.02</code> because the double value represented as <code>256.025d</code> is somewhat smaller than the rational 039 * value <code>256.025</code> and hence will be rounded down. 040 * <p> 041 * Notes: 042 * <ul> 043 * <li>This behaviour is very similar to that of the {@link java.math.BigDecimal#BigDecimal(double) BigDecimal(double)} 044 * constructor (but not to {@link java.math.BigDecimal#valueOf(double) valueOf(double)} which uses the string 045 * constructor).</li> 046 * <li>The problem can be circumvented with a double rounding step to a higher precision first, but it is complicated 047 * and we are not going into the details here</li> 048 * </ul> 049 * For those reasons we <b>cannot recommend to use DoubleRounder</b>. 050 */ 051@Deprecated 052public final class DoubleRounder { 053 054 private final ScaleMetrics scaleMetrics; 055 private final double ulp; 056 057 /** 058 * Creates a rounder for the given decimal precision. 059 * 060 * @param precision 061 * the decimal rounding precision, must be in {@code [0,18]} 062 * @throws IllegalArgumentException 063 * if precision is negative or larger than 18 064 */ 065 public DoubleRounder(int precision) { 066 this(toScaleMetrics(precision)); 067 } 068 069 /** 070 * Creates a rounder with the given scale metrics defining the decimal precision. 071 * 072 * @param scaleMetrics 073 * the scale metrics determining the rounding precision 074 * @throws NullPointerException 075 * if scale metrics is null 076 */ 077 public DoubleRounder(ScaleMetrics scaleMetrics) { 078 this.scaleMetrics = Objects.requireNonNull(scaleMetrics, "scaleMetrics cannot be null"); 079 this.ulp = scaleMetrics.getRoundingHalfEvenArithmetic().toDouble(1); 080 } 081 082 /** 083 * Returns the precision of this rounder, a value between zero and 18. 084 * 085 * @return this rounder's decimal precision 086 */ 087 public int getPrecision() { 088 return scaleMetrics.getScale(); 089 } 090 091 /** 092 * Rounds the given double value to the decimal precision of this rounder using {@link RoundingMode#HALF_UP HALF_UP} 093 * rounding. 094 * 095 * @param value 096 * the value to round 097 * @return the rounded value 098 * @see #getPrecision() 099 */ 100 public double round(double value) { 101 return round(value, scaleMetrics.getDefaultArithmetic(), scaleMetrics.getRoundingHalfEvenArithmetic(), ulp); 102 } 103 104 /** 105 * Rounds the given double value to the decimal precision of this rounder using the specified rounding mode. 106 * 107 * @param value 108 * the value to round 109 * @param roundingMode 110 * the rounding mode indicating how the least significant returned decimal digit of the result is to be 111 * calculated 112 * @return the rounded value 113 * @see #getPrecision() 114 */ 115 public double round(double value, RoundingMode roundingMode) { 116 return round(value, roundingMode, scaleMetrics.getRoundingHalfEvenArithmetic(), ulp); 117 } 118 119 /** 120 * Returns a hash code for this <code>DoubleRounder</code> instance. 121 * 122 * @return a hash code value for this object. 123 */ 124 @Override 125 public int hashCode() { 126 return scaleMetrics.hashCode(); 127 } 128 129 /** 130 * Returns true if {@code obj} is a <code>DoubleRounder</code> with the same precision as {@code this} rounder instance. 131 * 132 * @param obj 133 * the reference object with which to compare 134 * @return true for a double rounder with the same precision as this instance 135 */ 136 @Override 137 public boolean equals(Object obj) { 138 if (obj == this) 139 return true; 140 if (obj == null) 141 return false; 142 if (obj instanceof DoubleRounder) { 143 return scaleMetrics.equals(((DoubleRounder) obj).scaleMetrics); 144 } 145 return false; 146 } 147 148 /** 149 * Returns a string consisting of the simple class name and the precision. 150 * 151 * @return a string like "DoubleRounder[precision=7]" 152 */ 153 @Override 154 public String toString() { 155 return "DoubleRounder[precision=" + getPrecision() + "]"; 156 } 157 158 /** 159 * Rounds the given double value to the specified decimal {@code precision} using {@link RoundingMode#HALF_UP 160 * HALF_UP} rounding. 161 * 162 * @param value 163 * the value to round 164 * @param precision 165 * the decimal precision to round to (aka decimal places) 166 * @return the rounded value 167 */ 168 public static final double round(double value, int precision) { 169 final ScaleMetrics sm = toScaleMetrics(precision); 170 final DecimalArithmetic halfEvenArith = sm.getRoundingHalfEvenArithmetic(); 171 return round(value, sm.getDefaultArithmetic(), halfEvenArith, halfEvenArith.toDouble(1)); 172 } 173 174 /** 175 * Rounds the given double value to the specified decimal {@code precision} using the specified rounding mode. 176 * 177 * @param value 178 * the value to round 179 * @param precision 180 * the decimal precision to round to (aka decimal places) 181 * @param roundingMode 182 * the rounding mode indicating how the least significant returned decimal digit of the result is to be 183 * calculated 184 * @return the rounded value 185 */ 186 public static final double round(double value, int precision, RoundingMode roundingMode) { 187 final ScaleMetrics sm = toScaleMetrics(precision); 188 final DecimalArithmetic halfEvenArith = sm.getRoundingHalfEvenArithmetic(); 189 return round(value, roundingMode, halfEvenArith, halfEvenArith.toDouble(1)); 190 } 191 192 private static final double round(double value, RoundingMode roundingMode, DecimalArithmetic halfEvenArith, double ulp) { 193 if (roundingMode == RoundingMode.UNNECESSARY) { 194 return checkRoundingUnnecessary(value, halfEvenArith, ulp); 195 } 196 return round(value, halfEvenArith.deriveArithmetic(roundingMode), halfEvenArith, ulp); 197 } 198 199 private static final double round(double value, DecimalArithmetic roundingArith, DecimalArithmetic halfEvenArith, double ulp) { 200 //return the value unchanged if 201 // (a) the value is infinite or NaN 202 // (b) the next double is 2 decimal UPLs away (or more): 203 // in this case no other double value represents the decimal value more accurately 204 if (!isFinite(value) || ulp * 2 <= Math.ulp(value)) { 205 return value; 206 } 207 // NOTE: condition (b) above prevents overflows as such cases do not get to here 208 final long uDecimal = roundingArith.fromDouble(value); 209 return halfEvenArith.toDouble(uDecimal); 210 } 211 212 private static final double checkRoundingUnnecessary(double value, DecimalArithmetic halfEvenArith, double ulp) { 213 //same condition as in round(..) method above 214 if (isFinite(value) && 2 * ulp > Math.ulp(value)) { 215 //By definition, rounding is necessary if there is another double value that represents our decimal more 216 //accurately. This is the case when we get a different double value after two conversions. 217 final long uDecimal = halfEvenArith.fromDouble(value); 218 if (halfEvenArith.toDouble(uDecimal) != value) { 219 throw new ArithmeticException( 220 "Rounding necessary for precision " + halfEvenArith.getScale() + ": " + value); 221 } 222 } 223 return value; 224 } 225 226 private static final ScaleMetrics toScaleMetrics(int precision) { 227 if (precision < Scales.MIN_SCALE | precision > Scales.MAX_SCALE) { 228 throw new IllegalArgumentException( 229 "Precision must be in [" + Scales.MIN_SCALE + "," + Scales.MAX_SCALE + "] but was " + precision); 230 } 231 return Scales.getScaleMetrics(precision); 232 } 233 234 /** 235 * Java-7 port of {@code Double#isFinite(double)}. 236 * <p> 237 * Returns {@code true} if the argument is a finite floating-point value; returns {@code false} otherwise (for NaN 238 * and infinity arguments). 239 * 240 * @param d 241 * the {@code double} value to be tested 242 * @return {@code true} if the argument is a finite floating-point value, {@code false} otherwise. 243 */ 244 private static boolean isFinite(double d) { 245 return Math.abs(d) <= Double.MAX_VALUE; 246 } 247}