diff --git a/jscl/src/main/java/jscl/JsclMathEngine.java b/jscl/src/main/java/jscl/JsclMathEngine.java index dbca872d..f58333f0 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.NumberFormatter; import org.solovyev.common.math.MathRegistry; import org.solovyev.common.msg.MessageRegistry; @@ -19,6 +20,8 @@ import javax.annotation.Nullable; import java.math.BigDecimal; import java.util.List; +import static midpcalc.Real.NumberFormat.*; + public class JsclMathEngine implements MathEngine { public static final AngleUnit DEFAULT_ANGLE_UNITS = AngleUnit.deg; @@ -38,7 +41,7 @@ public class JsclMathEngine implements MathEngine { }; private char groupingSeparator = GROUPING_SEPARATOR_DEFAULT; private boolean roundResult = false; - private boolean scienceNotation = false; + private int numberFormat = FSE_NONE; private int precision = 5; private boolean useGroupingSeparator = false; @Nonnull @@ -166,11 +169,17 @@ public class JsclMathEngine implements MathEngine { } final NumberFormatter nf = numberFormatter.get(); nf.setGroupingSeparator(useGroupingSeparator ? groupingSeparator : NumberFormatter.NO_GROUPING); - nf.setPrecision(roundResult ? precision : NumberFormatter.DEFAULT_PRECISION); - if (scienceNotation) { - nf.useEngineeringFormat(NumberFormatter.DEFAULT_MAGNITUDE); - } else { - nf.useSimpleFormat(); + nf.setPrecision(roundResult ? precision : NumberFormatter.NO_ROUNDING); + switch (numberFormat) { + case Real.NumberFormat.FSE_ENG: + nf.useEngineeringFormat(NumberFormatter.DEFAULT_MAGNITUDE); + break; + case FSE_SCI: + nf.useScientificFormat(NumberFormatter.DEFAULT_MAGNITUDE); + break; + default: + nf.useSimpleFormat(); + break; } return nf.format(value, nb.radix).toString(); } @@ -307,7 +316,14 @@ public class JsclMathEngine implements MathEngine { } public void setScienceNotation(boolean scienceNotation) { - this.scienceNotation = scienceNotation; + setNumberFormat(scienceNotation ? FSE_SCI : FSE_NONE); + } + + public void setNumberFormat(int numberFormat) { + if (numberFormat != FSE_SCI && numberFormat != FSE_ENG && numberFormat != FSE_NONE) { + throw new IllegalArgumentException("Unsupported format: " + numberFormat); + } + this.numberFormat = numberFormat; } public char getGroupingSeparator() { diff --git a/jscl/src/main/java/org/solovyev/common/NumberFormatter.java b/jscl/src/main/java/org/solovyev/common/NumberFormatter.java index dbfbfcc5..857b9f9e 100644 --- a/jscl/src/main/java/org/solovyev/common/NumberFormatter.java +++ b/jscl/src/main/java/org/solovyev/common/NumberFormatter.java @@ -5,21 +5,28 @@ import midpcalc.Real; import javax.annotation.Nonnull; import java.math.BigDecimal; +import static java.lang.Math.pow; import static midpcalc.Real.NumberFormat.*; public class NumberFormatter { public static final int NO_GROUPING = 0; - public static final int DEFAULT_PRECISION = 16; + public static final int NO_ROUNDING = -1; public static final int DEFAULT_MAGNITUDE = 5; + public static final int MAX_PRECISION = 16; private final Real.NumberFormat numberFormat = new Real.NumberFormat(); private final Real real = new Real(); private int format = FSE_NONE; private int simpleFormatMagnitude = DEFAULT_MAGNITUDE; - private int precision = DEFAULT_PRECISION; + private int precision = MAX_PRECISION; private char groupingSeparator; + public void useScientificFormat(int simpleFormatMagnitude) { + this.format = FSE_SCI; + this.simpleFormatMagnitude = simpleFormatMagnitude; + } + public void useEngineeringFormat(int simpleFormatMagnitude) { this.format = FSE_ENG; this.simpleFormatMagnitude = simpleFormatMagnitude; @@ -49,19 +56,27 @@ public class NumberFormatter { throw new IllegalArgumentException("Unsupported radix: " + radix); } double absValue = Math.abs(value); - final boolean dec = radix == 10; - final boolean fixedFormat = !dec || format == FSE_NONE || Math.pow(10, -simpleFormatMagnitude) <= absValue && absValue < Math.pow(10, simpleFormatMagnitude); + final boolean simpleFormat = useSimpleFormat(radix, absValue); - if (fixedFormat) { - final int newScale = (int) (precision * Math.max(1, radix / 10f)); + final int effectivePrecision = precision == NO_ROUNDING ? MAX_PRECISION : precision; + if (simpleFormat) { + final int newScale = (int) (effectivePrecision * Math.max(1, radix / 10f)); value = BigDecimal.valueOf(value).setScale(newScale, BigDecimal.ROUND_HALF_UP).doubleValue(); absValue = Math.abs(value); } - numberFormat.fse = fixedFormat ? FSE_FIX : FSE_ENG; + if (simpleFormat) { + numberFormat.fse = FSE_FIX; + } else if (format == FSE_NONE) { + // originally, a simple format was requested but we have to use something more appropriate, f.e. scientific + // format + numberFormat.fse = FSE_SCI; + } else { + numberFormat.fse = format; + } numberFormat.thousand = groupingSeparator; - numberFormat.precision = precision; + numberFormat.precision = effectivePrecision; numberFormat.base = radix; - numberFormat.maxwidth = fixedFormat ? 100 : 30; + numberFormat.maxwidth = simpleFormat ? 100 : 30; if (radix == 2 && value < 0) { return "-" + prepare(absValue); @@ -69,6 +84,23 @@ public class NumberFormatter { return prepare(value); } + private boolean useSimpleFormat(int radix, double absValue) { + if (radix != 10) { + return true; + } + if (absValue < pow(10, -MAX_PRECISION)) { + // should never use simple format for small numbers + return false; + } + if (format == FSE_NONE) { + return true; + } + if (pow(10, -simpleFormatMagnitude) <= absValue && absValue < pow(10, simpleFormatMagnitude)) { + return true; + } + return false; + } + @Nonnull private CharSequence prepare(double value) { return stripZeros(realFormat(value)).replace('e', 'E'); diff --git a/jscl/src/test/java/jscl/JsclMathEngineTest.java b/jscl/src/test/java/jscl/JsclMathEngineTest.java index 7abad5e0..96043c89 100644 --- a/jscl/src/test/java/jscl/JsclMathEngineTest.java +++ b/jscl/src/test/java/jscl/JsclMathEngineTest.java @@ -1,5 +1,6 @@ package jscl; +import midpcalc.Real; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -85,7 +86,7 @@ public class JsclMathEngineTest { @Test public void testEngineeringNotationWithRounding() throws Exception { final JsclMathEngine me = JsclMathEngine.getInstance(); - me.setScienceNotation(true); + me.setNumberFormat(Real.NumberFormat.FSE_ENG); me.setRoundResult(true); me.setPrecision(5); @@ -141,7 +142,7 @@ public class JsclMathEngineTest { @Test public void testEngineeringNotationWithoutRounding() throws Exception { final JsclMathEngine me = JsclMathEngine.getInstance(); - me.setScienceNotation(true); + me.setNumberFormat(Real.NumberFormat.FSE_ENG); me.setRoundResult(false); assertEquals("10E6", me.format(10000000d)); diff --git a/jscl/src/test/java/org/solovyev/common/NumberFormatterTest.java b/jscl/src/test/java/org/solovyev/common/NumberFormatterTest.java index 6ac9c5d0..c00a02be 100644 --- a/jscl/src/test/java/org/solovyev/common/NumberFormatterTest.java +++ b/jscl/src/test/java/org/solovyev/common/NumberFormatterTest.java @@ -3,7 +3,10 @@ package org.solovyev.common; import org.junit.Before; import org.junit.Test; +import static java.lang.Math.pow; import static org.junit.Assert.assertEquals; +import static org.solovyev.common.NumberFormatter.DEFAULT_MAGNITUDE; +import static org.solovyev.common.NumberFormatter.NO_ROUNDING; public class NumberFormatterTest { @@ -25,12 +28,109 @@ public class NumberFormatterTest { } @Test - public void testSimpleFormat() throws Exception { + public void testScientificFormatNoRounding() throws Exception { + numberFormatter.useScientificFormat(DEFAULT_MAGNITUDE); + numberFormatter.setPrecision(NO_ROUNDING); + + assertEquals("1", numberFormatter.format(1d)); + assertEquals("0.3333333333333333", numberFormatter.format(1d / 3)); + assertEquals("3.333333333333333E-19", numberFormatter.format(pow(10, -18) / 3)); + assertEquals("1.23456789E18", numberFormatter.format(123456789 * pow(10, 10))); + assertEquals("1E-16", numberFormatter.format(pow(10, -16))); + assertEquals("5.9999999999999949E18", numberFormatter.format(5999999999999994999d)); + + testScientificFormat(); + } + + @Test + public void testScientificFormatWithRounding() throws Exception { + numberFormatter.useScientificFormat(DEFAULT_MAGNITUDE); + numberFormatter.setPrecision(5); + + assertEquals("1", numberFormatter.format(1d)); + assertEquals("0.33333", numberFormatter.format(1d / 3)); + assertEquals("3.33333E-19", numberFormatter.format(pow(10, -18) / 3)); + assertEquals("1.23457E18", numberFormatter.format(123456789 * pow(10, 10))); + assertEquals("1E-16", numberFormatter.format(pow(10, -16))); + assertEquals("6E18", numberFormatter.format(5999999999999994999d)); + + + testScientificFormat(); + } + + @Test + public void testSimpleFormatNoRounding() throws Exception { numberFormatter.useSimpleFormat(); + numberFormatter.setPrecision(NO_ROUNDING); + + assertEquals("1", numberFormatter.format(1d)); + assertEquals("0.000001", numberFormatter.format(pow(10, -6))); + assertEquals("0.3333333333333333", numberFormatter.format(1d / 3)); + assertEquals("3.333333333333333E-19", numberFormatter.format(pow(10, -18) / 3)); + assertEquals("1234567890000000000", numberFormatter.format(123456789 * pow(10, 10))); + assertEquals("0.0000000000000001", numberFormatter.format(pow(10, -16))); + + testSimpleFormat(); + } + + @Test + public void testSimpleFormatWithRounding() throws Exception { + numberFormatter.useSimpleFormat(); + numberFormatter.setPrecision(5); + + assertEquals("1", numberFormatter.format(1d)); + assertEquals("0", numberFormatter.format(pow(10, -6))); + assertEquals("0.33333", numberFormatter.format(1d / 3)); + assertEquals("3.33333E-19", numberFormatter.format(pow(10, -18) / 3)); + assertEquals("1234567890000000000", numberFormatter.format(123456789 * pow(10, 10))); + assertEquals("0", numberFormatter.format(pow(10, -16))); + + testSimpleFormat(); + } + + // testing simple format with and without rounding + private void testSimpleFormat() { + assertEquals("0.00001", numberFormatter.format(pow(10, -5))); + assertEquals("100", numberFormatter.format(pow(10, 2))); + assertEquals("1000000000000000000", numberFormatter.format(pow(10, 18))); + assertEquals("1E19", numberFormatter.format(pow(10, 19))); + assertEquals("1E20", numberFormatter.format(pow(10, 20))); + assertEquals("1E100", numberFormatter.format(pow(10, 100))); + + assertEquals("0.01", numberFormatter.format(pow(10, -2))); + assertEquals("1E-17", numberFormatter.format(pow(10, -17))); + assertEquals("1E-18", numberFormatter.format(pow(10, -18))); + assertEquals("1.5E-18", numberFormatter.format(1.5 * pow(10, -18))); + assertEquals("1E-100", numberFormatter.format(pow(10, -100))); + assertEquals("5000000000000000000", numberFormatter.format(5000000000000000000d)); assertEquals("5000000000000000000", numberFormatter.format(5000000000000000001d)); assertEquals("5999999999999994900", numberFormatter.format(5999999999999994999d)); assertEquals("5E19", numberFormatter.format(50000000000000000000d)); assertEquals("5E40", numberFormatter.format(50000000000000000000000000000000000000000d)); } + + // testing scientific format with and without rounding + private void testScientificFormat() { + assertEquals("0.00001", numberFormatter.format(pow(10, -5))); + assertEquals("1E-6", numberFormatter.format(pow(10, -6))); + assertEquals("100", numberFormatter.format(pow(10, 2))); + assertEquals("10000", numberFormatter.format(pow(10, 4))); + assertEquals("1E5", numberFormatter.format(pow(10, 5))); + assertEquals("1E18", numberFormatter.format(pow(10, 18))); + assertEquals("1E19", numberFormatter.format(pow(10, 19))); + assertEquals("1E20", numberFormatter.format(pow(10, 20))); + assertEquals("1E100", numberFormatter.format(pow(10, 100))); + + assertEquals("0.01", numberFormatter.format(pow(10, -2))); + assertEquals("1E-17", numberFormatter.format(pow(10, -17))); + assertEquals("1E-18", numberFormatter.format(pow(10, -18))); + assertEquals("1.5E-18", numberFormatter.format(1.5 * pow(10, -18))); + assertEquals("1E-100", numberFormatter.format(pow(10, -100))); + + assertEquals("5E18", numberFormatter.format(5000000000000000000d)); + assertEquals("5E18", numberFormatter.format(5000000000000000001d)); + assertEquals("5E19", numberFormatter.format(50000000000000000000d)); + assertEquals("5E40", numberFormatter.format(50000000000000000000000000000000000000000d)); + } } \ No newline at end of file