001/**
002 * The MIT License (MIT)
003 *
004 * Copyright (c) 2015-2016 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 java.math.RoundingMode;
027import java.util.Objects;
028
029import org.decimal4j.api.DecimalArithmetic;
030import org.decimal4j.scale.ScaleMetrics;
031import org.decimal4j.scale.Scales;
032
033/**
034 * Utility class to round double values to an arbitrary decimal precision between 0 and 18. The rounding is efficient
035 * and garbage free.
036 */
037public final class DoubleRounder {
038
039        private final ScaleMetrics scaleMetrics;
040        private final double ulp;
041
042        /**
043         * Creates a rounder for the given decimal precision.
044         * 
045         * @param precision
046         *            the decimal rounding precision, must be in {@code [0,18]}
047         * @throws IllegalArgumentException
048         *             if precision is negative or larger than 18
049         */
050        public DoubleRounder(int precision) {
051                this(toScaleMetrics(precision));
052        }
053
054        /**
055         * Creates a rounder with the given scale metrics defining the decimal precision.
056         * 
057         * @param scaleMetrics
058         *            the scale metrics determining the rounding precision
059         * @throws NullPointerException
060         *             if scale metrics is null
061         */
062        public DoubleRounder(ScaleMetrics scaleMetrics) {
063                this.scaleMetrics = Objects.requireNonNull(scaleMetrics, "scaleMetrics cannot be null");
064                this.ulp = scaleMetrics.getRoundingHalfEvenArithmetic().toDouble(1);
065        }
066
067        /**
068         * Returns the precision of this rounder, a value between zero and 18.
069         * 
070         * @return this rounder's decimal precision
071         */
072        public int getPrecision() {
073                return scaleMetrics.getScale();
074        }
075
076        /**
077         * Rounds the given double value to the decimal precision of this rounder using {@link RoundingMode#HALF_UP HALF_UP}
078         * rounding.
079         * 
080         * @param value
081         *            the value to round
082         * @return the rounded value
083         * @see #getPrecision()
084         */
085        public double round(double value) {
086                return round(value, scaleMetrics.getDefaultArithmetic(), scaleMetrics.getRoundingHalfEvenArithmetic(), ulp);
087        }
088
089        /**
090         * Rounds the given double value to the decimal precision of this rounder using the specified rounding mode.
091         * 
092         * @param value
093         *            the value to round
094         * @param roundingMode
095         *            the rounding mode indicating how the least significant returned decimal digit of the result is to be
096         *            calculated
097         * @return the rounded value
098         * @see #getPrecision()
099         */
100        public double round(double value, RoundingMode roundingMode) {
101                return round(value, roundingMode, scaleMetrics.getRoundingHalfEvenArithmetic(), ulp);
102        }
103
104        /**
105         * Returns a hash code for this <tt>DoubleRounder</tt> instance.
106         * 
107         * @return a hash code value for this object.
108         */
109        @Override
110        public int hashCode() {
111                return scaleMetrics.hashCode();
112        }
113
114        /**
115         * Returns true if {@code obj} is a <tt>DoubleRounder</tt> with the same precision as {@code this} rounder instance.
116         * 
117         * @param obj
118         *            the reference object with which to compare
119         * @return true for a double rounder with the same precision as this instance
120         */
121        @Override
122        public boolean equals(Object obj) {
123                if (obj == this)
124                        return true;
125                if (obj == null)
126                        return false;
127                if (obj instanceof DoubleRounder) {
128                        return scaleMetrics.equals(((DoubleRounder) obj).scaleMetrics);
129                }
130                return false;
131        }
132
133        /**
134         * Returns a string consisting of the simple class name and the precision.
135         * 
136         * @return a string like "DoubleRounder[precision=7]"
137         */
138        @Override
139        public String toString() {
140                return "DoubleRounder[precision=" + getPrecision() + "]";
141        }
142
143        /**
144         * Rounds the given double value to the specified decimal {@code precision} using {@link RoundingMode#HALF_UP
145         * HALF_UP} rounding.
146         * 
147         * @param value
148         *            the value to round
149         * @param precision
150         *            the decimal precision to round to (aka decimal places)
151         * @return the rounded value
152         */
153        public static final double round(double value, int precision) {
154                final ScaleMetrics sm = toScaleMetrics(precision);
155                final DecimalArithmetic halfEvenArith = sm.getRoundingHalfEvenArithmetic();
156                return round(value, sm.getDefaultArithmetic(), halfEvenArith, halfEvenArith.toDouble(1));
157        }
158
159        /**
160         * Rounds the given double value to the specified decimal {@code precision} using the specified rounding mode.
161         * 
162         * @param value
163         *            the value to round
164         * @param precision
165         *            the decimal precision to round to (aka decimal places)
166         * @param roundingMode
167         *            the rounding mode indicating how the least significant returned decimal digit of the result is to be
168         *            calculated
169         * @return the rounded value
170         */
171        public static final double round(double value, int precision, RoundingMode roundingMode) {
172                final ScaleMetrics sm = toScaleMetrics(precision);
173                final DecimalArithmetic halfEvenArith = sm.getRoundingHalfEvenArithmetic();
174                return round(value, roundingMode, halfEvenArith, halfEvenArith.toDouble(1));
175        }
176
177        private static final double round(double value, RoundingMode roundingMode, DecimalArithmetic halfEvenArith, double ulp) {
178                if (roundingMode == RoundingMode.UNNECESSARY) {
179                        return checkRoundingUnnecessary(value, halfEvenArith, ulp);
180                }
181                return round(value, halfEvenArith.deriveArithmetic(roundingMode), halfEvenArith, ulp);
182        }
183
184        private static final double round(double value, DecimalArithmetic roundingArith, DecimalArithmetic halfEvenArith, double ulp) {
185                if (!isFinite(value) || 2 * ulp <= Math.ulp(value)) {
186                        return value;
187                }
188                final long uDecimal = roundingArith.fromDouble(value);
189                return halfEvenArith.toDouble(uDecimal);
190        }
191
192        private static final double checkRoundingUnnecessary(double value, DecimalArithmetic halfEvenArith, double ulp) {
193                if (isFinite(value) && 2 * ulp > Math.ulp(value)) {
194                        final long uDecimal = halfEvenArith.fromDouble(value);
195                        if (halfEvenArith.toDouble(uDecimal) != value) {
196                                throw new ArithmeticException(
197                                                "Rounding necessary for precision " + halfEvenArith.getScale() + ": " + value);
198                        }
199                }
200                return value;
201        }
202
203        private static final ScaleMetrics toScaleMetrics(int precision) {
204                if (precision < Scales.MIN_SCALE | precision > Scales.MAX_SCALE) {
205                        throw new IllegalArgumentException(
206                                        "Precision must be in [" + Scales.MIN_SCALE + "," + Scales.MAX_SCALE + "] but was " + precision);
207                }
208                return Scales.getScaleMetrics(precision);
209        }
210
211        /**
212         * Java-7 port of {@code Double#isFinite(double)}.
213         * <p>
214         * Returns {@code true} if the argument is a finite floating-point value; returns {@code false} otherwise (for NaN
215         * and infinity arguments).
216         *
217         * @param d
218         *            the {@code double} value to be tested
219         * @return {@code true} if the argument is a finite floating-point value, {@code false} otherwise.
220         */
221        private static boolean isFinite(double d) {
222                return Math.abs(d) <= Double.MAX_VALUE;
223        }
224}