From d9e9380a6d3692dc8246515d9e5c62e32c236da4 Mon Sep 17 00:00:00 2001 From: serso Date: Sun, 3 Apr 2016 00:46:58 +0200 Subject: [PATCH] True engineering number formatting --- jscl/src/main/java/jscl/JsclMathEngine.java | 193 +++++++++++------ jscl/src/main/java/midpcalc/Real.java | 2 +- .../test/java/jscl/JsclMathEngineTest.java | 204 +++++++++++++----- .../test/java/jscl/math/ExpressionTest.java | 14 +- 4 files changed, 299 insertions(+), 114 deletions(-) diff --git a/jscl/src/main/java/jscl/JsclMathEngine.java b/jscl/src/main/java/jscl/JsclMathEngine.java index 6aba3f7f..c9305d87 100644 --- a/jscl/src/main/java/jscl/JsclMathEngine.java +++ b/jscl/src/main/java/jscl/JsclMathEngine.java @@ -9,6 +9,7 @@ import jscl.math.operator.Percent; import jscl.math.operator.Rand; import jscl.math.operator.matrix.OperatorsRegistry; import jscl.text.ParseException; +import midpcalc.Real; import org.solovyev.common.math.MathRegistry; import org.solovyev.common.msg.MessageRegistry; import org.solovyev.common.msg.Messages; @@ -29,7 +30,22 @@ public class JsclMathEngine implements MathEngine { public static final int MAX_FRACTION_DIGITS = 20; @Nonnull private static JsclMathEngine instance = new JsclMathEngine(); - + @Nonnull + private final ConstantsRegistry constantsRegistry = new ConstantsRegistry(); + @Nonnull + private final ThreadLocal defaultNumbersFormat = new ThreadLocal() { + @Override + protected DecimalFormat initialValue() { + return new DecimalFormat(); + } + }; + @Nonnull + private final ThreadLocal smallNumbersFormat = new ThreadLocal() { + @Override + protected DecimalFormat initialValue() { + return new DecimalFormat("##0.#####E0"); + } + }; @Nonnull private DecimalFormatSymbols decimalGroupSymbols = new DecimalFormatSymbols(Locale.getDefault()); private boolean roundResult = false; @@ -41,8 +57,6 @@ public class JsclMathEngine implements MathEngine { @Nonnull private NumeralBase numeralBase = DEFAULT_NUMERAL_BASE; @Nonnull - private final ConstantsRegistry constantsRegistry = new ConstantsRegistry(); - @Nonnull private MessageRegistry messageRegistry = Messages.synchronizedMessageRegistry(new FixedCapacityListMessageRegistry(10)); { @@ -150,71 +164,128 @@ public class JsclMathEngine implements MathEngine { @Nonnull public String format(@Nonnull Double value, @Nonnull NumeralBase nb) throws NumeralBaseException { if (value.isInfinite()) { - // return predefined constant for infinity - if (value >= 0) { - return Constants.INF.getName(); - } else { - return Constants.INF.expressionValue().negate().toString(); - } + return formatInfinity(value); + } else if (value.isNaN()) { + // return "NaN" + return String.valueOf(value); + } else if (nb == NumeralBase.dec) { + return formatDec(value); } else { - if (value.isNaN()) { - // return "NaN" - return String.valueOf(value); - } else { - if (nb == NumeralBase.dec) { - // decimal numeral base => do specific formatting + return convert(value, nb); + } + } - // detect if current number is precisely equals to constant in constants' registry (NOTE: ONLY FOR SYSTEM CONSTANTS) - IConstant constant = findConstant(getConstantsRegistry().getSystemEntities(), value); + private String formatDec(@Nonnull Double value) { + if (value == 0d) { + return "0"; + } + // detect if current number is precisely equals to constant in constants' registry (NOTE: ONLY FOR SYSTEM CONSTANTS) + final IConstant constant = findConstant(value); + if (constant != null) { + return constant.getName(); + } - if (constant == null) { - final IConstant piInv = this.getConstantsRegistry().get(Constants.PI_INV.getName()); - if (piInv != null && value.equals(piInv.getDoubleValue())) { - constant = piInv; - } - } + if (scienceNotation) { + return formatDecEngineering(value); + } - if (constant == null) { - // prepare decimal format - final DecimalFormat df; + return formatDecDefault(value); + } - if (roundResult) { - value = new BigDecimal(value).setScale(precision, BigDecimal.ROUND_HALF_UP).doubleValue(); - } + @Nonnull + private String formatDecEngineering(@Nonnull Double value) { + final double absValue = Math.abs(value); + final boolean smallNumber = absValue < 1 && absValue >= 0.001; + final Real.NumberFormat nf = new Real.NumberFormat(); + nf.fse = smallNumber ? Real.NumberFormat.FSE_FIX : Real.NumberFormat.FSE_ENG; + if (useGroupingSeparator) { + nf.thousand = decimalGroupSymbols.getGroupingSeparator(); + } + if (roundResult) { + nf.precision = precision; + } + final Real real = new Real(Double.toString(value)); + return stripTrailingZeros(real.toString(nf)); + } - if (value != 0d && value != -0d) { - if (Math.abs(value) < Math.pow(10, -5) || scienceNotation) { - df = new DecimalFormat("##0.#####E0"); - } else { - df = new DecimalFormat(); - } - } else { - df = new DecimalFormat(); - } - - df.setDecimalFormatSymbols(decimalGroupSymbols); - df.setGroupingUsed(useGroupingSeparator); - df.setGroupingSize(nb.getGroupingSize()); - - if (!scienceNotation) { - // using default round logic => try roundResult variable - if (!roundResult) { - // set maximum fraction digits high enough to show all fraction digits in case of no rounding - df.setMaximumFractionDigits(MAX_FRACTION_DIGITS); - } else { - df.setMaximumFractionDigits(precision); - } - } - - return df.format(value); - - } else { - return constant.getName(); - } - } else { - return convert(value, nb); - } + @Nonnull + private String stripTrailingZeros(@Nonnull String s) { + final int dot = s.indexOf('.'); + if (dot < 0) { + // no dot - no trailing zeros + return s; + } + final int e = s.lastIndexOf('E'); + final int start; + String exponent = ""; + if (e > 0) { + exponent = s.substring(e); + if (exponent.length() == 2 && exponent.charAt(1) == '0') { + exponent = ""; } + start = e - 1; + } else { + start = s.length() - 1; + } + final int i = findLastNonZero(s, start); + return s.substring(0, i == dot ? i : i + 1) + exponent; + } + + private int findLastNonZero(String s, int start) { + int i = start; + for (; i >= 0; i--) { + if (s.charAt(i) != '0') { + break; + } + } + return i; + } + + @Nonnull + private String formatDecDefault(@Nonnull Double value) { + final BigDecimal bd; + if (roundResult) { + bd = BigDecimal.valueOf(value).setScale(precision, BigDecimal.ROUND_HALF_UP); + } else { + bd = BigDecimal.valueOf(value); + } + value = bd.doubleValue(); + if (value == 0) { + return "0"; + } + + final DecimalFormat df = Math.abs(value) < Math.pow(10, -5) ? smallNumbersFormat.get() : defaultNumbersFormat.get(); + df.setDecimalFormatSymbols(decimalGroupSymbols); + df.setGroupingUsed(useGroupingSeparator); + df.setGroupingSize(NumeralBase.dec.getGroupingSize()); + if (roundResult) { + df.setMaximumFractionDigits(precision); + } else { + // set maximum fraction digits high enough to show all fraction digits in case of no rounding + df.setMaximumFractionDigits(MAX_FRACTION_DIGITS); + } + return df.format(value); + } + + @Nullable + private IConstant findConstant(@Nonnull Double value) { + final IConstant constant = findConstant(constantsRegistry.getSystemEntities(), value); + if (constant != null) { + return constant; + } + final IConstant piInv = constantsRegistry.get(Constants.PI_INV.getName()); + if (piInv != null && value.equals(piInv.getDoubleValue())) { + return piInv; + } + return null; + } + + private String formatInfinity(@Nonnull Double value) { + // return predefined constant for infinity + if (value >= 0) { + return Constants.INF.getName(); + } else { + return Constants.INF.expressionValue().negate().toString(); } } diff --git a/jscl/src/main/java/midpcalc/Real.java b/jscl/src/main/java/midpcalc/Real.java index d8dfe4d2..d0736b2b 100755 --- a/jscl/src/main/java/midpcalc/Real.java +++ b/jscl/src/main/java/midpcalc/Real.java @@ -6599,7 +6599,7 @@ public final class Real { width -= prefix; ftoaExp.setLength(0); if (useExp) { - ftoaExp.append('e'); + ftoaExp.append('E'); ftoaExp.append(tmp4.exponent - pointPos); width -= ftoaExp.length(); } diff --git a/jscl/src/test/java/jscl/JsclMathEngineTest.java b/jscl/src/test/java/jscl/JsclMathEngineTest.java index c308c968..3423f4f9 100644 --- a/jscl/src/test/java/jscl/JsclMathEngineTest.java +++ b/jscl/src/test/java/jscl/JsclMathEngineTest.java @@ -1,8 +1,9 @@ package jscl; -import org.junit.Assert; import org.junit.Test; +import static org.junit.Assert.assertEquals; + /** * User: serso * Date: 12/15/11 @@ -15,70 +16,177 @@ public class JsclMathEngineTest { try { me.setUseGroupingSeparator(true); - Assert.assertEquals("1", me.format(1d, NumeralBase.bin)); - Assert.assertEquals("10", me.format(2d, NumeralBase.bin)); - Assert.assertEquals("11", me.format(3d, NumeralBase.bin)); - Assert.assertEquals("100", me.format(4d, NumeralBase.bin)); - Assert.assertEquals("101", me.format(5d, NumeralBase.bin)); - Assert.assertEquals("110", me.format(6d, NumeralBase.bin)); - Assert.assertEquals("111", me.format(7d, NumeralBase.bin)); - Assert.assertEquals("1000", me.format(8d, NumeralBase.bin)); - Assert.assertEquals("1001", me.format(9d, NumeralBase.bin)); - Assert.assertEquals("1 0001", me.format(17d, NumeralBase.bin)); - Assert.assertEquals("1 0100", me.format(20d, NumeralBase.bin)); - Assert.assertEquals("1 0100", me.format(20d, NumeralBase.bin)); - Assert.assertEquals("1 1111", me.format(31d, NumeralBase.bin)); + assertEquals("1", me.format(1d, NumeralBase.bin)); + assertEquals("10", me.format(2d, NumeralBase.bin)); + assertEquals("11", me.format(3d, NumeralBase.bin)); + assertEquals("100", me.format(4d, NumeralBase.bin)); + assertEquals("101", me.format(5d, NumeralBase.bin)); + assertEquals("110", me.format(6d, NumeralBase.bin)); + assertEquals("111", me.format(7d, NumeralBase.bin)); + assertEquals("1000", me.format(8d, NumeralBase.bin)); + assertEquals("1001", me.format(9d, NumeralBase.bin)); + assertEquals("1 0001", me.format(17d, NumeralBase.bin)); + assertEquals("1 0100", me.format(20d, NumeralBase.bin)); + assertEquals("1 0100", me.format(20d, NumeralBase.bin)); + assertEquals("1 1111", me.format(31d, NumeralBase.bin)); me.setRoundResult(true); me.setPrecision(10); - Assert.assertEquals("111 1111 0011 0110", me.format(32566d, NumeralBase.bin)); - Assert.assertEquals("100.0100 1100 11", me.format(4.3d, NumeralBase.bin)); - Assert.assertEquals("1 0001 0101 0011.0101 0101 10", me.format(4435.33423d, NumeralBase.bin)); - Assert.assertEquals("1100.0101 0101 01", me.format(12.3333d, NumeralBase.bin)); - Assert.assertEquals("1 0011 1101 1110 0100 0011 0101 0101.0001 1111 00", me.format(333333333.1212213321d, NumeralBase.bin)); + assertEquals("111 1111 0011 0110", me.format(32566d, NumeralBase.bin)); + assertEquals("100.0100 1100 11", me.format(4.3d, NumeralBase.bin)); + assertEquals("1 0001 0101 0011.0101 0101 10", me.format(4435.33423d, NumeralBase.bin)); + assertEquals("1100.0101 0101 01", me.format(12.3333d, NumeralBase.bin)); + assertEquals("1 0011 1101 1110 0100 0011 0101 0101.0001 1111 00", me.format(333333333.1212213321d, NumeralBase.bin)); - Assert.assertEquals("0.EE EE EE EE EE", me.format(14d / 15d, NumeralBase.hex)); - Assert.assertEquals("7F 36", me.format(32566d, NumeralBase.hex)); - Assert.assertEquals("24", me.format(36d, NumeralBase.hex)); - Assert.assertEquals("8", me.format(8d, NumeralBase.hex)); - Assert.assertEquals("1 3D", me.format(317d, NumeralBase.hex)); - Assert.assertEquals("13 DE 43 55.1F 08 5B EF 14", me.format(333333333.1212213321d, NumeralBase.hex)); - Assert.assertEquals("D 25 0F 77 0A.6F 73 18 FC 50", me.format(56456345354.43534534523459999d, NumeralBase.hex)); - Assert.assertEquals("3 E7.4C CC CC CC CC", me.format(999.3d, NumeralBase.hex)); + assertEquals("0.EE EE EE EE EE", me.format(14d / 15d, NumeralBase.hex)); + assertEquals("7F 36", me.format(32566d, NumeralBase.hex)); + assertEquals("24", me.format(36d, NumeralBase.hex)); + assertEquals("8", me.format(8d, NumeralBase.hex)); + assertEquals("1 3D", me.format(317d, NumeralBase.hex)); + assertEquals("13 DE 43 55.1F 08 5B EF 14", me.format(333333333.1212213321d, NumeralBase.hex)); + assertEquals("D 25 0F 77 0A.6F 73 18 FC 50", me.format(56456345354.43534534523459999d, NumeralBase.hex)); + assertEquals("3 E7.4C CC CC CC CC", me.format(999.3d, NumeralBase.hex)); me.setRoundResult(false); - Assert.assertEquals("0.00 00 00 00 00 00 00 00 00 6C", me.format(0.00000000000000000000009d, NumeralBase.hex)); - Assert.assertEquals("0.00 00 00 00 00 00 00 00 00 0A", me.format(0.000000000000000000000009d, NumeralBase.hex)); + assertEquals("0.00 00 00 00 00 00 00 00 00 6C", me.format(0.00000000000000000000009d, NumeralBase.hex)); + assertEquals("0.00 00 00 00 00 00 00 00 00 0A", me.format(0.000000000000000000000009d, NumeralBase.hex)); } finally { me.setUseGroupingSeparator(false); } - Assert.assertEquals("1", me.format(1d, NumeralBase.bin)); - Assert.assertEquals("10", me.format(2d, NumeralBase.bin)); - Assert.assertEquals("11", me.format(3d, NumeralBase.bin)); - Assert.assertEquals("100", me.format(4d, NumeralBase.bin)); - Assert.assertEquals("101", me.format(5d, NumeralBase.bin)); - Assert.assertEquals("110", me.format(6d, NumeralBase.bin)); - Assert.assertEquals("111", me.format(7d, NumeralBase.bin)); - Assert.assertEquals("1000", me.format(8d, NumeralBase.bin)); - Assert.assertEquals("1001", me.format(9d, NumeralBase.bin)); - Assert.assertEquals("10001", me.format(17d, NumeralBase.bin)); - Assert.assertEquals("10100", me.format(20d, NumeralBase.bin)); - Assert.assertEquals("10100", me.format(20d, NumeralBase.bin)); - Assert.assertEquals("11111", me.format(31d, NumeralBase.bin)); - Assert.assertEquals("111111100110110", me.format(32566d, NumeralBase.bin)); + assertEquals("1", me.format(1d, NumeralBase.bin)); + assertEquals("10", me.format(2d, NumeralBase.bin)); + assertEquals("11", me.format(3d, NumeralBase.bin)); + assertEquals("100", me.format(4d, NumeralBase.bin)); + assertEquals("101", me.format(5d, NumeralBase.bin)); + assertEquals("110", me.format(6d, NumeralBase.bin)); + assertEquals("111", me.format(7d, NumeralBase.bin)); + assertEquals("1000", me.format(8d, NumeralBase.bin)); + assertEquals("1001", me.format(9d, NumeralBase.bin)); + assertEquals("10001", me.format(17d, NumeralBase.bin)); + assertEquals("10100", me.format(20d, NumeralBase.bin)); + assertEquals("10100", me.format(20d, NumeralBase.bin)); + assertEquals("11111", me.format(31d, NumeralBase.bin)); + assertEquals("111111100110110", me.format(32566d, NumeralBase.bin)); - Assert.assertEquals("7F36", me.format(32566d, NumeralBase.hex)); - Assert.assertEquals("24", me.format(36d, NumeralBase.hex)); - Assert.assertEquals("8", me.format(8d, NumeralBase.hex)); - Assert.assertEquals("13D", me.format(317d, NumeralBase.hex)); + assertEquals("7F36", me.format(32566d, NumeralBase.hex)); + assertEquals("24", me.format(36d, NumeralBase.hex)); + assertEquals("8", me.format(8d, NumeralBase.hex)); + assertEquals("13D", me.format(317d, NumeralBase.hex)); } @Test public void testPiComputation() throws Exception { final JsclMathEngine me = JsclMathEngine.getInstance(); - Assert.assertEquals("-1+122.46467991473532E-18*i", me.evaluate("exp(√(-1)*Π)")); + assertEquals("-1+122.46467991473532E-18*i", me.evaluate("exp(√(-1)*Π)")); + } + + @Test + public void testEngineeringNotationWithRounding() throws Exception { + final JsclMathEngine me = JsclMathEngine.getInstance(); + me.setScienceNotation(true); + me.setRoundResult(true); + me.setPrecision(5); + + assertEquals("10E6", me.format(10000000d)); + assertEquals("99E6", me.format(99000000d)); + assertEquals("999E6", me.format(999000000d)); + assertEquals("999E6", me.format(999000001d)); + assertEquals("999.00001E6", me.format(999000011d)); + assertEquals("1E6", me.format(1000000d)); + assertEquals("111.11E3", me.format(111110d)); + assertEquals("111.1E3", me.format(111100d)); + assertEquals("111E3", me.format(111000d)); + assertEquals("110E3", me.format(110000d)); + assertEquals("100E3", me.format(100000d)); + assertEquals("10E3", me.format(10000d)); + assertEquals("1E3", me.format(1000d)); + assertEquals("100", me.format(100d)); + assertEquals("100.1", me.format(100.1d)); + assertEquals("100.12", me.format(100.12d)); + assertEquals("100.12345", me.format(100.123454d)); + assertEquals("100.12346", me.format(100.123455d)); + assertEquals("100.12346", me.format(100.123456d)); + assertEquals("1", me.format(1d)); + assertEquals("-42", me.format(-42d)); + assertEquals("-999", me.format(-999d)); + assertEquals("-999.99", me.format(-999.99d)); + assertEquals("-0.1", me.format(-0.1d)); + assertEquals("-0.12", me.format(-0.12d)); + assertEquals("-0.123", me.format(-0.123d)); + assertEquals("-0.1234", me.format(-0.1234d)); + assertEquals("0.1", me.format(0.1)); + assertEquals("0.01", me.format(0.01)); + assertEquals("0.001", me.format(0.001)); + assertEquals("0.001", me.format(0.00100000001)); + assertEquals("0.0011", me.format(0.0011)); + assertEquals("999.999E-6", me.format(0.000999999)); + assertEquals("100E-6", me.format(0.0001)); + assertEquals("1E-6", me.format(0.000001)); + assertEquals("10E-9", me.format(0.00000001)); + + assertEquals("-100.001E3", me.format(-100001d)); + assertEquals("100.001E3", me.format(100001d)); + assertEquals("111.111E3", me.format(111111d)); + assertEquals("111.11123E3", me.format(111111.234567d)); + assertEquals("111.11123E3", me.format(111111.23456d)); + assertEquals("111.11123E3", me.format(111111.2345d)); + assertEquals("111.11123E3", me.format(111111.2345d)); + assertEquals("111.11123E3", me.format(111111.234d)); + assertEquals("111.11123E3", me.format(111111.23d)); + assertEquals("111.1112E3", me.format(111111.2d)); + } + + @Test + public void testEngineeringNotationWithoutRounding() throws Exception { + final JsclMathEngine me = JsclMathEngine.getInstance(); + me.setScienceNotation(true); + me.setRoundResult(false); + + assertEquals("10E6", me.format(10000000d)); + assertEquals("99E6", me.format(99000000d)); + assertEquals("999E6", me.format(999000000d)); + assertEquals("999.000001E6", me.format(999000001d)); + assertEquals("999.000011E6", me.format(999000011d)); + assertEquals("1E6", me.format(1000000d)); + assertEquals("111.11E3", me.format(111110d)); + assertEquals("111.1E3", me.format(111100d)); + assertEquals("111E3", me.format(111000d)); + assertEquals("110E3", me.format(110000d)); + assertEquals("100E3", me.format(100000d)); + assertEquals("10E3", me.format(10000d)); + assertEquals("1E3", me.format(1000d)); + assertEquals("100", me.format(100d)); + assertEquals("100.1", me.format(100.1d)); + assertEquals("100.12", me.format(100.12d)); + assertEquals("100.123454", me.format(100.123454d)); + assertEquals("100.123455", me.format(100.123455d)); + assertEquals("100.123456", me.format(100.123456d)); + assertEquals("1", me.format(1d)); + assertEquals("-42", me.format(-42d)); + assertEquals("-999", me.format(-999d)); + assertEquals("-999.99", me.format(-999.99d)); + assertEquals("-0.1", me.format(-0.1d)); + assertEquals("-0.12", me.format(-0.12d)); + assertEquals("-0.123", me.format(-0.123d)); + assertEquals("-0.1234", me.format(-0.1234d)); + assertEquals("0.1", me.format(0.1)); + assertEquals("0.01", me.format(0.01)); + assertEquals("0.001", me.format(0.001)); + assertEquals("0.0011", me.format(0.0011)); + assertEquals("999.999E-6", me.format(0.000999999)); + assertEquals("100E-6", me.format(0.0001)); + + assertEquals("100.001E3", me.format(100001d)); + assertEquals("111.111E3", me.format(111111d)); + assertEquals("111.111234567E3", me.format(111111.234567d)); + assertEquals("111.11123456E3", me.format(111111.23456d)); + assertEquals("111.1112345E3", me.format(111111.2345d)); + assertEquals("111.1112345E3", me.format(111111.2345d)); + assertEquals("111.111234E3", me.format(111111.234d)); + assertEquals("111.11123E3", me.format(111111.23d)); + assertEquals("111.1112E3", me.format(111111.2d)); } } diff --git a/jscl/src/test/java/jscl/math/ExpressionTest.java b/jscl/src/test/java/jscl/math/ExpressionTest.java index d2c84412..d1d13116 100644 --- a/jscl/src/test/java/jscl/math/ExpressionTest.java +++ b/jscl/src/test/java/jscl/math/ExpressionTest.java @@ -798,13 +798,19 @@ public class ExpressionTest { me.setScienceNotation(true); Assert.assertEquals("0", Expression.valueOf("0.0").simplify().toString()); - Assert.assertEquals("1E0", Expression.valueOf("1.0").simplify().toString()); - Assert.assertEquals("100E0", Expression.valueOf("100.0").simplify().toString()); + Assert.assertEquals("1", Expression.valueOf("1.0").simplify().toString()); + Assert.assertEquals("100", Expression.valueOf("100.0").simplify().toString()); + me.setScienceNotation(false); me.setRoundResult(true); me.setPrecision(5); Assert.assertEquals("0", Expression.valueOf("1222/(10^9)").numeric().toString()); + me.setScienceNotation(true); + me.setRoundResult(true); + me.setPrecision(5); + Assert.assertEquals("1.222E-6", Expression.valueOf("1222/(10^9)").numeric().toString()); + me.setRoundResult(true); me.setPrecision(10); Assert.assertEquals("1.222E-6", Expression.valueOf("1222/(10^9)").numeric().toString()); @@ -816,11 +822,11 @@ public class ExpressionTest { Assert.assertEquals("0.3333333333333333", Expression.valueOf("1/3").numeric().toString()); me.setScienceNotation(true); - Assert.assertEquals("333.33333E-3", Expression.valueOf("1/3").numeric().toString()); + Assert.assertEquals("0.3333333333333333", Expression.valueOf("1/3").numeric().toString()); me.setRoundResult(true); me.setPrecision(10); - Assert.assertEquals("333.33333E-3", Expression.valueOf("1/3").numeric().toString()); + Assert.assertEquals("0.3333333333", Expression.valueOf("1/3").numeric().toString()); me.setScienceNotation(false); me.setRoundResult(true);