Entwickler-Ecke
Delphi Language (Object-Pascal) / CLX - Fehlertoleranteres: StrToFloat
Seven of Nine - Fr 07.11.08 20:25
Titel: Fehlertoleranteres: StrToFloat
Immer mal wieder...
bekomme ich von den Benutzern meiner Software einen Fehlerbericht mit dem ich häufig nichts anzufangen wusste.
Heute hatte ich das Glück herauszufinden woran einer meiner sporadisch auftretenden fehler begründet lag.
-> Die von meine Programm geschriebenen Ini.Dateien konnten NACH einem Upadate oder Änderung der Windows Einstellungen nicht mehr sauber interpretiert werden.
Kurze fehlersuche... Aha deswegn hatte ich den Fehler früher nie gefunden. Es liegt häufig an den geänderten Einstellungen für "DecimalSeparator" und "Thousandseparator"
Ich weiß es gbt heirzu schon einige Threads. So richtig glücklich wurde ich aber mit keinem.
Ich suchte nach einem einem mgl. kurzem einfache Code der diese Fehler nur "eliminiert".
Ziel: Das "Einlesen" per StrToFloat fehertoleranter zu machen
Anbei mein Code dazu (Funktion)
evtl. kann's ja mal jemand gebrauchen. Einfach anstelle der StandradFnktion einbinden/verwenden
lG Martin
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
| Function MyStrToFloat(AString:String):Real; Var I,AnInt,ErrCode,ACount,Pos : Integer; begin ACount := 0; Pos := Length(AString); repeat If (AString[Pos] = '.') or (AString[Pos] = ',') then begin Inc(ACount); If (ACount > 1) then delete(Astring,Pos,1); end; Dec(Pos); until (Pos = 0);
For I:= 1 to length(AString) do begin Val(AString[I],AnInt,ErrCode); If (ErrCode > 0) then AString[I] :=DecimalSeparator; end;
Result := StrToFloat(AString); end; |
Moderiert von
Kha: Delphi-Tags hinzugefügt
Hidden - Fr 07.11.08 20:54
Hi,
Schreib' dir doch einfach eine eigene Funktion, in der du vor einem StrToFloat ein StrReplace einfügst ;)
Delphi-Quelltext
1: 2: 3: 4:
| function MyStrToFloat(const S: String): Extended; begin result := StrToFloat(StrReplace(StrReplace(S, ',', DecimalSeperator, []), '.', DecimalSeperator, [])); end; |
Schöner wäre natürlich ein const Array[Boolean] of Char = (',', '.'). Dann könntest du ein StrReplace mit Seperators[DecimalSeperator = ','], DecimalSeperator machen. Bin mir aber spontan nicht sicher, ob decimalSeperator theoretisch auch andereWerte als , und . enthalten kann.
Wenn du das ausschließen kannst:
Delphi-Quelltext
1: 2: 3: 4: 5: 6:
| function MyStrToFloat(const S: String): Extended; const Seperators: Array[Boolean] of char = (',', '.'); begin result := StrToFloat(StrReplace(S, Seperators[DecimalSeperator = ','], DecimalSeperator)); end; |
Ob jetzt der erste oder der zweite Wert im Array[Boolean] false oder true war, weiß ich nichtmehr. Musst du dann anpassen :)
mfG,
Seven of Nine - Sa 08.11.08 06:11
Danke für den Hinweis Daniel,
ich finde aber trotzdem das meine Funktion genau so Sinn macht. Gründe:
a) Meines Wissens kann der DecimalSeparator auch andere Werte als "." und "," haben. Genau deswegen bin ich den Weg gegangen ale zeichen des übergebenen Strings die "Nicht Zahlen" sind (per Überprüfung mit val Funktion) durch den gerade gültigen DecimalSeparator zu ersetzen. DAuert evtl eine ms länger in der Codeausführung, sollte aber dafür immer stimmen
b) TausenderSeparatoren können (wenn auch selten) vorkommen. Dies wird durch den ersten teil der Funktion ausgeschlossen.
Der Code ist zwar etwas unübersichtlich, löscht einfach, von rechten Teil des übergebenen Strings ausgehend, alle evtl vorhandenen TausenderSeparatoren. "Quick and drity" aber effizient ;)
lG Martin
alzaimar - Sa 08.11.08 09:22
So einfach ist das nicht. Z.b. sind bei uns im Vergleich zur USA Dezimalpunkt und Tausendtrenner vertauscht. Wenn man also formatierte Zahlen bekommt, weiss man nicht unbedingt, was sie bedeuten.
Aber eine kleine Heuristik hilft: Tausendertrennzeichen unterteilen eine Ziffernfolge in Dreiergruppen ('1.234.567,890').
Sei S eine Zahl, die mit unbekannten Trennzeichen formatiert wurde ("1,234,56" ist also nicht erlaubt) und L die Länge.
Sei ferner P die Position des ersten Sonderzeichens (keine Ziffer) und C dieses Zeichen.
Nun schaue ich mir das Zeichen vier Stellen weiter rechts an:
1. Es ist das gleiche Zeichen: C ist der Tausendtrenner. Wiederhole die Hewuristik mit P = P+4.
2. Es ist eine Ziffer. C ist der Dezimalpunkt. Der Tausendtrenner ist unbestimmt.
3. Es ist ein anderes Zeichen: C ist der Tausendtrenner und das andere Zeichen der Dezimalpunkt.
3. Es gibt dort kein Zeichen (P+4>L). Dann ist C ein Dezimalpunkt, wenn L<5. Der Tausendtrenner ist unbestimmt.
3.1 Wenn L = 5 (z.B. '1.234') wissen wir nicht, was C ist.
Wenn wir nun eine Menge von Zahlenstrings in einem unbestimmten Format haben, müssen wir nur einen String der Länge > 5 nehmen, und ihn durch die o.g. Heuristik jagen: Voila, wir wissen nun genau, was der Dezimalpunkt, und was der Tausendtrenner ist. Vorausgesetzt, diese Zeichen kommen in den Zahlenstrings vor.
Wir legen uns jedoch die Karten, wenn wir nur Zahlen der Form '1.234' haben. Hier ist das numerische Äquivalent einfach nicht zu bestimmen.
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57:
| Function MyStrToFloat(NumString: string): Double; Var i : Integer; cThousandSep, cDecimalSep: Char
procedure _Analyze; var C, C1: Char; L, P: Integer;
function _NextNonDigit(iStart: Integer): Integer; begin Result := iStart+1; while (Result <= L) and (NumString[Result] in ['0'..'9']) do Inc(Result); end;
begin cThousandSep := '?'; cDecimalSep := '?'; L := Length(NumString); P := 0; P := _NextNonDigit(P); if P > L then Exit; C := NumString[P];
Inc(P, 4); if P > L then begin if L < 5 then cDecimalSep := C; Exit; end; repeat C1 := NumString[P]; if C1 = C then begin cThousandSep := C; P := _NextNonDigit(P); end else if C1 in ['0'..'9'] then begin cDecimalSep := C; Break; end else begin cDecimalSep := C1; cThousandSep := C; Break; end; until P > L; end;
begin _Analyze; If (cDecimalSep='?') and (cThousandSep='?') Then Raise Exception.Create('Zahl kann nicht eindeutig konvertiert werden'); NumString := StringReplace(NumString,cThousandSep,'',[rfReplaceAll]); NumString := StringReplace(NumString,cDecimalSep,DecimalSeparator,[rfReplaceAll]); Result := StrToFloatDef(NumString,-1); end; |
delfiphan - Sa 08.11.08 11:01
Wenn du an einer sauberen Lösung interessiert bist, speichere die Daten entweder in einem klar definierten Format ab oder speichere das Format (Trennzeichen) der Zahlen mit.
Die automatische Erkennung oder die tolerante Konvertierung halte ich für die falsche Lösung.
alzaimar - Sa 08.11.08 17:10
Klar, das ist die beste Lösung, Aber was tun, wenn es ein Fremdformat ist oder die Daten aus Unbekanntistan kommen?
delfiphan - Sa 08.11.08 21:11
In diesem Fall geht es ja um seine eigene Software und nicht um Daten aus Unbekanntistan. Aber klar, wenn die Software schon ausgeliefert wurde, tja dann geht's wohl nicht anders ;)
Die Autoerkennung funktioniert wie du's selbst schon gesagt hast nicht in allen Fällen. Aber immerhin kann man das Format entweder rausfinden oder sagen, dass das Format unbestimmbar ist. Besser als gar nichts. Für eine Übergangslösung, einverstanden. Übrigens sehe ich gerade bei den Einstellungen z.B. für "Hindi (India)" folgendes Format: 12,34,56,789.00 oder bei "Inuktitut (Syllabics, Canada)": 123456,789.00
Aber wie ich schon sagte würde ich die Ini-Datei für die Zukunft anpassen.
alzaimar - Sa 08.11.08 23:22
delfiphan hat folgendes geschrieben : |
| Besser als gar nichts. |
Und schlechter als jede richtige Lösung. Und deshalb...
delfiphan hat folgendes geschrieben : |
| Aber wie ich schon sagte würde ich die Ini-Datei für die Zukunft anpassen. |
Indem ganz auf Tausendertrennzeichen verzichtet wird und ein fixer Dezimaltrenner verwendet wird. Oder einfach XML nehmen.
Übrigens klappt meine Heuristik auch bei den Inuit. Nur Inder haben's schwer.
Martok - So 09.11.08 01:14
alzaimar hat folgendes geschrieben : |
| Indem ganz auf Tausendertrennzeichen verzichtet wird und ein fixer Dezimaltrenner verwendet wird. |
Genau. Mach ich immer... Am besten übrigens amerikanisch mit Punkt, dann kann man das Komma noch für Aufzählungen nehmen.
alzaimar hat folgendes geschrieben : |
| Oder einfach XML nehmen. |
Was genau hat der Container mit dem Format zu tun?
Aya - So 09.11.08 03:44
Hi,
ich habe kein Delphi zur hand um das als code zu testen (und auch schon sehr lang kein Delphi mehr programmiert), deswegen kann ich nicht garantieren das der code unten fehlerfrei funktioniert:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
| function strToFloat42(Text: String) var pre, post: String; foundSep: Boolean; i: Integer; begin foundSep:=false; for i:=Length(Text) downto 1 do begin if Text[i] in ['0'..'9'] then begin if foundSep then pre:=Text[i] + pre else post:=Text[i] + post; end else foundSep:=true; end; result:=pre + '.' + post; end; |
Was ich hier tue ist einfach den String von hinten nach vorn durchzugehen. Das erste zeichen das keine zahl ist nehme ich als DezimalSeparator an und ignoriere alle weiteren nicht-Zahlen.
Beispiele:
123,45 würde zu 123.45
1.2.3,45 würde zu 123.45
12#34 würde zu 12.34
123bla45 würde zu 123.45
Aya
alzaimar - So 09.11.08 11:20
Was ist mit 123.456.789? oder mit '1.234'?
Lannes - So 09.11.08 11:29
Hallo,
'1.234' kann ohne weitere Informationen nie eindeutig interpretiert werden.
Der Punkt kann ein DecimalSeparator oder ThousandSeparator sein.
alzaimar - So 09.11.08 11:48
Lannes hat folgendes geschrieben : |
| '1.234' kann ohne weitere Informationen nie eindeutig interpretiert werden. |
Richtig, aber o.g. Code interpretiert ihn u.U. falsch. Mein Code hingegen trifft implizit genau diese Ausage ('Nicht eindeutig'), das die Separatoren nicht bestimmt werden.
Entwickler-Ecke.de based on phpBB
Copyright 2002 - 2011 by Tino Teuber, Copyright 2011 - 2026 by Christian Stelzmann Alle Rechte vorbehalten.
Alle Beiträge stammen von dritten Personen und dürfen geltendes Recht nicht verletzen.
Entwickler-Ecke und die zugehörigen Webseiten distanzieren sich ausdrücklich von Fremdinhalten jeglicher Art!