/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.f1x.util.format; import java.math.BigDecimal; /** * This class formats floating point number to textual representation with given precision. * Not thread safe. * For large numbers this method falls back to BigDecimal.toPlainString(). * Note: formatter always uses '.' dot as decimal separator (regardless of current locale). * */ public final class DoubleFormatter { public static final int MAX_WIDTH = 21; public static final int MAX_PRECISION = 15; private static final long MAX = Long.MAX_VALUE / 10; private final byte [] buffer = new byte [MAX_WIDTH]; private final int maxLength; private final int precision; private final long factor; public DoubleFormatter (int precision) { this (precision, MAX_WIDTH); } /** * @param precision maximum number of digits after decimal point (e.g. 3). Truncated part will be rounded. * @param maxLength maximum length a whole string should take (e.g. 16). */ public DoubleFormatter (int precision, int maxLength) { if (precision < 0 || precision > MAX_PRECISION) throw new IllegalArgumentException("Precision"); if (maxLength < 0 || maxLength > MAX_WIDTH) throw new IllegalArgumentException("Length"); this.precision = precision; this.maxLength = maxLength; this.factor = Math.round(Math.pow(10, precision+1)); } /** Formats given number into output byte buffer */ public int format (double number, byte [] output, int offset) { return format(number, precision, true, maxLength, factor, output, offset); } /** Formats given number into output byte buffer */ public int format (double number, int precision, byte [] output, int offset) { return format(number, precision, MAX_WIDTH, output, offset); } /** * @param precision maximum number of digits after decimal point (e.g. 3). Truncated part will be rounded. * @param maxLength maximum length a whole string should take (e.g. 16). */ public int format (double number, int precision, int maxLength, byte [] output, int offset) { return format(number, precision, true, maxLength, output, offset); } /** * @param precision maximum number of digits after decimal point (e.g. 3). Truncated part will be rounded. * @param roundUp defines rounding mode (RoundingMode.HALF_UP or RoundingMode.HALF_DOWN) * @param maxLength maximum length a whole string should take (e.g. 16). */ public int format (double number, int precision, boolean roundUp, int maxLength, byte [] output, int offset) { long factor = 10; for (int i = 0; i < precision; i++) factor*= 10; return format(number, precision, roundUp, maxLength, factor, output, offset); } /** * @param precision maximum number of digits after decimal point (e.g. 3). Truncated part will be rounded. * @param roundUp defines rounding mode (HALF_UP or HALF_DOWN) * @param maxLength maximum length a whole string should take (e.g. 16). */ private int format (double number, int precision, boolean roundUp, int maxLength, long factor10, byte [] output, int offset) { if (precision < 0 || precision > MAX_PRECISION) throw new IllegalArgumentException("Precision"); if (maxLength < 0 || maxLength > MAX_WIDTH) throw new IllegalArgumentException("Length"); if (Double.isNaN(number)) // Infinity will be checked a bit later throw new IllegalArgumentException("NaN"); boolean sign = false; double factoredNumber = number; if (number < 0) { sign = true; factoredNumber = Math.abs(number); } factoredNumber = factoredNumber*factor10; if (Double.isInfinite(factoredNumber) || factoredNumber > MAX) return formatLargeNumber (number, output, offset); //long numberAsDecimal = Math.round(factoredNumber); // this call costs 8% of total time we spend in this method on avg. long numberAsDecimal; { //factoredNumber = factoredNumber * 10; numberAsDecimal = (long) factoredNumber; double smallestDigit = factoredNumber % 10; numberAsDecimal = numberAsDecimal / 10; if (roundUp) { if (smallestDigit >= 5) numberAsDecimal++; // round up; } else { if (smallestDigit > 5) numberAsDecimal++; // round up; } } if (numberAsDecimal == 0) return formatZero(output, offset); int i = MAX_WIDTH; int fractional_part = precision; boolean needLeadingZero = (fractional_part > 0); while (numberAsDecimal > 0) { int digit = (int) (numberAsDecimal % 10); if (digit != 0 || i != MAX_WIDTH || fractional_part == 0) // skip trailing zeros buffer [--i] = IntFormatter.Digits[digit]; numberAsDecimal = numberAsDecimal / 10; if (fractional_part > 0) { fractional_part --; if (fractional_part == 0) { if (i != MAX_WIDTH) buffer [--i] = '.'; needLeadingZero = (numberAsDecimal == 0); } } } if (needLeadingZero) { if (fractional_part > 0) { // if number was less than zero while (fractional_part-- > 0) buffer [--i] = '0'; buffer [--i] = '.'; } buffer [--i] = '0'; } if (sign) buffer [--i] = '-'; int len = Math.min(MAX_WIDTH - i, maxLength); System.arraycopy(buffer, i, output, offset, len); return offset + len; } private static int formatZero(byte[] output, int offset) { output[offset++] = '0'; return offset; } // Super Rarely used to represent prices private static int formatLargeNumber(double number, byte[] output, int offset) { final String text = new BigDecimal(number).toPlainString(); final int cnt = text.length(); for(int i=0; i < cnt; i++) output[offset++] = (byte) text.charAt(i); return offset; } }