Announcement

Collapse
No announcement yet.

Rundungsphänomen bei Zuweisung via "AsFloat"

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • Rundungsphänomen bei Zuweisung via "AsFloat"

    Hallo zusammen!

    Ich moechte folgenden Quellcode zur Diskussion stellen.

    <PRE>
    procedure TForm1.btnStartClick(Sender: TObject);
    var
    e_org,
    e100,
    e1000: extended;
    begin
    with adoQryWerte do begin
    if Active then Close;
    Open;
    while not EOF do begin
    e_org := FieldByName('WERT').AsFloat;
    e100 := btKaufmRunden(FieldByName('WERT').AsFloat, 100);
    e1000 := btKaufmRunden(FieldByName('WERT').AsFloat, 1000);
    Memo1.Lines.Add(FloatToStr(e_org));
    Memo1.Lines.Add(FloatToStr(e100)+' - '+FloatToStr(e1000));
    Memo1.Lines.Add('- - - - - - - - - - - - - - - - - - - -');
    Edit;
    FieldByName('WERT').AsFloat := e_org;
    Post;
    Next;
    end;
    Close;
    end;
    end;
    </PRE>

    Die Abfrage adoQryWerte greift auf eine Tabelle zu, die in einer Access 2000 DB liegt. Diese Tabelle enthält nur ein Feld WERT; Typ: Zahl, Feldgroesse: double, Dezimalstellen: automatisch.

    Es werden zwei gerundete Werte berechnet: e100 auf zwei Stellen und e1000 auf drei Stellen nach dem Komma. Hier der Quellcode der Rundungsfkt.:

    <PRE>
    function btKaufmRunden(
    const x: extended;
    const Genauigkeit: integer): extended;
    begin

    Assert((Genauigkeit mod 10 = 0));

    if x < 0 then
    result := trunc((x * Genauigkeit) - 0.5) / Genauigkeit
    else
    result := trunc((x * Genauigkeit) + 0.5) / Genauigkeit;

    end;
    </PRE>

    Die Werte in der Tabelle lauten:
    <PRE>
    2,655
    2,6555
    2,65555
    </PRE>

    Hier die Ausgabe in Memo1 nach dem ersten Start-Click:

    <PRE>
    2,655
    2,66 - 2,655
    - - - - - - - - - - - - - - - - - - - - - -
    2,6555
    2,66 - 2,655 (FALSCH)
    - - - - - - - - - - - - - - - - - - - - - -
    2,65555
    2,66 - 2,656
    - - - - - - - - - - - - - - - - - - - - - -
    </PRE>

    Hier die Ausgabe nach dem zweiten Start-Click:

    <PRE>
    2,655
    2,65 (FALSCH) - 2,655
    - - - - - - - - - - - - - - - - - - - - - -
    2,6555
    2,66 - 2,655 (FALSCH)
    - - - - - - - - - - - - - - - - - - - - - -
    2,65555
    2,66 - 2,656
    - - - - - - - - - - - - - - - - - - - - - -
    </PRE>

    Der Clou an der ganzen Sache ist: wenn ich die Zeile zwischen dem edit und dem post auskommentiere, rechnet die Rundungsfunktion korrekt, und zwar unabhaengig davon, wie oft ich auf Start druecke:

    <PRE>
    2,655
    2,66 - 2,655
    - - - - - - - - - - - - - - - - - - - - - -
    2,6555
    2,66 - 2,656
    - - - - - - - - - - - - - - - - - - - - - -
    2,65555
    2,66 - 2,656
    - - - - - - - - - - - - - - - - - - - - - -
    </PRE>

    Das Beispiel laesst sich auch ueber ODBC nachvollziehen. Ich habe ebenfalls bei der Zuweisung anstatt AsFloat mal Value versucht - keine Wirkung.

    Obwohl e_org in keinem Zusammenhang mit e100 und e1000 steht, scheint die Zuweisung eine Auswirkung auf die Rundung zu haben. Ich habe auch Compileroptimierungsbemühungen unterdrückt, indem ich das Zurueckschreiben in die DB von einem Kriterium abhängig gemacht habe, das erst zur Laufzeit bekannt ist (über eine Checkbox).

    Laesst man in dem Beispiel e100 und e1000 komplett weg und macht alles ueber eine Variable entsteht derselbe Effekt.

    Kann jemand dieses Verhalten nachvollziehen ?<BR>
    Dieses vielleicht konstruiert anmutende Beispiel hat fuer mich durchaus einen realen Hintergrund und ist keinesfalls lustig.

    Meine Umgebung: Windows 2000 Prof, Access 2000, D5 Enterprise mit MDAC 2.6 Patch.

    Vielen Dank fuer Antworten oder sonstige Teilnahme an der Diskussion.

    Jens

  • #2
    Hallo zusammen!<br>

    Bisher habe ich noch keine Lösung für das Problem gefunden. Die Beteiligung scheint auch recht mager. Bin ich vielleicht in der falschen Rubrik ?<br>

    Jen

    Comment


    • #3
      Hallo Jens,

      für mich nicht nachvollziehbar. Das einzige ist, daß Paradox selbst amerikanisch rundet. Was passiert eigentlich in deiner Function. Welcher Wert kommt aus der Tabelle an?

      mfg Klaus-Pete

      Comment


      • #4
        Hallo Klaus-Peter!

        Ursrünglich wollte ich per Abfrage aus einer Tabelle Werte auslesen, diese runden und gerundet wieder zurückschreiben. Hier eine vereinfachte Version von btnStartClick:
        <pre>
        procedure TForm1.btnStartClick(Sender: TObject);
        var
        e_org: extended;
        begin
        with adoQryWerte do begin
        if Active then Close;
        Open;
        while not EOF do begin
        e_org := FieldByName('WERT').AsFloat;
        e_org := btKaufmRunden(e_org, 1000);
        Memo1.Lines.Add(FloatToStr(e_org));
        Edit;
        FieldByName('WERT').AsFloat := e_org; {****}
        Post;
        Next;
        end;
        Close;
        end;
        end
        </pre>
        Die Werte in der Tabelle sind:
        <pre>
        2,655
        2,6555
        2,65555
        </pre>
        So liest das Programm sie aus der Tabelle und übergibt sie auch korrekt an btKaufmRunden. Der Debugger zeigt dieselben Werte an, wie sie in der Tabellen stehen. btKaufmRunden erwartet neben dem zu rundenden Wert eine Genauigkeit, die die verlangte Anzahl von Nachkommastellen widerspiegelt: 10 für 1 Stelle nach dem Komma, 100 für 2, 1000 für 3 Stellen nach dem Komma genau usw. Gerundet wird tatsächlich über das Abschneiden unrelevanter Stellen mit trunc, was m.E. eigentlich unbedenklich sein sollte.<br>
        Es werden folgende Ergebnisse in Memo1 angezeigt und in die Datenbank zurückgeschrieben:
        <pre>
        2,655
        2,655 (falsch)
        2,656
        </pre>
        Kommentiere ich die Zeile mit den Sternchen komplett aus - schreibe ich also den Wert beim Schleifendurchlauf nicht zurück - werden folgende Werte angezeigt:
        <pre>
        2,655
        2,656 (richtig!)
        2,656
        </pre>
        Ich hoffe, mein Problem ist etwas verständlicher geworden...<br>
        Jen

        Comment


        • #5
          Hallo,

          &gt;Bin ich vielleicht in der falschen Rubrik ?

          in der Rubrig <i>Datenbankentwicklung</i> oder <i>ADO</i> ist die Antwort-Wahrscheinlichkeit auf diese Datenbank-bezogene Frage höher. Hier unter <i>Diverses</i> geht eine Frage aufgrund der Masse manchmal unter :-)

          Verbirgt sich hinter <i>adoQryWerte</i> eine TADOQuery-Komponente? Wenn ja, würde ich TADOQuery zuerst durch TADODataSet ersetzen. Für die Datenmenge sollten dann über den Feld-Editor persistente TField-Instanzen angelegt und im Objektinspektor konfiguriert werden. Aber auch dann besteht die Gefahr der unerwünschten Rundungen, da dies generell ein Problem vom ADO Express ist. Nur dann, wenn man direkt auf die nativen ADO-Objekte (Recordset-Objekt) zugreift, erhält man den "unverfälschten" Wert aus der Datenbank (da hier die TField-Instanzen nicht im Spiel sind). Vor einigen Tagen habe ich eine ähnliche Frage für den MS SQL Server 2000 mit einem Beispielprojekt beantwortet, mit dem sich diese Unterschiede jederzeit reproduzieren lassen. Dabei war folgendes beim SELECT (!) feststellbar: <br>
          a) TADOQuery: geht gar nicht <br>
          b) TADODataSet: geht, aber mit Rundungsfehlern <br>
          c) Recordset-Objekt: geht, liefert exaktes Ergebnis zurück<br>
          Der Fall c) sah damals so aus:
          <pre>
          uses ADOInt;

          procedure TForm1.Button2Click(Sender: TObject);
          var
          aCon : _Connection;
          aCommand : _Command;
          aRS : _Recordset;
          vRows : OleVariant;
          iCnt : Integer;
          begin
          aCon := CoConnection.Create;
          aCon.CursorLocation := adUseClient;
          aCon.Open(ADOConnection1.ConnectionString, '', '', adConnectUnspecified);
          try
          aRS := CoRecordSet.Create;
          aRS.CursorLocation := adUseClient;
          aCommand := CoCommand.Create;
          try
          with aCommand do begin
          CommandType := adCmdText;
          CommandText := 'select PK, Wert from TestTbl where Wert=?';
          Parameters.Append(CreateParameter('Wert', adDouble, adParamInput, 8, EmptyParam));
          Set_ActiveConnection(aCon);
          Parameters[0].Value := 3.987654321;
          aRS.Open(aCommand, EmptyParam, adOpenStatic, adLockOptimistic, adCmdUnspecified);
          ShowMessage(VarToStr(aRS.Collect[1]));
          aRS.Close;
          aRS := nil;
          end;
          finally
          aCommand := nil;
          end;
          finally
          aCon.Close;
          aCon := nil;
          end;
          end;
          </pre&gt

          Comment


          • #6
            Hallo!

            Ich habe mal die verschiedenen Lösungsansätze verfolgt: Zuerst habe ich die <b>TADOQuery</b> (übrigens richtig vermutet ) durch eine <b>TADODataSet</b> ersetzt. Es trat keine Änderung ein. Danach habe ich persistente Felder eingeführt und konfiguriert - ebenfalls ohne positiven Effekt.<br>
            Hier ist der Quellcode des letzten Versuches, den ich gerade unternahm:
            <pre>
            procedure TForm1.Button3Click(Sender: TObject);
            var
            aCon : _Connection;
            aCommand : _Command;
            aRS : _Recordset;
            vWert : olevariant;
            eFloat : extended;
            begin
            aCon := CoConnection.Create;
            aCon.CursorLocation := adUseClient;
            aCon.Open(ADOConnection1.ConnectionString, '', '', adConnectUnspecified);

            aRS := CoRecordSet.Create;
            aRS.CursorLocation := adUseClient;
            aCommand := CoCommand.Create;

            with aCommand do begin
            CommandType := adCmdText;
            CommandText := 'select * from Wert';
            Set_ActiveConnection(aCon);
            aRS.Open(aCommand, EmptyParam, adOpenStatic, adLockOptimistic, adCmdUnspecified);
            while not aRS.EOF do begin
            vWert := aRS.Fields[1].Get_Value;
            eFloat := btKaufmRunden(vWert, 1000);
            memo1.Lines.Add(FloatToStr(eFloat));
            aRS.Fields[1].Set_Value(eFloat); {*****}
            aRS.MoveNext;
            end;
            aRS.Close;
            aRS := nil;
            end;
            aCommand := nil;
            aCon.Close;
            aCon := nil;
            end;
            </pre>
            Dabei habe ich den gegebenen nativen ADO Code auf mein Problem übertragen. try finally Sequenzen habe ich mal weggelassen... Ich lese den Wert aus dem Feld (es ist das zweite Feld in der Datenmenge, daher Feldindex 1) direkt in eine OLEVariant Variable, benutze diese zum Aufruf meiner Rundungsfunktion (s.o.) und weise den gerundeten Wert einer extended Variablen zu. Schließlich gebe ich diese - wie gehabt - in dem Memo1 aus. Danach schreibe ich den gerundeten Wert zurück in die DB usf. (Gespeichert wird wohl implizit ?!) Die Ursprungswerte sind dieselben wie in den vorherigen Beispielen.<br>
            Hier die Ausgabe (und die Werte in der DB), wenn ich die Sternchenzeile ausführe:
            <pre>
            2,655
            2,655 (falsch)
            2,656
            </pre>
            Hier das Ergebnis mit auskommentierter Zeile:
            <pre>
            2,655
            2,656 (richtig)
            2,656
            </pre>
            Es hat sich kein Unterschied ergeben: Sobald ich versuche, den Wert in die DB zurückzuschreiben, arbeitet die Funktion nicht wie erwartet...<br>
            Es ist wie verhext... Ich habe das letzte Bsp. mal auf anderen Rechnern unserer Firma ausprobiert, um zu sehen, ob es an meiner Umgebung liegt - leider ohne Erfolg. Höchstwahrscheinlich liegt es nicht daran...<br>
            Kann es an einer "falschen" Access 2000 oder MDAC Version liegen ?<br>
            Meines Wissens ist bei uns MDAC 2.6 installiert.<br>
            Vielen Dank für jede Hilfe!<br>
            Jen

            Comment


            • #7
              Hallo,

              in meinem Beispiel habe ich auf den MS SQL Server 2000 zurückgegriffen - und bei einem SQL-Server ist die CursorLocation <b>clUseClient</b> fast immer richtig. Im Gegensatz dazu sollte man bei einer ACCESS-Datenbank auf die Kombination von <b>clUseServer</b> und <b>cmdTableDirect</b> zurückgreifen. In diesem Fall (clUseServer) ist die OLE DB Client Cursor Engine nicht im Spiel, so dass das Programm direkt mit den nativen Daten der Jet Engine hantiert. Hat die Umstellung auf clUseServer eine Auswirkung auf das Verhalten

              Comment


              • #8
                Du gehst davon aus, dass die Bildschirmausgabe <b>FloatToStr</b> Deine Werte nicht verfälscht. Was passiert aber bei 2,6549999999999. Die Anzahl der "9" ist jetzt zufällig gewählt, aber vielleicht solltest Du mal folgendes versuchen:<p>
                e_org := StrToFloat(FloatToSTR(FieldByName('WERT').AsFloat) );<p>
                Schöne Grüße, Mario Noac
                Schöne Grüße, Mario

                Comment


                • #9
                  Hallo,

                  welcher Feldtyp BCD oder Float wird für WERT benutzt?
                  Ändert sich das Verhalten wenn man zwischen diesen wechselt?
                  (bei nichtpersistenten Felder wird das über die Property EnableBCD
                  am Dataset eingestellt)

                  Grüße
                  Ralf Janse

                  Comment


                  • #10
                    Hallo!

                    @Andreas Kosch:<br>
                    Über ADO Express habe ich die vorgeschlagenen Parameter eingestellt, jedoch wieder ohne Erfolg.<br>
                    Nativ habe ich ebenfalls die <i>CursorLocation</i> von aCon und aRS auf <i>adUseServer</i> geändert. Da ich zur Laufzeit immer die Ausnahme "Die Argumente sind vom falschen Typ, liegen außerhalb des Gültigkeitsbereiches oder ..." bekomme, wenn ich an <I>aCommand.CommandType</i> die Konstante <i>adCmdTableDirect> zuweisen will, bleibt dieser auf <I>adCmdText</i>. Jedoch benutze ich <i>adCmdTableDirect</i> beim Öffnen von <i>aRS</i> als letzten Parameter:
                    <pre>
                    aRS.Open(aCommand, EmptyParam, adOpenStatic, adLockOptimistic, adCmdTableDirect);
                    </pre>
                    (Wenn der Eindruck entsteht, daß ich keine Ahnung vom direkten Zugriff auf ADO Objekte habe, so ist dies völlig richtig. Vielleicht sollte ich mir mal ein Buch kaufen...)
                    Ansonsten blieb die App gleich. Jedoch auch mit diesen Modifikationen erhielt ich keine Veränderung. Die Korrektheit der Rundung steht und fällt nach wie vor mit dem Auskommentieren der Zuweisungszeile.

                    @Mario Noack:<br>
                    Was soll ich sagen? So geht's! VIELEN DANK!<br>
                    Ich habe mir noch erlaubt, Deinen Vorschlag "konsequent" abzukürzen:
                    <pre>
                    e_org := StrToFloat(FieldByName('WERT').AsString);
                    </pre>
                    Obwohl ich nicht das Gefühl habe, das Übel an der Wurzel gepackt zu haben, erhalte ich somit eine erträgliche Umgehung des Problems!<br>

                    Eins steht fest: Fließkommaoperationen mit Computern sind nicht meine Sache

                    Nochmals Danke!<br>
                    Jen

                    Comment


                    • #11
                      Hallo!

                      @Ralf Jansen:<br>
                      Ja, das Verhalten ändert sich, sobald ich den Typ des persistenten Feldes von TFloatField auf TBCDField ändere. Zusätzlich lese ich den Wert über Value aus:
                      <pre>
                      e_org := FieldByName('WERT').Value;
                      </pre>
                      Die Ausgabe meines Programms:
                      <pre>
                      2,655
                      2,656
                      2,656
                      </pre>
                      Schön <br>
                      Nochmals der Hinweis: Die Änderung des Typs des persistenten Feldes allein hat bei mir <i>nicht</i> ausgereicht: Anstatt AsFloat mußte ich auch Value benutzen. Andersherum - also nur die Benutzung von Value ohne Persistenz - hatte ich ja schonmal getestet (s.o.) - ohne Erfolg.<br>
                      Aus der Sicht meiner Anwendung ist es jedoch praktischer, werde ich Marios Ansatz wähle, da ich bisher in der App keine persistenten Felder verwende. Das Nachpflegen ist - wie immer - aus Zeitmangel erstmal nicht drin.

                      Nochmal: Danke an alle!

                      Jen

                      Comment


                      • #12
                        Hallo Leute,

                        ab Delphi 6 gibt es auch eine gut funktionierende Funktion für
                        ein korrektes Runden in kaufmännische Anwendungen. Da ich noch
                        mit Delphi 5 arbeite habe ich folgende Routinen aus Delphi 6 in
                        eine eigene Unit übernommen.

                        <pre>
                        { IntPower: Raise base to an integral power. Fast. }
                        type
                        TRoundToRange = -37..37;

                        function IntPower(const Base: Extended; const Exponent: Integer): Extended register;
                        function SimpleRoundTo(const AValue: Double; const ADigit: TRoundToRange = -2): Double;

                        implementation

                        //cnsd - patch, new function from delphi6
                        function IntPower(const Base: Extended; const Exponent: Integer): Extended register;
                        asm
                        mov ecx, eax
                        cdq
                        fld1 { Result := 1 }
                        xor eax, edx
                        sub eax, edx { eax := Abs(Exponent) }
                        jz @@3
                        fld Base
                        jmp @@2
                        @@1: fmul ST, ST { X := Base * Base }
                        @@2: shr eax,1
                        jnc @@1
                        fmul ST(1),ST { Result := Result * X }
                        jnz @@1
                        fstp st { pop X from FPU stack }
                        cmp ecx, 0
                        jge @@3
                        fld1
                        fdivrp { Result := 1 / Result }
                        @@3:
                        fwait
                        end;

                        //cnsd - patch, new function from delphi6
                        function SimpleRoundTo(const AValue: Double; const ADigit: TRoundToRange = -2): Double;
                        var
                        LFactor: Double;
                        begin
                        LFactor := IntPower(10, ADigit);
                        Result := Trunc((AValue / LFactor) + 0.5) * LFactor;
                        end;

                        </pre>

                        Beispiel:

                        <pre>
                        with xFrameAuftragPosition, xdmAuftrag do
                        begin
                        Netto := tbAuftragNetto.value;
                        Fracht := tbAuftragFrachtkosten.Value;
                        Rabattprozent := tbAuftragRabatt_Prozent.Value;
                        SteuerProzent := tbAuftragSteuer_Prozent.Value;
                        ProvProzent := tbAuftragProvision_Prozent.Value;

                        // und jez wird jerechnet
                        if Rabattprozent > 0 then
                        begin
                        Rabattbetrag := (Netto / 100) * RabattProzent;
                        Rabattbetrag := Rabattbetrag * -1;
                        Rabattbetrag := SimpleRoundTo(Rabattbetrag);
                        end
                        else Rabattbetrag := 0;
                        </pre>

                        Gruß Matze
                        &#10

                        Comment

                        Working...
                        X