Java Menge und Preis auf zwei Stellen Signifikanz anhand realer Bedingungen runden in Java

kali-hi

Banned
Registriert
Sep. 2025
Beiträge
760
Hallo, irgendwie sehe ich den Wald vor lauter Bäumen gerade nicht...

Ich möchte die Menge und den Preis auf zwei Stellen Signifikanz anhand realer Bedingungen runden.

Beispiel:

PreisMengePreis neugerundetgerundet
95000.00.000052631695380.00.00005395380.0
2000.00.00252008.00.00252008.0
0.510.00.50210.00.502
0.0771.42857142860.0702871.00.0702
0.00124166.66666666670.00120484166.00.001204
0.000045111111.11111111110.00004518111111.00.0000451
111111.00.000045111555.4440.000045111555.44

Code:

Java:
  public static double formatAmount(double price, double amount) {
    double rawQuantity = 1.0 / price;
    int zeros = numberOfZeros(rawQuantity);
    double precision = Math.pow(10, -zeros);
    return Math.round(amount / precision) * precision;
  }

  public static double formatPrice(double old_price, double new_price) {
    return formatAmount(old_price, new_price);
  }

  public static int numberOfZeros(double value) {
    String text = String.format(Locale.ENGLISH, "%.10f", value);
    int indexOfDecimal = text.indexOf('.');
    String integerPart = text.substring(0, indexOfDecimal);
    String fractionPart = text.substring(indexOfDecimal + 1);
    if (Integer.parseInt(integerPart) == 0) {
      int count = 2;
      for (char c : fractionPart.toCharArray()) {
        if (c == '0') {
          count++;
        } else {
          break;
        }
      }
      return count;
    }
    return -integerPart.length();
  }

  public static void testFormatMethods() {
    double[][] testValues = {
      {95000.0, 5.0 / 95000.0},
      {2000.0, 5.0 / 2000.0},
      {0.5, 5.0 / 0.5},
      {0.07, 5.0 / 0.07},
      {0.0012, 5.0 / 0.0012},
      {0.000045, 5.0 / 0.000045},
      {111111.0, 5.0 / 111111.0}
    };
    int index = 1;
    for (double[] pair : testValues) {
      double price = pair[0];
      double amount = pair[1];
      double rawPrice = price * 1.004;
      double formattedAmount = formatAmount(price, amount);
      double formattedPrice = formatPrice(price, rawPrice);
      System.out.printf(
          Locale.ENGLISH,
          "%d. Price: %.10f, Raw Amount: %.10f, Raw Price: %.10f => Formatted Amount: %.10f, Formatted Price: %.10f%n",
          index++,
          price,
          amount,
          rawPrice,
          formattedAmount,
          formattedPrice);
    }
  }

Ausgabe:

Code:
1. Price: 95000.0000000000, Raw Amount: 0.0000526316, Raw Price: 95380.0000000000 => Formatted Amount: 0.0000530000, Formatted Price: 95380.0000000000
2. Price: 2000.0000000000, Raw Amount: 0.0025000000, Raw Price: 2008.0000000000 => Formatted Amount: 0.0025000000, Formatted Price: 2008.0000000000
3. Price: 0.5000000000, Raw Amount: 10.0000000000, Raw Price: 0.5020000000 => Formatted Amount: 10.0000000000, Formatted Price: 0.0000000000
4. Price: 0.0700000000, Raw Amount: 71.4285714286, Raw Price: 0.0702800000 => Formatted Amount: 100.0000000000, Formatted Price: 0.0000000000
5. Price: 0.0012000000, Raw Amount: 4166.6666666667, Raw Price: 0.0012048000 => Formatted Amount: 4000.0000000000, Formatted Price: 0.0000000000
6. Price: 0.0000450000, Raw Amount: 111111.1111111111, Raw Price: 0.0000451800 => Formatted Amount: 100000.0000000000, Formatted Price: 0.0000000000
7. Price: 111111.0000000000, Raw Amount: 0.0000450000, Raw Price: 111555.4440000000 => Formatted Amount: 0.0000450000, Formatted Price: 111555.4440000000

Bei 4, 5, 6 stimmt die Menge nicht und bei 3, 4, 5, 6, 7 stimmt der Preis nicht.

Sieht vielleicht jemand, was falsch ist? Vorweg: Das sind keine Hausaufgaben. :rolleyes:
 
Was ist Zeile 45? Warum kommen da 4 promille drauf, ist das ne Preissteigerung?

Und numberOfZeros? Du kommst auf die signifikanten Stellen mit log/log10 (log(10)==log(x)/log(10)) . log10(1)=0, log10(10)=1, etc. (int) casted runter, daher bist du bei log10(2)=0.xxx => 0 fast direkt richtig.

Du musst dann nur aufpassen dass double halt gewisse Werte nicht genau abbildet (z.B. 10.5).

EDIT: Math.floor für die signifikanten Stellen, weil bei negativen willst du ja towards -INF runden.
 
  • Gefällt mir
Reaktionen: kali-hi
Hancock schrieb:
Was ist Zeile 45?
Zeile 45 hab ich als Beispiel hinzugefügt. Zum aktuellen Preis soll in der Schleife immer +0.4% gerechnet werden, damit auch "krumme" Beträge zum Testen entstehen.

Also einfach den 10er log verwenden?
 
kali-hi schrieb:
Ich möchte die Menge und den Preis auf zwei Stellen Signifikanz anhand realer Bedingungen runden.
Wie meinen? Welche Bedingungen? Mit der Tabelle kann ich zumindest nichts anfangen. Am besten du erklärst die Anforderung anhand eines Beispieles.
 
Ich hoffe dir ist bewusst, dass du mit double arbeitest hier in deinen Beispielen und nicht mit BigDecimal. Was auch immer du da mit Finanzwerten mit double machst ist falsch.
 
  • Gefällt mir
Reaktionen: Bob.Sponge, JP-M und BeBur
@Hancock Hat geklappt:

Java:
  public static double formatAmount(double amount) {
    double rawAmount;
    if (amount < 1) {
      rawAmount = 10_000 * amount;
    } else {
      rawAmount = 100_000_000_000L / amount;
    }
    int numIntegerDigits = numberOfIntegerDigits(rawAmount);
    int numFractionDigits = Math.max(0, 8 - numIntegerDigits);
    double precision = Math.pow(10, -numFractionDigits);
    return Math.round(amount / precision) * precision;
  }

  public static int numberOfIntegerDigits(double value) {
    long intValue = (long) Math.abs(value);
    return Math.floorDiv((int) Math.log10(intValue), 1) + 1;
  }

  public static void testFormatMethods() {
    double[] testValues = {95000.0, 2000.0, 0.5, 0.07, 0.0012, 0.000045, 111111.0};
    int index = 1;
    for (double value : testValues) {
      double price2 = value * 1.004;
      double amount = 5.0 / value;
      double formattedPrice = formatAmount(price2);
      double formattedAmount = formatAmount(amount);
      System.out.printf(
          Locale.ENGLISH,
          "%d. %.10f %.10f %.10f -> %.10f %.10f%n",
          index++,
          value,
          price2,
          amount,
          formattedPrice,
          formattedAmount);
    }
  }

Code:
1. 95000.0000000000 95380.0000000000 0.0000526316 -> 95380.0000000000 0.0000000000
2. 2000.0000000000 2008.0000000000 0.0025000000 -> 2008.0000000000 0.0025000000
3. 0.5000000000 0.5020000000 10.0000000000 -> 0.5020000000 10.0000000000
4. 0.0700000000 0.0702800000 71.4285714286 -> 0.0702800000 71.0000000000
5. 0.0012000000 0.0012048000 4166.6666666667 -> 0.0012050000 4167.0000000000
6. 0.0000450000 0.0000451800 111111.1111111111 -> 0.0000000000 111111.1100000000
7. 111111.0000000000 111555.4440000000 0.0000450000 -> 111555.4400000000 0.0000000000

Einzig, die Menge bei 1 und 7 ist noch falsch. Ich kann auf 8 Stellen Präzision gehen...

Tornhoof schrieb:
Was auch immer du da mit Finanzwerten mit double machst ist falsch.
Nö. Solche pauschalen Aussagen sind immer falsch.
Ergänzung ()

kali-hi schrieb:
rawAmount = 10_000 * amount;
Oh... eine 0 vergessen
 
Zuletzt bearbeitet:
kali-hi schrieb:
Nö. Solche pauschalen Aussagen sind immer falsch.
Deine FormatAmount Funktion hat jetzt schon Rundungsfehler drinnen, all deine 10_0000 * double, 100_000_000_000L / double Rechnerei ist bei entsprechenden Eingabedaten verlustbehaftet.
 
  • Gefällt mir
Reaktionen: coolfabs und Wo bin ich hier
Dann les dir mal durch wie double funktioniert, vielleicht gibts einen Eureka moment ;)
 
  • Gefällt mir
Reaktionen: Bob.Sponge
Du vergisst, dass das Endergebnis gerundet sein soll, da spielt die Operatorungenauigkeit bei * und / keine Rolle. 🤷‍♂️
Ergänzung ()

Es geht ja nur um Eingabewerte zwischen 1.000.000 bis ca. 0.000001 und ich behaupte einfach mal, dass meine Methode für diese Werte immer genau ist - dass also die Rechen- und Darstellungsfehler von double in Bezug auf die * und / Operatoren (btw. + und - wären noch viel schlimmer...) vernachlässigbar sind, bzw. dann im Ergebnis nicht mehr auftreten.
 
Zuletzt bearbeitet:
Hier noch einmal mit BigDecimal:

Java:
  public static double formatAmount(double amount) {
    // Desired number of non-zero decimal places, i.e., "significant places". This can be fewer if
    // there are many int places on the left.
    final int precision = 2;
    Pattern p = Pattern.compile("\\.(0*)");

    BigDecimal bd = BigDecimal.valueOf(amount).stripTrailingZeros();
    int digits = bd.precision() - bd.scale();

    Matcher m = p.matcher(bd.toPlainString());
    if (!m.find()) {
      throw new IllegalStateException("No decimal point found in " + bd.toPlainString());
    }
    int zeros = m.group(1).length();

    if (amount < 1) {
      return bd.setScale(zeros + precision, RoundingMode.HALF_UP).doubleValue();
    }
    return bd.setScale(Math.min(precision, 8 - digits), RoundingMode.HALF_UP).doubleValue();
  }

  public static void testFormatMethods() {
    double rv = 7.0 / 6.0;
    double[] testValues = {
      rv * 100000,
      rv * 10000,
      rv * 1000,
      rv * 100,
      rv * 10,
      rv,
      rv / 10,
      rv / 100,
      rv / 1000,
      rv / 10000,
      rv / 100000
    };
    int index = 1;
    for (double value : testValues) {
      double price2 = value * (1.0 + (1.0 / 300.0));
      double amount = 5.0 / value;
      double formattedPrice = formatAmount(price2);
      double formattedAmount = formatAmount(amount);
      System.out.printf(
          Locale.ENGLISH,
          "%d. %.10f %.10f %.10f -> %.10f %.10f%n",
          index++,
          value,
          price2,
          amount,
          formattedPrice,
          formattedAmount);
    }
  }
 
Das ist ein bisschen besser, aber immer noch nicht richtig, weil die Funktion double annimmt und zurück gibt. Die Probleme beschränken sich auch nicht nur auf die Ungenauigkeiten an sich. Ein Haufen Zeug kann schief gehen, wenn man denkt, dass doubles sich wie reele Zahlen verhalten werden.

Man kann argumentieren, dass auf irgendeine Weise die gesamte Weltwirtschaft daran hängt, dass das rechnen mit doubles zuverlässig genug funktioniert - weil Excel mit double arbeitet.

Aber man muss da auch auf dieses xkcd verweisen Link und das die falsche Verwendung von double die Spitze des xkcd comic Eisberges ist und man semantisch falsche Dinge nicht produzieren sollte
 
  • Gefällt mir
Reaktionen: Bob.Sponge
BeBur schrieb:
Das ist ein bisschen besser, aber immer noch nicht richtig, weil die Funktion double annimmt und zurück gibt.
Hätte ich vielleicht etwas besser beschreiben sollen... Die Quelle der "Daten" liefert doubles, und will auch doubles wieder haben. Deshalb ist es ok, dass diese Methode double als Ein- und Ausgabe hat.

Btw:
kali-hi schrieb:
.stripTrailingZeros();
das strip war unnötig und provoziert bei 1.0 usw. einen Fehler.

BeBur schrieb:
und man semantisch falsche Dinge nicht produzieren sollte
Wie gesagt, ich bin kein Entscheider und muss das nehmen, was da ist ;)
 
Mal blöd von der Seite gefragt: Sollen jetzt irgendwelche Zahlen auf zwei signifikante Stellen gerundet werden oder irgendwelche Zahlen auf zwei Dezimalstellen gerundet werden oder irgendwelche Zahlen nach zwei Dezimalstellen abgeschnitten werden? Oder wieder was anderes? Aus dem Eröffnungspost wird mir das irgendwie nicht klar.
 
Hmmm, die Börse sagt einfach nur, sie nimmt 8 Stellen Genauigkeit entgegen...

Diese können so verteilt sein:
######.##
#####.###
####.####
###.#####
##.######
#.#######
0.########
0.0########
0.00########
usw.

Dies soll die Methode herausfinden, die signifikante Folge "ausschneiden" und "runden" (also die nachfolgenden Dezimalziffern entfernen).

Jetzt besser verständlich? Solange Bitcoin nicht die Millionenmarke knackt, ist ja auch noch alles ok ;)
 
@BeBur Danke, und diese mathematischen Funktionen sollte es in Java auch alle vorhanden sein :)

Vielleicht auch besser als die BigDecimal Variante
 
numerOfIntegerDigits ist leider gerade falsch (und zu oft) gecastet

return (int) Math.floor(Math.log10(Math.abs(val)))+1;

Du darfst nicht vorher zu long/int casten, weil 0.1 => 0 wäre und wenn du vor floor auf int castest, kannst du bei z.B. log10(0.1111) (==-0.9...) dann towards zero runden und kommst auf 1 als signifikante Stelle (statt 0).
 
  • Gefällt mir
Reaktionen: kali-hi
Danke, ich bin aber aktuell bei der BD Variante, um mir die eigene Rechnerei zu ersparen...
 
kali-hi schrieb:
Hmmm, die Börse sagt einfach nur, sie nimmt 8 Stellen Genauigkeit entgegen...
btw. ohne ausführliche automatische Tests ist es ziemlich fahrlässig, Geldtransfer automatisiert durch Code durchzuführen.
 
  • Gefällt mir
Reaktionen: Bob.Sponge
Zurück
Oben