Zahlen runden

Irgendwie komme ich bei einem kniffligen Problem nicht weiter, je nach der Anzahl der Vor- und Nachkommastellen sollen double-Werte nach folgendem Schema gerundet werden:

1.23456 → 1.2
12.3456 → 12.3
123.456 → 123.0
1234.56 → 1235.0
12345.6 → 12346.0
0.123456 → 0.12
0.0123456 → 0.012
0.00123456 → 0.0012
0.000123456 → 0.0001

Hier mein Versuch:

	private double roundHalf(double f) {
		f = BigDecimal.valueOf(f).setScale(getScale(f), RoundingMode.HALF_UP).doubleValue();
		System.out.println(f);
		return f;
	}

	private int getScale(double f) {
		String s = String.valueOf(f);
		int a = s.indexOf('.');
		Pattern p = Pattern.compile("[1-9]");
		Matcher m = p.matcher(s);
		if (m.find()) {
			int b = m.start();
			if (a >= 3) {
				return 0;
			} else {
				if (b >= 2) {
					return b;
				} else {
					return 1;
				}
			}
		}
		return 0;
	}

Ist dieser Ansatz „gut“? Für 0.000123456 funktioniert er z. B. nicht.

(Beitrag wurde vom Autor zurückgezogen und wird automatisch in 2400 Stunden gelöscht, sofern dieser Beitrag nicht gemeldet wird)

Habs endlich. :relaxed:

		roundHalf(1.23456);
		roundHalf(12.3456);
		roundHalf(123.456);
		roundHalf(1234.56);
		roundHalf(12345.6);
		roundHalf(0.123456);
		roundHalf(0.0123456);
		roundHalf(0.00123456);
		roundHalf(0.000123456);

	private double roundHalf(double f) {
		f = BigDecimal.valueOf(f).setScale(getScale(f), RoundingMode.HALF_UP).doubleValue();
		System.out.println(f);
		return f;
	}

	private int getScale(double f) {
		String s = String.format("%f", f);
		int a = s.indexOf(',');
		Pattern p = Pattern.compile("[1-9]");
		Matcher m = p.matcher(s);
		if (m.find()) {
			int b = m.start();
			if (a >= 2) {
				if (3 - a < 0) {
					return 0;
				} else {
					return 3 - a;
				}
			} else {
				if (b >= 2) {
					return b + 1;
				} else {
					return 2;
				}
			}
		}
		return 0;
	}

1.23
12.3
123.0
1235.0
12346.0
0.123
0.0123
0.00123
1.23E-4

So bleiben alle Vorkommastellen erhalten + mindestens 3 Nicht-0-Ziffern bleiben erhalten + .5 wird aufgerundet. Falls jemand noch eine Vereinfachung sieht, bitte her damit.

Byte-Welt-Wiki: Fließkommazahlen mit Java runden

Vielleicht findest du da noch Anregungen.

2 Likes

Weißt du zufällig, wie man das Komma als Dezimaltrennzeichen wegbekommen könnte? Ich nehme an, auf amerikanischen Systemen stände dort ein . … (Portabilität)

Was genau ist dein Ziel? Willst du die Kommas in einer Kommazahl loswerden? → Sinn?
Oder geht es dir darum statt dem Komma ein Punkt zu haben?

Ne, es geht darum, dass ich die Menge und den Preis von Aktien in double-Genauigkeit habe und berechne, der „Broker“ will aber nicht endlos lange Dezimalzahlen haben.

Beispiel:
Ein BTC kostet gerade 40275 USD, eine 40275,55-Order mag er aber nicht.
Oder ein Dogecoin kostet gerade 0.0539 USD, eine 0,05299-Order mag das System aber auch nicht.

Die Schwierigkeit besteht also darin, für „wilde Zahlen“ deren höchstwertigen Ziffernstellen herauszufinden, die ungleich 0 sind. (Hoffe, man kann das verstehen)

while(number<1) {number*=10;}

Das wäre eine Idee, aber vielleicht könnte man auch, anstatt den Scale-Wert arithmetisch zu ermitteln, Look-Up-Tables verwenden, also zum Beispiel so:
if (f <= 150 && f > 50) { return 1; }, etc.,
aber dann müsste ich die „Backend-Logik“ kennen, und die kenne ich ja nicht. :wink:

Das „Schema“ aus dem ersten Post ist nur eine Sammlung von Beispielen. Wie es bei Computern und Programmierung so üblich ist, müßte man genauer beschreiben, was man will. Vielleicht sollte "So bleiben alle Vorkommastellen erhalten + mindestens 3 Nicht-0-Ziffern bleiben erhalten + .5 wird aufgerundet." ein Versuch sein, aber … das wirkt halt schon arg beliebig. Und wenn du jetzt genau begründen solltest, warum da zwar 12.3 (und nicht 12.0) aber 123.0 (und nicht 123.4) rauskommen sollte, müßtest du wahrscheinlich sowas sagen wie " :man_shrugging: ja ich will das halt so…".

Wie auch immer:

An sich wirkt das Problem sehr ähnlich zum klassischen Problem, das man hat, wenn man z.B. einen Funktionsplotter implementieren will, und dort Achsenbeschriftungen vorkommen sollen. Egal, ob der dargestellte Bereich zwischen 0.00001 und 0.00002 liegt, oder zwischen 100000 und 200000 (oder zwischen 100000 und 100001 ?!), die Achsenbeschriftung soll „schön aussehen“ und „irgendwie passen“.

Und… da ich schonmal sowas gemacht habe, habe ich auch sowas mal versucht. Der Code liegt unter Viewer/Axes.java at master · javagl/Viewer · GitHub . Da du aus irgendeinem Grund eine Stelle mehr wolltest, ist der Code hier mal angepasst:

import java.util.Locale;

public class AxesFormattingTest
{
    public static void main(String[] args)
    {
        testFormat();
    }
    private static void testFormat()
    {
        testFormat(1.23456);
        testFormat(12.3456);
        testFormat(123.456);
        testFormat(1234.56);
        testFormat(12345.6);
        testFormat(0.123456);
        testFormat(0.0123456);
        testFormat(0.00123456);
        testFormat(0.000123456);        
    }

    private static void testFormat(double value)
    {
        String formatString = formatStringForBW(value);
        String s = String.format(Locale.ENGLISH, formatString, value);
        System.out.println("For "+value+" format "+formatString+" gives "+s);
    }
    
    /**
     * Modified for byte-welt.net post:
     * 
     * Returns a format string that can be used in <code>String#format</code> 
     * to format values of the given order. The exact meaning of this is 
     * intentionally left unspecified, but for numbers that are "reasonable" 
     * to be displayed as decimal numbers (without scientific notation),
     * this function will return a format with the "appropriate" number of
     * decimal digits in order to format axis labels.  
     * 
     * @param order The order
     * @return The format string
     */
    static String formatStringForBW(double order)
    {
        if (order < 1e-100 || !Double.isFinite(order))
        {
            return "%f";
        }
        double exponent = Math.floor(Math.log10(order));
        int digits = (int)Math.abs(exponent);
        if (order >= 1.0)
        {
            digits = 0;
        }
        digits += 1; // For BW
        String result = "%."+digits+"f";
        return result;
    }
    
}

Die Ausgabe ist

For 1.23456 format %.1f gives 1.2
For 12.3456 format %.1f gives 12.3
For 123.456 format %.1f gives 123.5
For 1234.56 format %.1f gives 1234.6
For 12345.6 format %.1f gives 12345.6
For 0.123456 format %.2f gives 0.12
For 0.0123456 format %.3f gives 0.012
For 0.00123456 format %.4f gives 0.0012
For 1.23456E-4 format %.5f gives 0.00012

Vielleicht ist das mit der Stelleberechnung per log10 zumindest eine Inspiration.

  • Über Performance brauchen wir im Vergleich zu BigDecimal, String#format und Regex ( :roll_eyes: ) ja vermutlich nicht zu reden.
  • Was bei negativen Zahlen rauskommt, auch nicht (BTW: Die order bei mir ist, wie der Name schon sagt, nicht die zu formatierende Zahl selbst - d.h. die Funktion wird nur mit postitiven Zahlen aufgerufen, und liefert nur den Format-String)
  • Darüber, was auf einem Rechner mit anderer Locale rauskommt, wenn man s.indexOf(',') macht, auch nicht.
  • Über solchen irritierend-haarsträubend-planlos-beliebig-murksigen Firlefanz wie if (3 - a < 0) (vs. if (a > 3)) eigentlich auch nicht.

Bleibt noch was übrig, worüber man reden müßte? Nah, für mich im Moment nicht.

Ich brauche dieses „spezielle Runden“ nur für Zahlen größer-gleich 0. (Ein negativer Kaufpreis wird unter Kaufleuten als unsinnig angesehen… Kleiner Spaß :smile: )

Danke für den Tipp mit Locale und mit log10.

Mir ist die Regel dahinter nicht ganz klar. Wieso ist die Nachkommastelle im ersten Fall 3 und im zweiten Fall 0?
Ist wie mit Folgen: 1,2,3,4,5,… Wie lautet die nächste Zahl. Ich sage 17. :slight_smile:

Ich denke, ich hab es jetzt

	public double roundFair(double d) {
		double log = Math.round(Math.log10(d));
		if (log > 0) {
			return BigDecimal.valueOf(d).setScale(log == 1 ? 1 : 0, RoundingMode.HALF_UP).doubleValue();
		}
		return BigDecimal.valueOf(d).setScale(2 - (int) log, RoundingMode.HALF_UP).doubleValue();
	}

		System.out.println(roundFair(3.11111));
		System.out.println(roundFair(4.11111));
		System.out.println(roundFair(10));
		System.out.println(roundFair(100));
		System.out.println(roundFair(1000));

		System.out.println(roundFair(1.23456));
		System.out.println(roundFair(12.3456));
		System.out.println(roundFair(123.456));
		System.out.println(roundFair(1234.56));
		System.out.println(roundFair(12345.6));
		System.out.println(roundFair(0.123456));
		System.out.println(roundFair(0.0123456));
		System.out.println(roundFair(0.00123456));
		System.out.println(roundFair(0.000123456));

3.11
4.1
10.0
100.0
1000.0
1.23
12.3
123.0
1235.0
12346.0
0.123
0.0123
0.00123
1.23E-4

Wobei nun aber 3.11111 zu 3.11 wird und 4.11111 zu 4.1 wird, aber so werden denke ich alle Werte vom Server akzeptiert.

Hm… ich hab damals fuer ein charting script die maximalen Nachkommastellen pro Asset haben muessen, sonst war es nicht moeglich zu wissen wieviele ich darstellen soll.
Sowas aendert sich auch, speziell bei Assets bei denen der Preis sehr schnell grosse Spruenge machen kann.

Edit: d.h. also bei Doge 4 Nachkommastellen, und bei BTC gar keine (?), zumindest im Moment.

Das Thema ist nun gelöst.

	public double roundFair(double d) {
		return BigDecimal.valueOf(d).setScale(getScale(d), RoundingMode.HALF_UP).doubleValue();
	}

	private int getScale(double d) {
		if (d >= 10 && d < 100) {
			return 2;
		}
		int log = (int) Math.ceil(Math.log10(d));
		if (log > 0) {
			return log <= 2 ? 3 - log : 0;
		}
		return 3 - log;
	}

Nun ja, ich könnte auch beim Server nachfragen, wie viele Nachkommastellen er haben möchte, aber ich könnte es auch selber berechnen, das ist daten- und stromsparender. :sunglasses:

So ist es!

Sorry, irgendwie kann ich die einfachste Bool’sche Algebra nicht mehr oder ich werde senil :smile: :smile: Kann ja mal vorkommen…

So ist’s richtig:

	private int getScale(double d) {
		if (d >= 10 && d < 100) {
			return 2;
		}
		int log = (int) Math.ceil(Math.log10(d));
		if (log < 3) {
			return 3 - log;
		}
		return 0;
	}