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}