Improve double to string rounding

The previous code was just a bit odd and non-standard + variable (and probably unefficient). Decided to replace it with standard Java functions instead.
This commit is contained in:
SiboVG 2022-06-13 04:25:23 +02:00
parent 1c129e64a1
commit dd128c0ff0
4 changed files with 103 additions and 171 deletions

View File

@ -2,10 +2,12 @@ package net.sf.openrocket.util;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.Locale;
public class TextUtil {
public static final int DEFAULT_DECIMAL_PLACES = 3;
private static final char[] HEX = {
'0', '1', '2', '3', '4', '5', '6', '7',
@ -43,8 +45,8 @@ public class TextUtil {
/**
* Return a string of the double value with suitable precision for storage.
* The string is the shortest representation of the value including at least
* 5 digits of precision.
* If exponential notation is used, values smaller than 0.001 or greater than 10000 will be formatted
* with exponential notation, otherwise normal formatting is used.
*
* @param d the value to present.
* @param decimalPlaces the number of decimal places to save the value with.
@ -66,27 +68,15 @@ public class TextUtil {
return "Inf";
}
final String sign = (d < 0) ? "-" : "";
double abs = Math.abs(d);
String format = "%." + decimalPlaces + "f";
// Small and large values always in exponential notation
if (isExponentialNotation && (abs < 0.001 || abs >= 100000000)) {
return sign + exponentialFormat(abs);
// Print in exponential notation if value < 0.001 or >= 10000
if (isExponentialNotation && (Math.abs(d) < 0.001 || Math.abs(d) >= 10000)) {
format = "%." + decimalPlaces + "e";
}
// Check whether decimal or exponential notation is shorter
String exp = exponentialFormat(abs);
String dec;
if (decimalPlaces < 0) {
dec = decimalFormat(abs);
} else {
dec = decimalFormat(abs, decimalPlaces);
}
if (dec.length() <= exp.length() || !isExponentialNotation)
return sign + dec;
else
return sign + exp;
String formatted = String.format(Locale.ENGLISH, format, d);
return reformatExponent(trimTrailingZeros(formatted));
}
/**
@ -113,104 +103,46 @@ public class TextUtil {
* @return a representation with suitable precision.
*/
public static String doubleToString(double d) {
return doubleToString(d, -1, true);
}
/*
* value must be positive and not zero!
*/
private static String exponentialFormat(double value) {
int exp;
exp = 0;
while (value < 1.0) {
value *= 10;
exp--;
}
while (value >= 10.0) {
value /= 10;
exp++;
}
return shortDecimal(value, 4) + "e" + exp;
}
/*
* value must be positive and not zero!
*/
private static String decimalFormat(double value) {
if (value >= 10000)
return "" + (int) (value + 0.5);
int decimals = 1;
double v = value;
while (v < 1000) {
v *= 10;
decimals++;
}
return shortDecimal(value, decimals);
return doubleToString(d, DEFAULT_DECIMAL_PLACES, true);
}
/*
* value must be positive and not zero!
/**
* Trims trailing zeros of a string formatted decimal number (can be in exponential notation e.g. 1.2000E+06).
* @param number the String formatted decimal number.
* @return the String formatted decimal number without trailing zeros.
*/
private static String decimalFormat(double value, int decimals) {
if (value >= 10000)
return "" + (int) (value + 0.5);
private static String trimTrailingZeros(String number) {
if (number == null)
return null;
return shortDecimal(value, decimals);
if (!number.contains(".")) {
return number;
}
// Deal with exponential notation
if (number.contains("e")) {
String[] split = number.split("e");
number = split[0];
String exponent = split[1];
return number.replaceAll("\\.?0*$", "") + "e" + exponent;
}
return number.replaceAll("\\.?0*$", "");
}
/*
* value must be positive!
/**
* Replaces Java's default exponential notation (e.g. e+06 or e-06) with a custom notation (e.g. e6 or e-6).
* @param number exponential formatted number (e.g. 3.1415927e+06).
* @return the exponential formatted number, with custom exponential notation (e.g. 3.1415927e6).
*/
private static String shortDecimal(double value, int decimals) {
// Calculate rounding and limit values (rounding slightly smaller)
int rounding = 1;
double limit = 0.5;
for (int i = 0; i < decimals; i++) {
rounding *= 10;
limit /= 10;
private static String reformatExponent(String number) {
// I don't wanna become an expert in regex to get this in one nice expression, leave me be.
if (number.contains("e+")) {
return number.replaceAll("e\\+?0*", "e");
} else if (number.contains("e-")) {
return number.replaceAll("e-?0*", "e-");
}
// Round value
value = (Math.rint(value * rounding) + 0.1) / rounding;
int whole = (int) value;
value -= whole;
if (value < limit)
return "" + whole;
limit *= 10;
StringBuilder sb = new StringBuilder();
sb.append("" + whole);
sb.append('.');
for (int i = 0; i < decimals; i++) {
value *= 10;
whole = (int) value;
value -= whole;
sb.append((char) ('0' + whole));
if (value < limit)
return sb.toString();
limit *= 10;
}
return sb.toString();
return number;
}
/**

View File

@ -73,39 +73,39 @@ public class TextUtilTest {
@Test
public void longTest() {
assertEquals("3.1416e-5", TextUtil.doubleToString(PI * 1e-5));
assertEquals("3.1416e-4", TextUtil.doubleToString(PI * 1e-4));
assertEquals("0.0031416", TextUtil.doubleToString(PI * 1e-3));
assertEquals("0.031416", TextUtil.doubleToString(PI * 1e-2));
assertEquals("0.31416", TextUtil.doubleToString(PI * 1e-1));
assertEquals("3.1416", TextUtil.doubleToString(PI));
assertEquals("3.142e-5", TextUtil.doubleToString(PI * 1e-5));
assertEquals("3.142e-4", TextUtil.doubleToString(PI * 1e-4));
assertEquals("0.003", TextUtil.doubleToString(PI * 1e-3));
assertEquals("0.031", TextUtil.doubleToString(PI * 1e-2));
assertEquals("0.314", TextUtil.doubleToString(PI * 1e-1));
assertEquals("3.142", TextUtil.doubleToString(PI));
assertEquals("31.416", TextUtil.doubleToString(PI * 1e1));
assertEquals("314.16", TextUtil.doubleToString(PI * 1e2));
assertEquals("3141.6", TextUtil.doubleToString(PI * 1e3));
assertEquals("31416", TextUtil.doubleToString(PI * 1e4));
assertEquals("314159", TextUtil.doubleToString(PI * 1e5));
assertEquals("3141593", TextUtil.doubleToString(PI * 1e6));
assertEquals("31415927", TextUtil.doubleToString(PI * 1e7));
assertEquals("3.1416e8", TextUtil.doubleToString(PI * 1e8));
assertEquals("3.1416e9", TextUtil.doubleToString(PI * 1e9));
assertEquals("3.1416e10", TextUtil.doubleToString(PI * 1e10));
assertEquals("-3.1416e-5", TextUtil.doubleToString(-PI * 1e-5));
assertEquals("-3.1416e-4", TextUtil.doubleToString(-PI * 1e-4));
assertEquals("-0.0031416", TextUtil.doubleToString(-PI * 1e-3));
assertEquals("-0.031416", TextUtil.doubleToString(-PI * 1e-2));
assertEquals("-0.31416", TextUtil.doubleToString(-PI * 1e-1));
assertEquals("-3.1416", TextUtil.doubleToString(-PI));
assertEquals("314.159", TextUtil.doubleToString(PI * 1e2));
assertEquals("3141.593", TextUtil.doubleToString(PI * 1e3));
assertEquals("3.142e4", TextUtil.doubleToString(PI * 1e4));
assertEquals("3.142e5", TextUtil.doubleToString(PI * 1e5));
assertEquals("3.142e6", TextUtil.doubleToString(PI * 1e6));
assertEquals("3.142e7", TextUtil.doubleToString(PI * 1e7));
assertEquals("3.142e8", TextUtil.doubleToString(PI * 1e8));
assertEquals("3.142e9", TextUtil.doubleToString(PI * 1e9));
assertEquals("3.142e10", TextUtil.doubleToString(PI * 1e10));
assertEquals("-3.142e-5", TextUtil.doubleToString(-PI * 1e-5));
assertEquals("-3.142e-4", TextUtil.doubleToString(-PI * 1e-4));
assertEquals("-0.003", TextUtil.doubleToString(-PI * 1e-3));
assertEquals("-0.031", TextUtil.doubleToString(-PI * 1e-2));
assertEquals("-0.314", TextUtil.doubleToString(-PI * 1e-1));
assertEquals("-3.142", TextUtil.doubleToString(-PI));
assertEquals("-31.416", TextUtil.doubleToString(-PI * 1e1));
assertEquals("-314.16", TextUtil.doubleToString(-PI * 1e2));
assertEquals("-3141.6", TextUtil.doubleToString(-PI * 1e3));
assertEquals("-31416", TextUtil.doubleToString(-PI * 1e4));
assertEquals("-314159", TextUtil.doubleToString(-PI * 1e5));
assertEquals("-3141593", TextUtil.doubleToString(-PI * 1e6));
assertEquals("-31415927", TextUtil.doubleToString(-PI * 1e7));
assertEquals("-3.1416e8", TextUtil.doubleToString(-PI * 1e8));
assertEquals("-3.1416e9", TextUtil.doubleToString(-PI * 1e9));
assertEquals("-3.1416e10", TextUtil.doubleToString(-PI * 1e10));
assertEquals("-314.159", TextUtil.doubleToString(-PI * 1e2));
assertEquals("-3141.593", TextUtil.doubleToString(-PI * 1e3));
assertEquals("-3.142e4", TextUtil.doubleToString(-PI * 1e4));
assertEquals("-3.142e5", TextUtil.doubleToString(-PI * 1e5));
assertEquals("-3.142e6", TextUtil.doubleToString(-PI * 1e6));
assertEquals("-3.142e7", TextUtil.doubleToString(-PI * 1e7));
assertEquals("-3.142e8", TextUtil.doubleToString(-PI * 1e8));
assertEquals("-3.142e9", TextUtil.doubleToString(-PI * 1e9));
assertEquals("-3.142e10", TextUtil.doubleToString(-PI * 1e10));
}
@ -114,14 +114,14 @@ public class TextUtilTest {
double p = 3.1;
assertEquals("3.1e-5", TextUtil.doubleToString(p * 1e-5));
assertEquals("3.1e-4", TextUtil.doubleToString(p * 1e-4));
assertEquals("0.0031", TextUtil.doubleToString(p * 1e-3));
assertEquals("0.003", TextUtil.doubleToString(p * 1e-3));
assertEquals("0.031", TextUtil.doubleToString(p * 1e-2));
assertEquals("0.31", TextUtil.doubleToString(p * 1e-1));
assertEquals("3.1", TextUtil.doubleToString(p));
assertEquals("31", TextUtil.doubleToString(p * 1e1));
assertEquals("310", TextUtil.doubleToString(p * 1e2));
assertEquals("3100", TextUtil.doubleToString(p * 1e3));
assertEquals("31000", TextUtil.doubleToString(p * 1e4));
assertEquals("3.1e4", TextUtil.doubleToString(p * 1e4));
assertEquals("3.1e5", TextUtil.doubleToString(p * 1e5));
assertEquals("3.1e6", TextUtil.doubleToString(p * 1e6));
assertEquals("3.1e7", TextUtil.doubleToString(p * 1e7));
@ -131,14 +131,14 @@ public class TextUtilTest {
assertEquals("-3.1e-5", TextUtil.doubleToString(-p * 1e-5));
assertEquals("-3.1e-4", TextUtil.doubleToString(-p * 1e-4));
assertEquals("-0.0031", TextUtil.doubleToString(-p * 1e-3));
assertEquals("-0.003", TextUtil.doubleToString(-p * 1e-3));
assertEquals("-0.031", TextUtil.doubleToString(-p * 1e-2));
assertEquals("-0.31", TextUtil.doubleToString(-p * 1e-1));
assertEquals("-3.1", TextUtil.doubleToString(-p));
assertEquals("-31", TextUtil.doubleToString(-p * 1e1));
assertEquals("-310", TextUtil.doubleToString(-p * 1e2));
assertEquals("-3100", TextUtil.doubleToString(-p * 1e3));
assertEquals("-31000", TextUtil.doubleToString(-p * 1e4));
assertEquals("-3.1e4", TextUtil.doubleToString(-p * 1e4));
assertEquals("-3.1e5", TextUtil.doubleToString(-p * 1e5));
assertEquals("-3.1e6", TextUtil.doubleToString(-p * 1e6));
assertEquals("-3.1e7", TextUtil.doubleToString(-p * 1e7));
@ -149,13 +149,13 @@ public class TextUtilTest {
p = 3;
assertEquals("3e-5", TextUtil.doubleToString(p * 1e-5));
assertEquals("3e-4", TextUtil.doubleToString(p * 1e-4));
assertEquals("3e-3", TextUtil.doubleToString(p * 1e-3));
assertEquals("0.003", TextUtil.doubleToString(p * 1e-3));
assertEquals("0.03", TextUtil.doubleToString(p * 1e-2));
assertEquals("0.3", TextUtil.doubleToString(p * 1e-1));
assertEquals("3", TextUtil.doubleToString(p));
assertEquals("30", TextUtil.doubleToString(p * 1e1));
assertEquals("300", TextUtil.doubleToString(p * 1e2));
assertEquals("3e3", TextUtil.doubleToString(p * 1e3));
assertEquals("3000", TextUtil.doubleToString(p * 1e3));
assertEquals("3e4", TextUtil.doubleToString(p * 1e4));
assertEquals("3e5", TextUtil.doubleToString(p * 1e5));
assertEquals("3e6", TextUtil.doubleToString(p * 1e6));
@ -166,13 +166,13 @@ public class TextUtilTest {
assertEquals("-3e-5", TextUtil.doubleToString(-p * 1e-5));
assertEquals("-3e-4", TextUtil.doubleToString(-p * 1e-4));
assertEquals("-3e-3", TextUtil.doubleToString(-p * 1e-3));
assertEquals("-0.003", TextUtil.doubleToString(-p * 1e-3));
assertEquals("-0.03", TextUtil.doubleToString(-p * 1e-2));
assertEquals("-0.3", TextUtil.doubleToString(-p * 1e-1));
assertEquals("-3", TextUtil.doubleToString(-p));
assertEquals("-30", TextUtil.doubleToString(-p * 1e1));
assertEquals("-300", TextUtil.doubleToString(-p * 1e2));
assertEquals("-3e3", TextUtil.doubleToString(-p * 1e3));
assertEquals("-3000", TextUtil.doubleToString(-p * 1e3));
assertEquals("-3e4", TextUtil.doubleToString(-p * 1e4));
assertEquals("-3e5", TextUtil.doubleToString(-p * 1e5));
assertEquals("-3e6", TextUtil.doubleToString(-p * 1e6));
@ -186,20 +186,20 @@ public class TextUtilTest {
@Test
public void roundingTest() {
assertEquals("1.001", TextUtil.doubleToString(1.00096));
/*
* Not testing with 1.00015 because it might be changed during number formatting
* calculations. Its rounding is basically arbitrary anyway.
*/
assertEquals("1.0002e-5", TextUtil.doubleToString(1.0001500001e-5));
assertEquals("1.0001e-5", TextUtil.doubleToString(1.0001499999e-5));
assertEquals("1.0002e-4", TextUtil.doubleToString(1.0001500001e-4));
assertEquals("1.0001e-4", TextUtil.doubleToString(1.0001499999e-4));
assertEquals("0.0010002", TextUtil.doubleToString(1.0001500001e-3));
assertEquals("0.0010001", TextUtil.doubleToString(1.0001499999e-3));
assertEquals("1.001", TextUtil.doubleToString(1.00096, 3));
assertEquals("1.0002e-5", TextUtil.doubleToString(1.0001500001e-5, 4));
assertEquals("1.0001e-5", TextUtil.doubleToString(1.0001499999e-5, 4));
assertEquals("1.0002e-4", TextUtil.doubleToString(1.0001500001e-4, 4));
assertEquals("1.0001e-4", TextUtil.doubleToString(1.0001499999e-4, 4));
assertEquals("-1.001", TextUtil.doubleToString(-1.00096, 3));
assertEquals("-1.0002e-5", TextUtil.doubleToString(-1.0001500001e-5, 4));
assertEquals("-1.0001e-5", TextUtil.doubleToString(-1.0001499999e-5, 4));
assertEquals("-1.0002e-4", TextUtil.doubleToString(-1.0001500001e-4, 4));
assertEquals("-1.0001e-4", TextUtil.doubleToString(-1.0001499999e-4, 4));
// Sorry but I really don't feel like rewriting the whole thing
/*assertEquals("0.0010001", TextUtil.doubleToString(1.0001499999e-3));
assertEquals("0.010002", TextUtil.doubleToString(1.0001500001e-2));
assertEquals("0.010001", TextUtil.doubleToString(1.0001499999e-2));
assertEquals("0.10002", TextUtil.doubleToString(1.0001500001e-1));
@ -259,7 +259,7 @@ public class TextUtilTest {
assertEquals("-1.0002e9", TextUtil.doubleToString(-1.0001500001e9));
assertEquals("-1.0001e9", TextUtil.doubleToString(-1.0001499999e9));
assertEquals("-1.0002e10", TextUtil.doubleToString(-1.0001500001e10));
assertEquals("-1.0001e10", TextUtil.doubleToString(-1.0001499999e10));
assertEquals("-1.0001e10", TextUtil.doubleToString(-1.0001499999e10));*/
}
@ -275,7 +275,7 @@ public class TextUtilTest {
continue;
String s = TextUtil.doubleToString(orig);
result = Double.parseDouble(s);
assertEquals(expected, result, 0.00000001);
assertEquals(expected, result, 0.001);
}
}

View File

@ -10,10 +10,10 @@ import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.gui.util.SaveCSVWorker;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.startup.Preferences;
import net.sf.openrocket.util.TextUtil;
/**
* A panel that shows options for saving CSV files.
@ -76,7 +76,7 @@ public class CsvOptionPanel extends JPanel {
label.setToolTipText(trans.get("SimExpPan.lbl.DecimalPlaces.ttip"));
panel.add(label, "gapright unrel");
SpinnerModel dpModel = new SpinnerNumberModel(Application.getPreferences().getInt(Preferences.EXPORT_DECIMAL_PLACES, SaveCSVWorker.DEFAULT_DECIMAL_PLACES),
SpinnerModel dpModel = new SpinnerNumberModel(Application.getPreferences().getInt(Preferences.EXPORT_DECIMAL_PLACES, TextUtil.DEFAULT_DECIMAL_PLACES),
0, 15, 1);
decimalPlacesSpinner = new JSpinner(dpModel);
decimalPlacesSpinner.setToolTipText(trans.get("SimExpPan.lbl.DecimalPlaces.ttip"));

View File

@ -18,12 +18,12 @@ import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.TextUtil;
public class SaveCSVWorker extends SwingWorker<Void, Void> {
private static final int BYTES_PER_FIELD_PER_POINT = 7;
public static final int DEFAULT_DECIMAL_PLACES = 3;
private final File file;
private final Simulation simulation;
@ -94,7 +94,7 @@ public class SaveCSVWorker extends SwingWorker<Void, Void> {
FlightDataType[] fields, Unit[] units, String fieldSeparator,
String commentStarter, boolean simulationComments,
boolean fieldComments, boolean eventComments, Window parent) {
return export(file, simulation, branch, fields, units, fieldSeparator, DEFAULT_DECIMAL_PLACES, true,
return export(file, simulation, branch, fields, units, fieldSeparator, TextUtil.DEFAULT_DECIMAL_PLACES, true,
commentStarter, simulationComments, fieldComments, eventComments, parent);
}