Support Info abonnieren
per RSS Feed hier : VO Support Newsfeed
Support zu Visual Objects
Tipp des Monats für September 03

Alles über FLOATs




Nachtrag hier


Druckversion anzeigen: Druckversion
Wussten Sie schon... ?
 
  ... dass Visual Objects doch richtig rechnet! Oder doch nicht?


 
Alles was Sie über FLOATs wissen sollten. (Wir empfehlen unbedingt, dass Sie diesen Artikel - mindestens einmal! - ganz lesen, da verschiedene Themenbereiche angesprochen werden.)
Das Thema FLOATs kehrt mit eiserner Hartnäckigkeit immer wieder in Newsgroups zurück und ist bestimmt seit den letzten 10 Jahren Visual Objects eines der Top 10 Supportthemen.
LOCAL f1 AS FLOAT
f1 := 643.5/2
Das Ergebnis:

Für manche vielleicht überraschend (Nicht wenige sagen an dieser Stelle, dass VO nicht richtig rechnet!). Der obige Quelltext enthält allerdings einen weit verbreiteten Fehler! Beim Rechnen mit Fließkommazahlen sollte JEDE beteiligte Zahl eine Fließkommazahl sein! Im obigen Quelltext wird aber durch eine Integerzahl dividiert. Ändern wir den Quelltext also ab:
LOCAL f1 AS FLOAT
f1 := 643.5/2.0
Und siehe da, das neue Ergebnis lautet:

Dass das angezeigte Ergebnis jetzt allseits akzeptiert wird, liegt auch nur daran, dass das Ergebnis in diesem Fall nur zwei Nachkommastellen hat. Und hier kommt nämlich die Standard-Einstellung von SetDecimal(2) zur Geltung (Weitere Details dazu, wie für neu erzeugte FLOATs (in Ausdrücken oder Funktionen) die Anzahl der Nachkommastellen bestimmt wird, finden Sie in der VO-Hilfe zur SetDecimal()-Funktion). Um es klar zu sagen: In beiden Fällen hat VO richtig gerechnet! Es gibt für beide Fälle nur unterschiedliche Vorgehensweisen bei der Handhabung der Ergebnisanzeige!
Ein weiteres beeindruckendes Beispiel ist folgendes:
LOCAL f1 AS FLOAT
f1 := 659.9/220
Das Ergebnis:

Hier bringt nicht einmal die Korrektur f1 := 659.9/220.0 Abhilfe (Es wird immer noch 3,00 ausgegeben!). Erst die Einfügung von SetDecimal(5) bringt Aufklärung.
LOCAL f1 AS FLOAT
SetDecimal(5)
f1 := 659.9/220.0
? f1
Das Ergebnis wird jetzt so angezeigt:

Nun ist auch klar, dass bei der Voreinstellung von SetDecimal(2) auf 3,00 (für die Anzeige!) gerundet wurde. Eine Sache sollte man sich bei FLOATs auf jeden Fall merken: FLOATs enthalten neben dem berechneten Zahlenwert auch Formatierungsangaben! Aus der Hilfe zu SetDecimal(): All variables containing a FLOAT type have internal picture information relating to digits and decimals (see FloatFormat()). Diese zusätzlich gespeicherten Formatierungsangaben unterscheiden FLOATs eben auch von REAL4 und REAL8. Mit dieser Besonderheit der Zahlenspeicherung stellen FLOATs eine VO-Eigenheit dar und gehören auch in die Gruppe der dynamischen Variablen (sind also "Garbage collectable items"). Weitergerechnet wird natürlich mit dem internen, tatsächlichen Zahlenwert und nicht mit dem angezeigten. Sie brauchen nur mal den als 3,00 angezeigten Zahlenwert mal 1000 zu nehmen:
LOCAL f1 AS FLOAT
f1 := 659.9/220
? f1
? f1*1000.0
Die Ergebnisausgabe sieht jetzt so aus:

Das folgende Beispiel zeigt, was FLOATs mit numerischen Feldern zu tun haben. Und eigentlich kommt daher wohl auch ihre Daseinsberechtigung!
FUNCTION Start
LOCAL f1 AS FLOAT
DBCREATE("Temp.dbf",{{"FLoat1","N",5,1},{"Float2","N",2,0}})
USE Temp
append blank
FIELDPUT(1,643.5)
FIELDPUT(2,2)
f3 := FIELDGET(1)/FIELDGET(2)
? f3
inkey(0)
Use
RETURN NIL
Diesmal stehen die beiden Zahlen aus dem ersten Beispiel in einer DBF. Man beachte, dass für das zweite Feld keine Nachkommastellen definiert sind, dieses also auch als INT behandelt werden könnte. Die folgende Ergebnisanzeige belehrt uns jedoch eines besseren:
Die Ergebnisausgabe sieht jetzt so aus:

Das bedeutet: ALLE numerischen Felder werden wie FLOATs behandelt. Solche FLOATs übernehmen die Formatierungsangaben (Vor- und Nachkommastellen) aus der DBF-Struktur. Achtung: Uns wurden sogar Fälle berichtet, in denen Aufsummierungen von Feldinhalten zu leichten Abweichungen in der Summe vom tatsächlichen Wert führten, wenn diese Feldinhalte zunächst an REAL4- oder REAL8-Variablen zugewiesen wurden! (Während die meisten Bücher zu VO praktische keine Hinweise zur Unterscheidung von FLOATs und REALs bringen, die über die Wertebereichsangaben aus dem Programmer's Guide hinausgehen - ein englischsprachiges Buch zu VO 2.5 schreibt sogar: Man wundert sich eigentlich, wer den Datentyp FLOAT braucht!-, schreibt allein das Buch von A. Binas-Holz zu VO 2.0, man solle REALs nur in Ausnahmefällen benutzen, nämlich wenn man mit C Daten austauscht, ansonsten seien in VO-Anwendungen immer FLOATs zu bevorzugen. Und schon in ihrem Buch zu VO 1.0 aus dem Jahr 1994 "Das offizielle Buch zu CA-Visual Objects" schreibt sie auf S. 242: Die Verwendung der Fließkommatypen REAL4 und REAL8 in reinen CA-Visual-Objects-Programmen ist nicht zu empfehlen. Verwenden Sie grundsätzlich den Typ FLOAT, wenn Sie mit Fließkommazahlen zu tun haben. REAL4 und REAL8 sind für die Zusammenarbeit mit C-DLLs gedacht, die diese Typen erwarten. Mehr gibt's dazu nicht zu sagen, außer: Schade, dass sich so viele Gerüchte so lange in den Newsgroups halten.)
Quintessenz: Wenn Sie mal dem angezeigten Ergebnis nicht trauen, dann sollten Sie als erstes das Anzeigeformat überprüfen! Das wird durch Funktionen wie SetDecimal(), SetFixed(), SetDigit() und FloatFormat() bzw. durch die DBF-Struktur festgelegt. In allen uns bekannten Fällen lag das Problem entweder an diesen Einstellungen oder daran, dass bei Berechnungen nicht alle Operanden vom Typ FLOAT waren (siehe das erste und das dritte Beispiel oben). (Nebenbemerkung: Bei einem Problem mit der Round()-Funktion gab es in der Tat mal einen Fehler in VO, der mit einem Problem der Runtime des C5-Compilers zusammenhing, in dem alle VO-Versionen bis einschließlich VO 2.6 compiliert wurden. Dieses Problem wurde jedoch mit einem Patch zu 2.5 behoben! Richtig: dieser VO-Patch fixt ein Problem des Runtime-Systems des C5-Compilers!)
 
Und noch ein vorletzter Tipp zum Thema FLOATs: Vergleichen Sie NIE FLOAT-Variablen! Zumindest nicht wie im folgenden Beispiel (aber geben Sie zu, so haben/hätten Sie's auch gemacht). Wie wir oben gesehen haben, ist eine als 3,00 angezeigte FLOAT-Variable nicht zwingend = 3,00.
LOCAL f1 AS FLOAT
f1 := 659.9/220
? f1 // zeigt 3,00 an!!!
IF f1 = 3,00 // schlägt fehl!
    ? "f1 = 3,0"
ENDIF
Das gleiche gilt natürlich für Differenzen (IF f1 - f2 = 0.0). Mathematisch gesehen sind zwei Fließkommazahlen immer nur genügend genau gleich. Nämlich dann, wenn

|f1-f2| <e

, wobei Sie "genügend genau" festlegen, indem Sie z.B. e = 0,000001 oder einen Toleranzwert Ihrer Wahl setzen. Das selbe sieht dann in VO-Syntax so aus:
LOCAL f1,f2 AS FLOAT
f1 := 659.9/220.0
f2 := 3.00
SetFloatDelta(0.001) // entspricht dem obigen Epsilon-Wert!
IF f1 = f2 //hier wird der mit SetFloatdelta() gesetzte Toleranzwert berücksichtigt!
    ? "f1 (genügend genau) = 3,0" // wird in diesem Fall angezeigt!
ENDIF
Die Ergebnisausgabe sieht jetzt so aus:

 
Und da wäre zum Schluss noch eine weitere Situation, in der VO "falsch" (also vermeintlich falsch!) rechnet:
Man nehme nur eine INT-Zahl und dividiere diese durch eine andere INT-Zahl (oder man kann auch für beide DWORDs verwenden) und schon ist der "Fehler" (meistens) produziert:
LOCAL i1,i2 AS DWORD
LOCAL f3 AS FLOAT
i1 := 22
i2 := 95
f3 := Integer(1000-(i1*i2/50))
Dass der Ausdruck i1*i2/50 nur zufällig - und bei diesen Belegungen überhaupt nicht - ganzzahlig durch 50 teilbar ist, ist der Ausgangspunkt für die Beschwerde. Was soll VO eigentlich mit dem Ergebnis einer solchen Division machen (das Ergebnis wird intern in einer versteckten LOCAL gespeichert! - wegen der Klammer um den Ausdruck), wenn sie nicht aufgeht? Soll VO jetzt raten, dass der Programmierer auf keinen Fall eine Fließkommazahl haben will, weil sich nun mal Druckkoordinaten nicht in Bruchteilen von Dots ausdrücken lassen oder Pixelwerte auf einem Device Context immer ganzzahlig sein müssen? Oder will er doch einen Fließkommawert als Ergebnis von i1*i2/50 zulassen? Lieber VO-Programmierer, Sie legen es selbst fest! Unter Application Properties/Compiler Settings (siehe Abb. unten) Und die Voreinstellung für die Division von Integerwerten ist, dass das Ergebnis wiederum ein Integerwert ist - keine Rundung, sondern die Nachkommastellen werden im gespeicherten Ergebnis einfach ignoriert! Die Hilfe sagt dazu:
Integer Divisions
If checked, permits the division of two integers to yield a floating point result. If not checked, the division of two integers is always an integer, and the remainder is discarded.

Sie können global für die Anwendung erzwingen, dass das Ergebnis immer ein Integerwert ist, indem Sie die Compilereinstellungen entsprechend eingestellt lassen (kein Häkchen setzen!). Im Gegensatz zu den weiter oben aufgeführten Beispielen handelt es sich hier NICHT um eine Frage der Ergebnisanzeige, sondern darum, wie wird das (Zwischen-)Ergebnis gespeichert! Sie erhalten so das Ergebnis 959 im obigen Beispiel (,da jetzt von 1000 der Wert 41 subtrahiert wird):

Oder Sie aktivieren diese Option (erlauben also, dass das Ergebnis eine Fließkomma sein darf) und verwenden im Bedarfsfall die Integer()-Funktion, um ausnahmsweise doch ein ganzzahliges Ergebnis zu erzwingen. Mit dem obigen Beispiel-Quelltext erhalten Sie jetzt das Ergebnis 958 (,da jetzt von 1000 der Wert 41,8 subtrahiert wird).

Nachtrag zu Fließkommazahlen und Rundungen


Dass das Thema Fließkommazahlen viele Entwickler beschäftigt, haben wir an den Rückmeldungen zu diesem Tipp des Monats gemerkt. Und einen besonders interessanten Hinweis bekamen wir von einem Leser:
LOCAL nWert AS FLOAT
nWert := 0.29*100.0 // ergibt 29.00, soweit ok
SELF:oDCText1:caption := NTrim(nWert) // zeigt 29,00 an, immer noch ok
SELF:oDCText2:caption:= Str( Integer( nwert ) ,8 , n ) // zeigt 28 an
// oder 28,00000 je nach Belegung von n. Hoppla! Wie das?

Wie kann das sein ?
Weitere Zahlen, bei denen dieser Effekt auftritt, 1,15 3,235
Bei anderen Zahlen, z.B. 0,28 , 0,39 oder 1,14 ist alles OK
Das Problem liegt auch hier wieder in der internen Darstellung von Floats (die intern -abgesehen von der Format-Angabe- genau wie REAL4 und REAL8 als binäre Fließkommazahl und einem 2-er-Exponenten dargestellt werden, sog. IEEE 754-Format, das praktisch jeder moderne Compiler verwendet). Das bedeutet eine Zahl 0,29, die intern wie angegeben konvertiert wird, ist evtl. nicht exakt 0,29. Die VO-Hilfe zur Integer-Funktion nimmt darauf Bezug und empfiehlt in solchen Szenarien, wie oben angesprochen, stattdessen die Funktion Round(0.29*100.0, 0) - wichtig: 2. Parameter = 0 - anzuwenden. Die obige Aussage (gemeint ist "..ist evtl. nicht exakt 0,29") wird dadurch unterstützt, dass Frac(0.29*100.0) den Wert 1.0 ausgibt, d.h. der Rest, der übrig bleibt, wenn man die Integer-Funktion einsetzt, sollte doch normalerweise eine Zahl sein, die echt zwischen 0 und 1 liegt und nicht 1.0 sein. M.a.W. 0.29*100.0 ist intern wohl 28.99999999999999 oder so ähnlich. Leider lässt sich dies mit keiner SetDecimal()-Einstellung oder sonstigen Einstellungen verifizieren. Jedoch kann man sich selbst mal auf folgender Internetseite die interne Darstellung (IEEE-Format) anzeigen lassen:From Decimal Floating-Point To 32-bit and 64-bit Hexadecimal Representations Along with Their Binary Equivalents. Gibt man dort im ersten Eingabefeld (decimal floating point) 0.29 ein und klickt dann auf "Rounded", dann sieht man unten 2.8999999e-1 als Ergebnis der Rückumwandlung des internen Werts.
Für die anderen genannten Zahlen trifft wohl Entsprechendes zu. Diese Fließkomma-Problematik zieht sich durch alle Programmiersprachen und schreit eigentlich nach einer BCD-Arithmetik (BCD=Binary Coded Decimal) für die meisten kaufmännischen Anwendungen. Mehr über berühmte Probleme mit Fließkommazahlen lesen Sie unter Decimal Arithmetic FAQ Part 1 – General Questions. Sehr lesenswert und für jeden, der kaufmännische Anwendungen schreibt, dringend zur Bewußtseinsschärfung empfohlen.
 
Diese Tipps stammen von Dieter Crispien
 
Schicken Sie uns Ihren eigenen Tipp des Monats per Email
Druckversion anzeigen: Druckversion

 

Letzte Änderung des Inhalts auf dieser Seite: 10.10.03