/*
* 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 org.junit.BeforeClass;
import org.junit.Test;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class Test_DoubleFormatter {
private static final double EPSILON = 0.0000000000000000000001;
private static final int MAX_WIDTH = 512;
private static final DecimalFormat [] DECIMAL_FORMATTERS = new DecimalFormat[DoubleFormatter.MAX_PRECISION+1];
private static final DoubleFormatter [] DOUBLE_FORMATTERS = new DoubleFormatter[DoubleFormatter.MAX_PRECISION+1];
private final byte [] buffer = new byte [MAX_WIDTH];
@BeforeClass
public static void initFormattersCache() {
String standardFormatPattern = "###.";
for (int i = 0; i < DoubleFormatter.MAX_PRECISION+1; i++) {
DECIMAL_FORMATTERS[i] = new DecimalFormat(standardFormatPattern);
DOUBLE_FORMATTERS[i] = new DoubleFormatter(i);
standardFormatPattern += '#';
}
}
@Test
public void testAroundBoundariesOfLong() {
assertFormat(0.99e19, 1, "9900000000000000000");
assertFormat(0.99e-19, 1, "0");
}
@Test
public void testSimple() {
assertFormat(1.234, 3, "1.234");
assertFormat(0.123, 3, "0.123");
assertFormat(12.345, 0, "12");
assertFormat(12.345, 1, "12.3");
assertFormat(12.345, 2, "12.35"); // rounding up
assertFormat(12.344, 2, "12.34"); // rounding down
assertFormat(12.345, 3, "12.345");
assertFormat(12.345, 4, "12.345");
assertFormat(12.345, 5, "12.345");
}
@Test
public void testNegative() {
assertFormat(-1.23, 3, "-1.23");
}
@Test
public void powerOfTen() {
assertFormat(0.0001, 3, "0");
assertFormat(0.001, 3, "0.001");
assertFormat(0.01, 3, "0.01");
assertFormat(0.1, 3, "0.1");
assertFormat(1, 3, "1");
assertFormat(10, 3, "10");
assertFormat(100, 3, "100");
assertFormat(1000, 3, "1000");
assertFormat(10000, 3, "10000");
assertFormat(100000, 3, "100000");
}
@Test
public void powerOfTenNegative() {
assertFormat(-0.0001, 3, "0");
//assertFormat(-0.0001, "-0", 3);
assertFormat(-0.001, 3, "-0.001");
assertFormat(-0.01, 3, "-0.01");
assertFormat(-0.1, 3, "-0.1");
assertFormat(-1, 3, "-1");
assertFormat(-10, 3, "-10");
assertFormat(-100, 3, "-100");
assertFormat(-1000, 3, "-1000");
assertFormat(-10000, 3, "-10000");
assertFormat(-100000, 3, "-100000");
}
@Test
public void testWholeNumbers() {
assertFormat(1, 3, "1");
assertFormat(123, 3, "123");
assertFormat(1, 0, "1");
assertFormat(123, 0, "123");
}
@Test
public void testPrecision0() {
assertFormat(12345.6, 0, "12346");
assertFormat(1234.56, 0, "1235");
assertFormat(123.456, 0, "123");
assertFormat(12.3456, 0, "12");
assertFormat(1.23456, 0, "1");
assertFormat(0.123456, 0, "0");
assertFormat(0.0123456, 0, "0");
assertFormat(0.0012345, 0, "0");
assertFormat(0.0001234, 0, "0");
assertFormat(0.0000123, 0, "0");
assertFormat(0.0000012, 0, "0");
assertFormat(0.0000001, 0, "0");
}
@Test
public void testPrecision1() {
assertFormat(123456., 1, "123456");
assertFormat(12345.6, 1, "12345.6");
assertFormat(1234.56, 1, "1234.6");
assertFormat(123.456, 1, "123.5");
assertFormat(12.3456, 1, "12.3");
assertFormat(1.23456, 1, "1.2");
assertFormat(0.123456, 1, "0.1");
assertFormat(0.0123456, 1, "0");
assertFormat(0.0012345, 1, "0");
assertFormat(0.0001234, 1, "0");
assertFormat(0.0000123, 1, "0");
assertFormat(0.0000012, 1, "0");
assertFormat(0.0000001, 1, "0");
}
@Test
public void testPrecision2() {
assertFormat(123456., 2, "123456");
assertFormat(12345.6, 2, "12345.6");
assertFormat(1234.56, 2, "1234.56");
assertFormat(123.456, 2, "123.46");
assertFormat(12.3456, 2, "12.35");
assertFormat(1.23456, 2, "1.23");
assertFormat(0.123456, 2, "0.12");
assertFormat(0.0123456, 2, "0.01");
assertFormat(0.0012345, 2, "0");
assertFormat(0.0001234, 2, "0");
assertFormat(0.0000123, 2, "0");
assertFormat(0.0000012, 2, "0");
assertFormat(0.0000001, 2, "0");
}
@Test
public void testPrecision3() {
assertFormat(123456., 3, "123456");
assertFormat(12345.6, 3, "12345.6");
assertFormat(1234.56, 3, "1234.56");
assertFormat(123.456, 3, "123.456");
assertFormat(12.3456, 3, "12.346");
assertFormat(1.23456, 3, "1.235");
assertFormat(0.123456, 3, "0.123");
assertFormat(0.0123456, 3, "0.012");
assertFormat(0.0012345, 3, "0.001");
assertFormat(0.0001234, 3, "0");
assertFormat(0.0000123, 3, "0");
assertFormat(0.0000012, 3, "0");
assertFormat(0.0000001, 3, "0");
}
@Test
public void testZeros() {
assertFormat(0, 3, "0");
assertFormat(-2.0 / Float.POSITIVE_INFINITY, 3, "0");
assertFormat(-0., 3, "0");
}
@Test
public void testPeriodic() {
assertFormat(1.0 / 3.0, 9, "0.333333333");
}
@Test
public void testDoubleMaxValue() {
assertFormat(Double.MAX_VALUE, DoubleFormatter.MAX_PRECISION, "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368");
}
@Test
public void testLongMaxValue() {
assertFormat(Long.MAX_VALUE, DoubleFormatter.MAX_PRECISION, "9223372036854775808");
}
@Test
public void testDoubleMinValue() {
assertFormat(Double.MIN_VALUE, DoubleFormatter.MAX_PRECISION, "0");
}
@Test
public void testLongMinValue() {
assertFormat(Long.MIN_VALUE, DoubleFormatter.MAX_PRECISION, "-9223372036854775808");
}
@Test
public void testNaN() {
try {
new DoubleFormatter(3).format(Double.NaN, buffer, 0);
fail("Failed to detect NaN");
} catch (IllegalArgumentException expected) {
// ignore
}
}
@Test
public void testInfinity() {
try {
new DoubleFormatter(3).format(Double.POSITIVE_INFINITY, buffer, 0);
fail("Failed to detect INFINITY");
} catch (IllegalArgumentException expected) {
// ignore
}
try {
new DoubleFormatter(3).format(Double.NEGATIVE_INFINITY, buffer, 0);
fail("Failed to detect INFINITY");
} catch (IllegalArgumentException expected) {
// ignore
}
}
@Test
public void testEnum() {
double number = 0.01 * Long.MIN_VALUE;
while (number < -0.01) {
assertFormat(number, 2);
number = number / 10;
}
number = 0.01 * Long.MAX_VALUE;
while (number > 0.01) {
assertFormat(number, 2);
number = number / 10;
}
}
@Test
public void testRandom() {
Random rnd = new Random(13132);
for (int i = 0; i < 100000; i++) {
double number = rnd.nextDouble() * Math.pow(10, rnd.nextInt(8));
assertFormat(number, 5);
}
}
@Test
public void testBug() {
assertFormat(-92233720368547760.0, 2, "-92233720368547760");
}
@Test
public void testBug1() {
assertFormat(49405.994999999995, 2, "49405.99");
}
@Test
public void testBug2() {
assertFormat(1.123450000000001, 5, "1.12345");
assertFormat(1.123450000000005, 5, "1.12345");
assertFormat(1.1234599999999, 5, "1.12346");
assertFormat(1.123455, 5, true, "1.12346");
assertFormat(1.123455, 5, false, "1.12345");
assertFormat(120.37500001, 5, true, "120.375");
assertFormat(120.37500001, 5, false, "120.375");
assertFormat(120.3751, 9, "120.3751");
assertFormat(120.37501, 9, "120.37501");
assertFormat(120.375001, 9, "120.375001");
assertFormat(120.3750001, 9, "120.3750001");
assertFormat(120.37500001, 9, "120.37500001");
assertFormat(120.375000001, 9, "120.375000001");
assertFormat(120.3750000001, 9, "120.375");
}
@Test
public void testBigDecimal () {
assertFormat(111111114443343.5625, 3, "111111114443343.5625"); // for large numbers we use BigDecimal's format that doesn't have precision
}
@Test
public void test3() {
double ask = 123.00006;
double bid = 123.00005;
double quote1 = bid + (ask-bid)/3;
double quote2 = ask + (ask-bid)/3;
// 1/10 of PIP precision
assertFormat(quote1, 5, "123.00005");
assertFormat(quote2, 5, "123.00006");
// default precision
assertFormat(quote1, 9, "123.000053333");
assertFormat(quote2, 9, "123.000063333");
// after rounding
assertFormat(roundTickUp(quote1, 5), 9, "123.00006");
assertFormat(roundTickUp(quote2, 5), 9, "123.00007");
assertFormat(roundTickDown(quote1, 5), 9, "123.00005");
assertFormat(roundTickDown(quote2, 5), 9, "123.00006");
}
@Test
public void test4() {
double value = 1.0 / 3;
assertFormat(value, 9, "0.333333333");
assertFormat(value*3, 9, "1");
assertFormat(1.0/7, 9, "0.142857143");
assertFormat(1.0 - 1.0/9.0, 9, "0.888888889");
}
@Test
public void test09999999() {
double d0999999 = 0;
double delta = 0.9;
// get 0.999(9) = 0.9 + 0.09 + 0.009 + 0.0009 + ...
while (true) {
d0999999 += delta;
delta /= 10;
if (d0999999 + delta == d0999999 + delta + delta/10)
break;
}
assertFormat(d0999999, 9, true, "1");
assertFormat(d0999999, 9, false, "1");
}
@Test
public void testRoundUpAndDown() {
assertFormat(1.123450000000001, 5, true, "1.12345");
assertFormat(1.123450000000001, 5, false, "1.12345");
assertFormat(1.123459999999999, 5, true, "1.12346");
assertFormat(1.123459999999999, 5, false, "1.12346");
assertFormat(1.123455, 5, false, "1.12345");
assertFormat(1.123455, 5, true, "1.12346");
assertFormat(1.1234559, 5, true, "1.12346");
assertFormat(1.1234559, 5, false, "1.12346");
assertFormat(1.59, 0, true, "2");
assertFormat(1.59, 0, false, "2");
}
private static double roundTickUp(double value, int precision) {
double tickSize = 1;
while (precision-- > 0)
tickSize /= 10;
double accuracy = tickSize * 0.01;
return Math.ceil((value - accuracy) / tickSize) * tickSize;
}
private static double roundTickDown(double value, int precision) {
double tickSize = 1;
while (precision-- > 0)
tickSize /= 10;
double accuracy = tickSize * 0.01;
return Math.floor((value + accuracy) / tickSize) * tickSize;
}
@Test
public void oldStyleTest() {
DoubleFormatter df = new DoubleFormatter(9);
int len = df.format(1.2345, 9, buffer, 0);
assertEquals("1.2345", new String (buffer, 0, len));
int len2 = df.format(1.2345, buffer, 0);
assertEquals("1.2345", new String (buffer, 0, len2));
}
//TODO: Runs tpo slow (re-test periodically)
//@Test
public static void enumerateAll() {
double number = -10000, step = 0.000003;
while (number < 100000) {
assertFormat(number, 2);
number += step;
}
}
//TODO: Runs tpo slow (re-test periodically)
//@Test // Very slow (days)
public void decimalRange() {
long value = 0;
while (value < 10_000_000_000L) {
assertFormat( ((double)value) / 1_000_000_000, 9);
value ++;
}
}
////////// Tools ///////////////////////////////////////////////////////////////////////////////////////////////////
//
// private void verifyFormat (double value, int precision, String expectedFormat) {
// verifyFormat (value, precision, true, expectedFormat);
// }
//
// private void verifyFormat (double value, int precision, boolean roundUp, String expectedFormat) {
// DoubleFormatter df = DOUBLE_FORMATTERS[precision];
// DecimalFormat standardFormat = DECIMAL_FORMATTERS[precision];
// standardFormat.setRoundingMode(RoundingMode.HALF_UP);
// System.out.println (value);
// assertEquals("Java Format", expectedFormat, standardFormat.format(value));
// assertFormat(df, value, standardFormat);
// }
//
// private static void assertFormat(double number, int precision, String expected) {
// DoubleFormatter df = DOUBLE_FORMATTERS[precision];
// byte [] buffer= new byte[MAX_WIDTH];
// int length = df.format(number, buffer, 0);
// String actual = new String(buffer, 0, length);
// if (!expected.equals(actual)) {
// assertEquals(number, Double.valueOf(actual), EPSILON);
// }
// }
//
private static void assertFormat(double number, int precision) {
assertFormat(number, precision, true);
}
private static void assertFormat(double number, int precision, boolean roundUp) {
DecimalFormat standardFormat = DECIMAL_FORMATTERS[precision];
standardFormat.setRoundingMode(roundUp ? RoundingMode.HALF_UP : RoundingMode.HALF_DOWN);
String expected = standardFormat.format(number);
if (expected.equals("-0"))
expected = "0";
String actual = format(number, precision, roundUp);
if ( ! expected.equals(actual)) {
// Slow method:
String expectedBigDecimal = new BigDecimal(number).setScale(precision, (roundUp ? RoundingMode.HALF_UP : RoundingMode.HALF_DOWN)).toString();
if ( ! expectedBigDecimal.equals(actual)) {
System.err.printf("WARN: %f formatted as %s instead of %s with precision %d and rounding " + (roundUp ? "up" : "down") + '\n', number, actual, expected, precision);
if (Math.abs(number - Double.valueOf(actual)) > EPSILON)
fail("Format mismatch for number " + number + ":\nexpected:" + expected + "\n actual:" + actual);
}
}
}
private static void assertFormat(double number, int precision, String expected) {
assertFormat(number, precision, true, expected);
}
private static void assertFormat(double number, int precision, boolean roundUp, String expected) {
String actual = format(number, precision, roundUp);
if ( ! expected.equals(actual)) {
fail("Format mismatch for number " + number + ":\nexpected:" + expected + "\n actual:" + actual);
}
}
private static String format(double number, int precision, boolean roundUp) {
String actual;
try {
DoubleFormatter df = DOUBLE_FORMATTERS[precision];
byte [] buffer= new byte[MAX_WIDTH];
int length = df.format(number, precision, roundUp, DoubleFormatter.MAX_WIDTH, buffer, 0);
actual = new String (buffer, 0, length);
//System.out.println (number + "=>" + actual);
} catch (Exception e) {
throw new RuntimeException("Error formatting " + number + ": " + e.getMessage(), e);
}
return actual;
}
public static void main(String[] args) {
enumerateAll();
}
}