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
  //Delete existing ThousandSeperators first
  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);

  //Change to DecimalSeparator of current Windows-Version
  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 user profile iconKha: 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

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Besser als gar nichts.
Und schlechter als jede richtige Lösung. Und deshalb...
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
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

user profile iconalzaimar hat folgendes geschrieben Zum zitierten Posting springen:
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.

user profile iconalzaimar hat folgendes geschrieben Zum zitierten Posting springen:
Oder einfach XML nehmen.

Was genau hat der Container mit dem Format zu tun?


alzaimar - So 09.11.08 01:46

user profile iconMartok hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconalzaimar hat folgendes geschrieben Zum zitierten Posting springen:
Oder einfach XML nehmen.

Was genau hat der Container mit dem Format zu tun?

XML ist -wohl definiert- zickig und erlaubt nur ein Format in einer Reihenfolge mit einem Zeichensatz. Das gibt es keine Mißverständnisse. Nur Frust, bis es funktioniert. :zwinker: Aber dann is Ruhe im Karton.


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

user profile iconLannes hat folgendes geschrieben Zum zitierten Posting springen:
'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.