Präzision double

Kann mir jemand sagen, in welchem Bereich double präzise ist, wenn ich Festkommaarithmetik machen möchte?
Mal angenommen, dass ich mich auf 3 Nachkommastellen festgelegt hätte und ein um 0 symmetrisch liegendes Interwall als Definitionsbereich nutzen möchte.

bei double hängt die Genauigkeit nicht davon ab, wie viele Stellen Du verwenden willst. 0.3 ist eben als Binärzahl einfach nicht darstellbar.

byt
TT

Es geht mir um die Genauigkeit bei der Übertragung vom Interface als Double in ein Domänenobjekt. In etwa so:
long internal = Math.round(input * 1000.0);
Dabei ist input ein double, welches als Eingabe übergeben wird. 1234,567 soll dann zu 1234567 werden und bei einer Rückkonversion soll dann auch wieder 1234,567 herauskommen.

Ich versuche die Grenze gerade exploratorisch herauszubekommen. Der Wertebereich ist ziemlich groß, also versuche ich einen hybriden Ansatz aus bisektions- und iterativer Suche.

Das ist der falsche Ansatz. Wenn Genauigkeit eine Rolle spielt (bei Geldbeträgen etwa) ist BigDecimal als Datentyp angesagt. Primitive Floating-Point Typen sind nur dann sinnvoll, wenn’s richtig schnell gehen muss, aber Genauigkeit nicht das OK-Kriterium ist. Beispiele sind Spiele und wissenschaftliche Berechnungen auf großen Datenmengen.

Hier ist der Test:[SPOILER]```public class DoublePrecisionTest {
public static void main(String[] args) {
Random random = new Random();
double d = 1.0;
for (int i = 0; i < 1000; i++) {
d = d + .001;
long rounded = Math.round(d * 1000.0);

		System.out.print(d);
		System.out.print(" - ");
		System.out.print(rounded);
		System.out.print(" - ");
		System.out.print(d * 1000.0);
		System.out.print(" - ");
		System.out.print(rounded == (d * 1000.0));
		System.out.print(" - ");
		System.out.print((double)(rounded)/1000.0);
		System.out.print(" - ");
		System.out.println((double)(rounded)/1000.0 == d);
	}
}

}```[/SPOILER]

bye
TT

Das trifft nicht so ganz das, was ich meine. Ich möchte double nur als Ein- und Ausgabetyp verwenden. Es sollen also Werte mit 1000stel Genauigkeit eingegeben und wieder ausgegeben werden können.
Ich habe meinen Test jetzt fertig und hoffe, dass da nichts mehr falsch ist. Es ist ein Kompromiss aus Laufzeit und der Sicherheit, dass ich auch wirklich die Grenze gefunden habe.

Ich teste dabei genau die benötigte Funktionalität. Die entscheidenden Zeilen:

if (candidate != Math.round(Double.valueOf(number) * 1000)) return false;```


in toto
[spoiler]```public class PrecisionTest {
    private final int factor = 1000;
    private final String formatStringPos;
    private final String formatStringNeg;

    public PrecisionTest() {
        final int fractionPoints = (int) Math.round(Math.log10(factor));
        formatStringPos = "%d.%0" + fractionPoints + "d";
        formatStringNeg = "-%d.%0" + fractionPoints + "d";
        System.out.println(formatStringPos);
    }

    public static void main(String[] args) {
        System.out.println("Upper bound: " + binarySearchTop(100000, Long.MAX_VALUE - 100000));
        System.out.println("Lower bound: " + binarySearchBottom(Long.MIN_VALUE + 100000, -100000));
    }

    private long binarySearchTop(long lowerBound, long upperBound) {
        System.out.println("[" + lowerBound + ", " + upperBound + "]");
        if (lowerBound >= upperBound) return lowerBound;
        final long candidate = (upperBound - lowerBound) / 2 + lowerBound;
        if (checkPrecisionDown(candidate)) {
            return binarySearchTop(candidate + 1, upperBound);
        } else {
            return binarySearchTop(lowerBound, candidate - 1);
        }
    }

    private boolean checkPrecisionDown(long candidate) {
        for (long i = candidate; i > candidate - 100000; i--) {
            final String number = String.format(formatStringPos, i / factor, i % factor);
            if (i != Math.round(Double.valueOf(number) * factor)) return false;
        }
        return true;
    }

    private long binarySearchBottom(long lowerBound, long upperBound) {
        System.out.println("[" + lowerBound + ", " + upperBound + "]");
        if (lowerBound >= upperBound) return lowerBound;
        final long candidate = (upperBound - lowerBound) / 2 + lowerBound;
        if (checkPrecisionUp(candidate)) {
            return binarySearchBottom(lowerBound, candidate - 1);
        } else {
            return binarySearchBottom(candidate + 1, upperBound);
        }
    }

    private boolean checkPrecisionUp(long candidate) {
        for (long i = candidate; i < candidate + 100000; i++) {
            final String number = String.format(formatStringNeg, -i / factor, -i % factor);
            if (i != Math.round(Double.valueOf(number) * factor)) return false;
        }
        return true;
    }
}```[/spoiler]


Dabei bekomme ich die Grenzen -4398046511104011 und 4398046511104020 ausgegeben. D. h., dass man eine Gleitkommazahl mit genau 3 Nachkommastellen als String einlesen kann und sie dann verlustfrei mit 1000 multiplizieren und in einem long ablegen kann, sofern die eingegebene Zahl zwischen -4.398.046.511.104,011 und 4.398.046.511.104,020 liegt.

Wie zu erwarten war, liegen die Grenzen bei anderen Faktoren woanders:
2 Nachkommastellen: -35.184.372.088.832,04 bis 35.184.372.088.832,01
1 Nachkommastelle: -562.949.953.421.312,2 bis 562.949.953.421.312,3
0 Nachkommastellen: -9.007.199.254.740.992 bis 9.007.199.254.740.992

[quote=cmrudolph]dass man eine Gleitkommazahl mit genau 3 Nachkommastellen als String einlesen kann und sie dann verlustfrei mit 1000 multiplizieren und in einem long ablegen kann,[/quote]Dann würde ich die auch “als String” mit 1000 multiplizieren:[SPOILER]```public class DoublePrecisionTest {
private static final Pattern numberPattern = Pattern
.compile("(\d+)(\.(\d*))?");

public static void main(String[] args) {
	convertToLong("12345.678");
	convertToLong("12345.67");
	convertToLong("12345.6");
	convertToLong("12345.");
	convertToLong("12345");
}

private static void convertToLong(String string) {
	System.out.print(string.concat(" - "));
	Matcher matcher = numberPattern.matcher(string);
	if (matcher.find()) {
		String abs = matcher.group(1);
		String fraction = matcher.group(3);
		if (null != fraction)
			while (fraction.length() < 3)
				fraction = fraction.concat("0");
		else
			fraction = "000";
		long l = Long.parseLong(abs.concat(fraction));
		System.out.println(l);
	}
}

}```[/SPOILER]
bye
TT

Versuch doch der Einfachheit halber erst mal long = Math.round(double * 1000.0) und lote die Grenzen selber aus. Die dürften so bei 10^15,6… liegen.
ansonsten kann man doch einfach mit new StringBuilder(Double.toString()) den Dezimalpunkt um 3 Stellen verschieben, den Rest abschneiden und das Ganze dann in ein Long wandeln.

Wenn damit gerechnet werden soll, dann wäre ich da sehr vorsichtig.

a = b * 1000;
c = a * a;
d = c / 1000;
//dann folgt daraus
b * b != d;
//sondern 
b * b == d / 1000;

Da kommt man schnell mal durcheinander.

Ist eine gute Idee. Mal sehen, ob ich das dem Framework beibringen kann :wink:

Das habe ich ja gemacht (oder nur versucht?), kam aber auf die oben geposteten Grenzen. Wie kommst du auf 10^15,6. Mein Ergebnis unterscheidet sich davon immerhin um eine Größenordnung.

Den double bekomme ich vom Framework. Und nun möchte ich wissen, inwieweit die Werte verlässlich sind. Daber in meinem Test auch die Umwandeleskapade mit String generieren, parsen, multiplizieren und rückumwandeln.

Deshalb hat die Klasse auch eine vollständige Unittestabdeckung, damit da nichts schiefgeht.
Rechenoperationen erfolgen intern mit mul() oder add() und die Klasse ist immutable. Da wird nichts mehr schiefgehen.

[QUOTE=cmrudolph]Wie kommst du auf 10^15,6.[/QUOTE]Das ist 10^lg(2^53) und 2^53 ist der Maximalwert, der in die Mantisse eines Doubles (52 + implizit vorangestellte 1) passt, kurzgesagt, dessen Präzision. Wenn ich es mir recht überlege, dürften bei 3 Stellen hinterm Komma die Doublewerte also eigentlich nicht größer als 10^12,6 sein, weil wenn sie größer werden, hätten sie definitiv weniger als 3 Nachkommastellen im Ergebnis. Für Long bedeutet das aber immernoch 10^12,6 * 10^3 = 10^15,6 obwohl ein Long einen Maximalwert 10^18,9 (+/- 2^64 - 1) haben kann.

BTW.: Man könnte meinen, dass die Mantisse eines Doubles Werte von +/- 2^54-1 annehmen kann. Darauf sollte man sich aber nicht verlassen, weil mit Bit 52 hin und wieder mal ein NaNq (NaN quiet - NaN als Ergebnis einer zulässigen Berechnung, die keine ArithmeticException wirft) signalisiert wird. Wenn dann dieses Bit gesetzt ist, ist die Zahl NaN, die restlichen Matissenbits bedeutungslos (in Java zumindest) und es wird keine Exception ausgelöst.

BTW2.: Wenn dir die Idee mit dem StringBuilder besser gefällt (die ist auf jeden Fall sicherer, weil verschoben statt gerechnet wird), solltest du zu dem Doublewert immer 0,0005 hinzuzählen, damit Werte mit 4+ Nachkommastellen korrekt gerundet werden.

Laut meinem Taschenrechner ist der Zehnerlogarithmus von 2^53 = 15,95… Entscheidend ist aber, dass mit der Mantisse ein Wertebereich von +/- 2^53 abgedeckt wird (das ist etwa +/- 9 * 10^15). Wieso sind die durch meinen Test ermittelten Wertebereiche größer? Sie sind alle über +/- 3,5 * 10^16.

*** Edit ***

Nochmal eine Testschleife:

    for (long i = 4398046511103920L; i < 4398046511104120L; i++) {
        final String number = String.format(formatStringPos, i / factor, i % factor);
        if (i != Math.round(Double.valueOf(number) * factor)) {
            System.out.println(i + " vs. " + Math.round(Double.valueOf(number) * factor));
        } else {
            System.out.println(i);
        }
    }
}```
Das ergibt folgende Ausgabe:
[spoiler]

4398046511103920
4398046511103921
4398046511103922
4398046511103923
4398046511103924
4398046511103925
4398046511103926
4398046511103927
4398046511103928
4398046511103929
4398046511103930
4398046511103931
4398046511103932
4398046511103933
4398046511103934
4398046511103935
4398046511103936
4398046511103937
4398046511103938
4398046511103939
4398046511103940
4398046511103941
4398046511103942
4398046511103943
4398046511103944
4398046511103945
4398046511103946
4398046511103947
4398046511103948
4398046511103949
4398046511103950
4398046511103951
4398046511103952
4398046511103953
4398046511103954
4398046511103955
4398046511103956
4398046511103957
4398046511103958
4398046511103959
4398046511103960
4398046511103961
4398046511103962
4398046511103963
4398046511103964
4398046511103965
4398046511103966
4398046511103967
4398046511103968
4398046511103969
4398046511103970
4398046511103971
4398046511103972
4398046511103973
4398046511103974
4398046511103975
4398046511103976
4398046511103977
4398046511103978
4398046511103979
4398046511103980
4398046511103981
4398046511103982
4398046511103983
4398046511103984
4398046511103985
4398046511103986
4398046511103987
4398046511103988
4398046511103989
4398046511103990
4398046511103991
4398046511103992
4398046511103993
4398046511103994
4398046511103995
4398046511103996
4398046511103997
4398046511103998
4398046511103999
4398046511104000
4398046511104001
4398046511104002
4398046511104003
4398046511104004
4398046511104005
4398046511104006
4398046511104007
4398046511104008
4398046511104009
4398046511104010
4398046511104011
4398046511104012
4398046511104013
4398046511104014
4398046511104015
4398046511104016
4398046511104017
4398046511104018
4398046511104019
4398046511104020
4398046511104021 vs. 4398046511104022
4398046511104022 vs. 4398046511104023
4398046511104023 vs. 4398046511104024
4398046511104024 vs. 4398046511104025
4398046511104025 vs. 4398046511104026
4398046511104026 vs. 4398046511104027
4398046511104027 vs. 4398046511104028
4398046511104028 vs. 4398046511104029
4398046511104029 vs. 4398046511104030
4398046511104030 vs. 4398046511104031
4398046511104031
4398046511104032
4398046511104033
4398046511104034
4398046511104035
4398046511104036
4398046511104037
4398046511104038
4398046511104039
4398046511104040
4398046511104041
4398046511104042
4398046511104043
4398046511104044
4398046511104045
4398046511104046
4398046511104047
4398046511104048
4398046511104049
4398046511104050
4398046511104051
4398046511104052
4398046511104053
4398046511104054
4398046511104055
4398046511104056
4398046511104057
4398046511104058
4398046511104059
4398046511104060
4398046511104061
4398046511104062
4398046511104063 vs. 4398046511104064
4398046511104064 vs. 4398046511104065
4398046511104065 vs. 4398046511104066
4398046511104066 vs. 4398046511104067
4398046511104067 vs. 4398046511104068
4398046511104068 vs. 4398046511104069
4398046511104069 vs. 4398046511104070
4398046511104070 vs. 4398046511104071
4398046511104071 vs. 4398046511104072
4398046511104072 vs. 4398046511104073
4398046511104073
4398046511104074
4398046511104075
4398046511104076
4398046511104077
4398046511104078
4398046511104079
4398046511104080
4398046511104081
4398046511104082
4398046511104083
4398046511104084
4398046511104085
4398046511104086
4398046511104087
4398046511104088
4398046511104089
4398046511104090
4398046511104091
4398046511104092
4398046511104093
4398046511104094
4398046511104095
4398046511104096
4398046511104097
4398046511104098
4398046511104099
4398046511104100
4398046511104101
4398046511104102
4398046511104103
4398046511104104
4398046511104105 vs. 4398046511104106
4398046511104106 vs. 4398046511104107
4398046511104107 vs. 4398046511104108
4398046511104108 vs. 4398046511104109
4398046511104109 vs. 4398046511104110
4398046511104110 vs. 4398046511104111
4398046511104111 vs. 4398046511104112
4398046511104112 vs. 4398046511104113
4398046511104113 vs. 4398046511104114
4398046511104114 vs. 4398046511104115
4398046511104115
4398046511104116
4398046511104117
4398046511104118
4398046511104119

[/spoiler]
Dort sieht man auch die Grenze (4.398.046.511.104.020 als letzte gültige Zahl).

*** Edit ***

Vielleicht sollte ich mir nochmal den theoretischen Grenzbereich genauer ansehen. Vielleicht ist da nur ein einzelner Wert falsch, der bei der Bisektionssuche nicht gefunden wurde.

*** Edit ***

Ok, habe die Vorkommastelle mitgezählt. Der ermittelte Wertebereich ist kleiner: nämlich +/- 3,5 * 10^15 (nicht 16). Bei 0 Nachkommastellen kommt man dann an die theoretische Grenze heran.

[QUOTE=cmrudolph]Laut meinem Taschenrechner ist der Zehnerlogarithmus von 2^53 = 15,95…[/QUOTE]Was? Ja, äh ohh. Hab da wohl heute einen nächtlichen Ablesefehler übernommen.

[QUOTE=cmrudolph;84809]Entscheidend ist aber, dass mit der Mantisse ein Wertebereich von +/- 2^53 abgedeckt wird (das ist etwa +/- 9 * 10^15). Wieso sind die durch meinen Test ermittelten Wertebereiche größer? Sie sind alle über +/- 3,5 * 10^16.[/QUOTE]Also ich hab jetzt spaßeshalber mal was implementiert, von dem ich denke, dass es deinen Vorstellungen entspricht.


public class DoubleAndLong {
	public static final long MAX_VALUE_LONG = 8804986921812646L;
	public static final long MIN_VALUE_LONG = -22562155863737690L;
	public static final double MAX_VALUE_DOUBLE = 8804986921812.646;
	public static final double MIN_VALUE_DOUBLE = -22562155863737.690;
	public static final int PREC = 3;

	public static double toDouble(long value) {
		StringBuilder sb = new StringBuilder(String.valueOf(value));
		sb.insert(sb.length() - PREC, ".");
		double rc = Double.valueOf(sb.toString());
		// TODO: check borders;
		return rc;
	}

	public static long toLong(double value) {
		StringBuilder sb = new StringBuilder(String.format(Locale.ENGLISH, "%1.3f", value));
		int point = sb.indexOf(".");
		sb.deleteCharAt(point);
		point += PREC;
		while(sb.length() < point) {
			sb.append("0");
		}
		long rc = Long.valueOf(sb.toString(), 10);
		// TODO: check borders;
		return rc;
	}

	public static void main(String[] args) {
		long tmp;
		long upper = (Long.MAX_VALUE >> 1);
		long lower = 0;
		long current;
		double d;
		while ((upper - lower) > 1) {
			current = (upper + lower) / 2;
			d = toDouble(current);
			tmp = Long.valueOf(toLong(d));
			if(current != tmp) {
				upper = current;
			} else {
				lower = current;
			}
		}
		long MAX_VALUE = lower;
		upper = 0;
		lower = (Long.MIN_VALUE >> 1);
		while ((upper - lower) > 1) {
			current = (upper + lower) / 2;
			d = toDouble(current);
			tmp = Long.valueOf(toLong(d));
			if(current != tmp) {
				lower = current;
			} else {
				upper = current;
			}
		}
		long MIN_VALUE = upper;
	}
}```Und tatsächlich sind die Longwerte im positiven Bereich ca. + 10^15,94... und im negativen ca. - 10^16,35... hoch. Wenn mir nun noch jemand diese Asymetrie erklären kann, zumal die Mantisse immer gleich lang ist, wäre ich äusserst dankbar.

P.S.: Deine Werte sind auf jeden Fall zu hoch angelegt, weil du bei solch hohen Potenzen keine Genauigkeit auf 3 Nachkommastellen mehr hinbekommst, das siehst du bei einigen deiner Zahlen ja selber.

Edit: Was ich noch anbieten kann, wäre die Version, wo gerechnet wird, wobei ich auch ungefähr auf deine Werte komme. Die Frage ist dann nur, ob das dann auch immer passt. Die Diskrepanz zwischen +10^15,64... und -10^18,66... wird mir hier erst recht nicht klar.
```public class DoubleAndLong {
	public static final long MAX_VALUE_LONG = 4402484035469312L;
	public static final long MIN_VALUE_LONG = -4607131804711605760L;
	public static final double MAX_VALUE_DOUBLE = 4402484035469.312;
	public static final double MIN_VALUE_DOUBLE = -4607131804711605.760;
	public static final int PREC = 3;

	public static double toDouble(long value) {
		double rc = value / 1000.0;
		// TODO: check borders;
		return rc;
	}

	public static long toLong(double value) {
		long rc = (long) ((value + 0.0005) * 1000.0);
		// TODO: check borders;
		return rc;
	}

	public static void main(String[] args) {
		long tmp;
		long upper = (Long.MAX_VALUE >> 1);
		long lower = 0;
		long current;
		double d;
		while ((upper - lower) > 1) {
			current = (upper + lower) / 2;
			d = toDouble(current);
			tmp = Long.valueOf(toLong(d));
			if(current != tmp) {
				upper = current;
			} else {
				lower = current;
			}
		}
		long MAX_VALUE = lower;
		upper = 0;
		lower = (Long.MIN_VALUE >> 1);
		while ((upper - lower) > 1) {
			current = (upper + lower) / 2;
			d = toDouble(current);
			tmp = Long.valueOf(toLong(d));
			if(current != tmp) {
				lower = current;
			} else {
				upper = current;
			}
		}
		long MIN_VALUE = upper;
	}
}```
Edit2: Vergiss es... das 2. passt ganz und gar nicht, weil viel zu viele Longwerte die gleichen Doublewerte ergeben ( :( ). Das erste aber sollte recht sicher sein, denke ich.

double ist einfach der falsche Datentyp für den Job. Willst du Festkomma-Arithmetik, nimm den richtigen Typ, nämlich BigDecimal.

Wahrscheinlich hast du recht und das Rumgeeiere mit double bringt keinen Mehrwert. Es tauchen nur magische Zahlen auf und ein gewisser Unsicherheitsfaktor ist immer noch da. Was, wenn der User mal eine lange Zahl mit 6 Nachkommastellen eingibt? Dann wird der Wert nicht mehr korrekt übergeben.
BigDecimal ist aber wiederum so sperrig… Vielleicht nutze ich BigDecimal dann einfach als Transferformat und rechne intern wie gehabt mit long.