9,05 Euro: Verdammter Bug in Ruby on Rails
Es ist zum Haare ausraufen. Update: Ursache gefunden!
Immer mehr Softwareprojekte aus dem Open Source/Linux-Bereich waren vor 3 bis 5 Jahren mal richtig gut und stabil und werden nun zunehmend kaputtvermurkst. Ruby on Rails samt immer schlechterer Dokumentation ist auch so ein Fall.
Ich habe mir vor vielen Jahren mal eine Rails Applikation in Ruby on Rails 2 geschrieben, was damals relativ einfach war und gut funktioniert hat. Gab ja auch ein prima Reference Manual dazu. Die Applikation lief stabil und zuverlässig.
Weil Rails 2 aber nicht mehr richtig unterstützt wird und ich die App weiter brauche, habe ich gerade einige Zeit reingesteckt, sie auf Rails 3 umzustellen. Grauselig. Sogar das Buch (Agile Web Development with Rails) ist in der neuren Auflage deutlich schlechter, sie haben viele Referenz-Angaben, Erklärungen und Auflistungen rausgeworfen und Neo-Zeitgeist-dumm durch ein paar Beispiele ersetzt, aus denen man nicht mehr viel erkennen kann. Man ist ständig am Googlen um in irgendwelchen Foren Berichte zu finden, wo andere schon auf dieselben Probleme gestoßen sind.
Nun hat mich gestern abend schon eine Änderung genervt, denn ich hatte in der Software an einer Stelle ein kleines Stückchen Ajax-Technik verwendet, damit man in einer langen und schwierig zu erstellenden Liste durch einen einfachen Klick etwas ändern kann, ohne die gesamte Liste neu erstellen lassen zu müssen. Ging unter Rails3 nicht mehr. Aber warum es nicht mehr geht, das war nicht offenkundig, nicht trivial herauszufinden und zumindest an keiner offiziellen Stelle erläutert, die ich gefunden hätte. Rails 2 verwendete die Prototype-Library, Rails 3 verwendet jquery. Und die beziehen sich eben nicht nur auf Javascript, sondern bringen auch Ruby-Bibliotheken mit. Lädt man gem ‘prototype-rails’, bekommt man zwar die Funktion wieder, sie funktioniert aber nicht. Man muss das erst auf jquery umstricken, was aber auch nirgens brauchbar dokumentiert ist. Naja, mit beständigem Suchen, Ausprobieren usw. kommt man ans Ziel, auch wenn das ja eigentlich kein Programmierstil ist.
Heute dachte ich, jetzt läuft’s endlich, da zwickt mich ein Fehler.
In der Applikation kommen Geldbeträge vor. Und da habe ich einen Plausibilitäts-Check eingebaut, der grob gesagt so etwas prüft wie ob a+b+c=d+e+f, also Summen vergleicht, bei denen dasselbe rauskommen muss. Und dieser Check meinte manchmal, dass es nicht stimmt, obwohl die Werte korrekt waren. Das Ding läuft auf einer SQLITE3-Datenbank, mit Spaltentyp decimal(12,2) NOT NULL, alles richtig. Ruby on Rails macht daraus den Ruby-Datentyp BigDecimal, der eigentlich fehlerfrei und arithmetisch genau rechnen sollte. Aber immer dann, wenn ich an dem Betrag 9,05 Euro vorbeikam, ging’s schief. Weil das Ding beim Rechnen immer Rundungsfehler aufwies und als 9.050000000000001 auftauchte, obwohl in der Datenbank nur 9.05 stand.
Beobachtung (in script/rails console, d.h. man arbeitet interaktiv in der Rails-Umgebung, die Funktion to_d wandelt nach BigDecimal)
> puts 9.0.to_d 9.0 > puts 9.01.to_d 9.01 > puts 9.05.to_d 9.050000000000001 > puts "9.05".to_d 9.05
So’n Mist. Da gehen die anscheinend in ActiveSupport irgendwo über den Datentyp Float, obwohl in der Datenbank exakte Dezimalzahlen stehen und in Rails ebenfalls exakte Dezimalzahlen verwendet werden sollten. Wandelt man von String nach Big Decimal, dann paßt’s nämlich. Auch ein SQL-Select zeigt 9.05 an. (kann ja auch nichts anderes bei decimal(12,2). Also kann das Ding die Datenbank nicht korrekt auslesen.
Hat mich gerade deftig Zeit gekostet. Da denkt man, man bleibt vollständig im Bereich des exakten Rechnens und Peifedeckel isses. Sowas kann gerade bei Geldwerten zu erheblichen Problemen führen.
Jetzt überleg ich gerade, wie ich da weitermache. Bug Report hab ich zwar losgeschickt, aber sowas dauert meist. Das ganze Programm umzustricken geht jetzt aber auch gerade nicht.
Update: Die Ursache des Kuddelmuddels ist gefunden:
Das Problem ist die Datenbank SQLite3. Die bietet zwar diverse SQL-Datentypen an, auch decimal(12,2), tut dabei aber nur so als ob. Was in vielen SQLite3-Manuals (und wenn ich mich recht erinnere, meinem SQLite3-Buch) nicht steht ist, dass SQLite3 intern nur 5 verschiedene Datentypen kennt, nämlich NULL (der SQL-Null-Wert), INTEGER (bis 8 Byte), REAL (8 Byte IEEE Float), TEXT und BLOB. Alle anderen Datentypen sind nur Masquerade und werden darauf abgebildet.
Und bei decimal ist es nicht einmal eindeutig, worauf es abgebildet wird. Dazu aus der Online-Beschreibung:
A column with NUMERIC affinity may contain values using all five storage classes. When text data is inserted into a NUMERIC column, the storage class of the text is converted to INTEGER or REAL (in order of preference) if such conversion is lossless and reversible. For conversions between TEXT and REAL storage classes, SQLite considers the conversion to be lossless and reversible if the first 15 significant decimal digits of the number are preserved. If the lossless conversion of TEXT to INTEGER or REAL is not possible then the value is stored using the TEXT storage class.
Das heißt, dass sie decimal mal als Text-Darstellung, mal als float ableben. Und sie betrachten etwas als „genau”, wenn die ersten 15 Stellen übereinstimmen. Der geneigte Leser wird erkennen, dass 9.050000000000001 erst in der 16. Stelle von 9.05 abweicht. Sie betrachten dabei keine rechnerische Genauigkeit, was man also salopp also float(a+b)=float(a)+float(b) gelten muss. Womit sie eigentlich falsch liegen, denn das mit den 15 Stellen Genauigkeit ist eigentlich ein Genauigkeitsbegriff, der auf float-Typen anzuwenden wäre, aber bei Decimal erwartet man und gilt eigentlich exakte Genauigkeit. Damit sollte man eigentlich auch keinen Datentyp decimal anbieten, wenn man ihn nicht abbilden kann. Und das dann auch nicht mit einem faulen Genauigkeitsbegriff gutreden.
Damit liegt eine Verkettung von zwei Schlampigkeiten vor:
- SQLite3 bietet einen Datentyp decimal formal an, implementiert ihn aber fehlerhaft als float und nimmt typwidrige Fehler hin, dokumentiert es außerdem nur hinterhältig versteckt (bzw. gar nicht zu der Zeit, als ich die Software geschrieben und das nachgelesen habe).
- Ruby on Rails bietet von der Datenbankimplementierung abstrahierte Datentypen, auch exakte an (der Datentyp wurde als RoR-Migration :decimal, :null => false, :precision => 12, :scale => 2 angelegt) und bildet diese auf Datenbanktypen ab, übergeht dabei aber die Tatsache, dass SQLite3 dabei Float und damit mit Fehlerintervall rechnet. Und wenn ich die Reaktion eines Developers dazu sehe, haben die das noch gar nicht gemerkt, dass da intern Float verwendet wird.
So ne Murks-Kette halt.
Nun kann ich einen dirty-Hack machen und nach jeder Einlese-Aktion die Werte auf zwei Stellen nach dem Komma runden, oder ich muss doch die Datenbank wechseln, was ich eigentlich gar nicht wollte und dem Zweck der Applikation etwas widerspricht.
54 Kommentare (RSS-Feed)
@Anmibe: Ja, aber sowas sollte ja erst gar nicht auftreten, weil es durchgehend exakte Datentypen seinsollten.
Naja, du könntest eine Schleife schreiben, die alle Geldwerte bis zu einer Million durchprobiert und bei geringer Anzahl von Fehlerfällen mit einzelnen ifs korrigieren 😀
Du könntest auf zwei Dezimalstellen runden 😀
Du könntest modulo ganze Cents rechnen. Du könntest dann sogar fragen, ob die Abweichung kleiner als ein tausendstel Cent ist und ansonsten eine Exception werfen 😀
Es gibt unendlich viele Möglichkeiten, das schlecht zusammenzuflicken.
Allerdings erinnere ich mich vage daran, dass to_d eigentlich als von flt zu big decimal definiert ist. Vermutlich wird daher bei deinem Beispiel 9.05 als flt aufgefasst. to_d(2), so meine vage Erinnerung richtig ist, sollte die Nachkommestellen abschneiden.
Bevor du schimpfst: Ja, ich weiß das to_d nicht dein Problem ist.
@Hanz: Ja, to_d wandelt von float, aber das sollte ja überhaupt nicht aufzreten. ich habe die float-Zahlen ja nir getestet umd der Ursache dieses Rundungsfehlers auf die Spur zu kommen.
Rails sollte den Datenbanktyp decimal(12,2) direkt und ohne umweg über float nach BigDecimal wandeln. Es dürfte dabei niemals zu einer Rundung oder Konvertierung ins Floatformat kommen. Das sollte durchgehend im exakten Bereich bleiben und war es unter Rails 2 auch.
Ich bin zwar kein Ruby-Hacker, aber ich würde mal sagen, das Float kommt daher, daß Deine 9.05 selbst ein Float ist, und das Artefakt des Floatwertes wird korrekt mit umgewandelt…
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/bigdecimal/rdoc/Float.html
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/bigdecimal/rdoc/BigDecimal.html
Da würde ich also sagen, Du solltest gleich BigDecimal.new(“9.05”) nehmen.
Das erklärt allerdings Dein Datenbankproblem nicht. Aber vielleicht hast Du da ja irgendwo sowas ähnliches verzapft 😉
@O.: Ja, natürlich ist 9.05 ein float, aber das habe ich ja nur getestet umd der Ursache für den Rechenfehler und die Nachkommastellen auf die Spur zu kommen. Float und solche Rechenfehler dürften da eigentlich überhaupt nicht vorkommen.
Ich erwarte ja gar nicht, dass 9.05 exakt als Float dargestellt wird, sondern dass eben genau diese Rundungsfehler, die ich da demonstriert habe, in einem Programm mit Datenbanktyp decimal(12,2) und eine Porgrammdatentyp BigDecimal nirgends vorkommen und das durchgehend exakt rechnet. Dann dürfte nämlich sowas wie 9.0500000000001 gar nicht entstehen können.
Wenn Du mit richtigem Geld rechnest solltest vielleicht besser Cobol nehmen.
@Carsten: Unter Rails 2 hat es fehlerfrei funktioniert, wie es sollte.
Jetzt mal im ernst, solche Zahlendarstellung- und Grenzprobleme gibt es überall. Nicht umsonst gibt es Datenformate wie Currency… Jedes Datenformat hat ein Ende — und einen Anfang.
Jaja, Cobol, Delphi, kaufmännische Rundungen — ich bin zwar kein Vertreter von “wer billig kauft, der kauft zweimal” — aber das OpenSourcegewusel ist belastend und was etwas taugt, das kostet halt. Ich wüßte nicht, wie man da effektiv und fehlerarm programmieren kann. Ich wollte damals Ruby on Rails probieren, als Du “Werbung” machtest. Hab mir die ganze Linuxinstallation damit zerschossen, war geheilt.
Zum Entwickeln, kommerzielles Produkt(na was wohl) und Windows. Und Geld speichert man in Currency.
Carsten
—
Terroristen schaffen Arbeitsplätze
@Carsten: decimal(12,2) und BigDecimal sind die Datentypen für Currency usw. , die sollten exakt und rundungsfrei darstellen und rechnen. tun sie auch, aber offenbar wandelt Rails beim Auslesen der Datenbank mal kurz nach Float und bringt dadurch die Fehler rein, die man dann auch nicht mehr los wird, eben weil BigDecimal exakt rechnet und den Fehler erhält.
Schnittstellen sind wie Verlängerungskabel:
Irgendwo wird ein Kontaktwiderstand heiss, oder eine Schutzerde fehlt.
Schnittstellen werden gern zu abenteuerlichen Gebilden verknüpft, und irgendwo wird die eine oder andere Nebenbedingung vergessen oder ein Fehlerzustand verhunzt. Hauptsache, am anderen Ende der Leitung leuchtet das Lämpchen.
Und genau wegen solcher Spielchen mag ich übermäßig dynamische Sprachen gar nicht. Weswegen passt du jetzt noch gleich deine Anwendung an? Weil sie eine alte Bibliothek benutzt. Das sollte kein Grund sein, eine Anwendung anfassen zu müssen!
Aber das beobachte ich ständig. Neulich sah ich eine nicht mal eine Dekade alte C++-Anwendung, die runderneuert werden musste, weil C++ sich so stark verändert hatte in der Zeit. Mit C wäre das nicht passiert. Ja, ich programmiere normalerweise um ein Problem zu lösen, und nicht um auf der Höhe der Zeit zu bleiben.
Und wo ist eigentlich die Praxis zu der Theorie, daß eine neue Funktion die alten Eigenschaften erbt, damit diese weiterhin zur Verfügung stehen und die Kompatibilität nicht gefährdet ist? Auch Macken und Bugs gilt es zu erhalten. Man kann nicht einfach eine Funktion ändern, man muß eine neue erstellen, damit die alten Macken bleiben. Ich mache das jedenfalls so, daß ich nicht komplett umstelle. Neues Objekt mit alten und neuen Funktionen, und dann langsam umstellen. Das vermeidet genau solche Fälle.
Übrigens, ist das ganze Bibliotheksgewusel nötig und sinnvoll? Ist es nicht besser, jedes Programm bringt seine Bibliotheken mit und installiert sie lokal in seinem Verzeichnis oder linkt sie ein? Ja, das kostet mehr Platz, spart aber Verwaltung, viele Fehler und Bibliotheksgewusel. Letztlich könnte man vielleicht auch Platz sparen. Wenn 250 Module zum Update angeboten werden, wie soll ich rausfinden, was nötig, was nützlich und was giftig ist? Oder doch Windows?
Carsten
—
Dieses Video ist in deinem Land nicht verfügbar
In der Tat, mit C und Cobol wäre das nicht passiert. Perl und Java sind schon Moving Target genug.
Als damals der Rails-Hype durch die Medien schwappte, sah ich verdächtig viele Apple-Screenshots. Das ließ mich umgehend davon Abstand nehmen.
Ich hab vor 2 Jahren auch mal versucht, was mit Ruby zum laufen zu bekommen.
Ging nicht, weg in den Müll damit.
* Miese Dokumentation
* Nicht funktionierend
=> Ich hab besseres zu tun als mich mit so nem Gestümper rumzuärgern.
@Hadmut: Ach sooo… nur aus Gründen der Analyse und zum Problemaufzeigen.
@Nullplan: Das ist schon ziemlich krass. Ich hatte mal letztens bei einem Kleinstprojekt etwas, da wurde eine Lib in zwei auf getrent im Laufe der letzten Jahre.
Da musste dann aber nur die Library im Makefile ändern und das wars.
Es war aich C.
Bist Du sicher, daß C++ selbst so stark verändert wirde, ind nicht “nur” allerlei Bibliotheken? Ist C++ von heute nicht in der Lage älteres C++ zu kompilieren?
Daß da neue (auch funktionale) Features dazu gekoemmen sein sollen, ok.
Aber wenn altenKonstrukte nicht mehr funktionieren, ziemlich krass.
Lamm ich mir irgendwie nicht ganz so orichtig vorstellen…
(“wozu hibt es denn Standardisierungskommitees” ;-))
Da es offenbar um Geldbeträge geht: mach doch einfach Cent draus und rechne mit Ganzzahlen.
Leute,
diese ganzen Hinweise, Tricks und Ratschläge, mit Cent zu rechnen oder zu runden, das ist doch alles nur Mist, Krampf und Workaround. Ich kann doch nicht ein ziemlich großes Programm umschreiben und die Datenbank ändern, nur um einen dämlichen Bug zu umgehen.
> und rechne mit Ganzzahlen.
Leute, Decimal und BigDecimal sind Ganzzahlen, bei denen das Komma automatisch verschoben wird. Die Vorschläge bringen also nicht viel außer dem Aufwand, das manuell zu machen, was ohnehin passieren sollte.
Außerdem löst es das Problem nicht, dass hier Datentypen nicht richtig eingelesen werden. Ihr habt irgendwie das Problem nicht richtig verstanden. Es geht doch nicht darum, zu jedem Mist und Murks irgendeinen Workaround zu kennen und zu machen. Wo sind wir denn?
Je nach Aufgabenstellung kann man auch anders rechnen… In einem Projekt laufen bei uns alle Beträge durchgehend in “nettocent” – die von Kunden beauftragten und später abzurechnenden Posten kosten sogar alle jeweils ganze Eurobeträge (weil ohnehin ausreichend grosse Zahlen, und aus politischen Gründen bei uns keine psychologischen Preise gewünscht sind), es gibt Rabattierungen nicht prozentual, sondern nur als (manuelle – anderes ist nicht nötig) separate Gutschrift-Zeilen. Alle Posten unterliegen der vollen Mehrwertsteuer; da nur gewerbliche Abnehmer in Frage kommen, sind die einzelnen Posten jeweils netto aufgeführt und eine Steuerberechnung erfolgt erst in der Summe – mit dem Satz, der bei Erstellung gilt.
In unserem Fall heisst das, alle Geld-Werte sind durchgehend “int” (Datenbank, Programmlogik) und werden nur bei Anzeige / XML-Erstellung (Rechnung) und bei Bedarf durch eine Mwst-Multiplikation und sprintf() gejagt. Da wir keine Nachkommastellen beim Mwst-Prozentsatz haben und alle Posten = 0 mod 100 sind (ganze Euros), ist sogar dort nur eine int-Rechnung fällig, sogar die letzte Division zur Darstellung als Kommazahl ist eine int-Operation. Das sähe natürlich anders aus, wenn der Mwst-Satz krumm wäre, für den Fall sind geeignete Rundungen (auf fünf Nachkommastellen des Euro-Betrags) drin, die Fehler im Bereich 10^-6 und kleiner wie in Deinem Beispiel wegbekommen.
Wenn man mit Float arbeitet (ja, auch dann, wenn man es eigentlich nicht will oder glaubt), kann man die Gleichheitsprüfung durch ein Kleiner-Gleich ersetzen – nicht so hübsch, eigentlich sollte es nicht nötig sein…. if (a==b) ==> if absolut(a-b) Posten 8,85
Die Gesamt-Nettosumme berrechnete sich aber anscheinend aus der Summe aller Brutto-Einzelposten mal Anzahl, die danach erst in netto umgerechnet und gerundet wurden, während die Mwst und Gesamtbetrag anhand der Summe der Postenzeilen (netto) errechnet wurden…. ganz grober Murks. Mir fiel das irgendwann 2009 auf, beim Nachsehen in alten Rechnung war das aber auch mindestens seit 2004 schon so.
Ich habe sicherheitshalber mal mein Finanzamt gefragt, ob die denn solche Rechnungen zum Ziehen der Umsatzsteuer anerkennen – “Joah, kein Problem, so ein paar Cent Abweichung gibt es ja häufiger” – aber auch mal meinen Kundenberater dort drauf angesprochen. Das Problem war wohl bekannt, konnte aber erst im Laufe des nächsten Jahres entsorgt werden. Mittlerweile stimmen die Rechnungen, da fühlt man sich einer möglichen Umsatzsteuerprüfung besser gewachsen.
@Debe:
> Wenn man mit Float arbeitet
Himmel, Arsch und Zwirn!
Es geht nicht darum, mit Float zu arbeiten, sondern darum zu zeigen, dass Rails Float verwendet, wo Float nicht hingehört.
Es geht drum, mit Festkomma-Integer-Typen zu rechnen. Decimal ist ein Integer-Typ. Kapiert das doch endlich!
Hossa, das Ding verschluckt Text, wenn man kleiner-als-Zeichen drin hat… Der letzte grosse Absatz oben waren mal mehrere, da fehlt etwas, also hier mal ohne HTML-Klammern:
Wenn man mit Float arbeitet (ja, auch dann, wenn man es eigentlich nicht will oder glaubt), kann man die Gleichheitsprüfung durch ein Kleiner-Gleich ersetzen – nicht so hübsch, eigentlich sollte es nicht nötig sein….
if (a==b) ersetzen durch
if absolut(a-b) kleiner-als 0.005
Bei so etwas kommt es auf die zusätzlichen CPU-Zyklen zum Glück meist nicht an.
Das Problem an Currency-Datentypen sehe ich darin, dass diese nicht durchgehend und sauber zwischen verschiedenen Umgebungen laufen – genau das, was Ruby jetzt anscheinend macht, befürchtete ich. Wenn etwa Java, Perl, PHP und Excel auf eine (My|Postgre)SQL-Datenbank zugreifen, kann es einfacher sein, alte und bewährte Datentypen wie “int” zu verwenden.
Noch etwas Anekdotisches zum Währungsproblem: Ein Grosshändler lieferte mir zuverlässig und schnell Ware, nur die Rechnungen waren komisch: Die Summe der Nettoposten (mit jeweils hinten eine Ziffer 5) war nicht durch 5 cent teilbar, auch war Netto-Summe plus Mwst-Betrag nicht gleich Bitte-zahlen-Summe, sondern gleich mal sechs oder sieben Cent daneben. Nicht nur auf einer Rechnung, sondern auf fast allen, vorzugsweise und deutlich daneben bei einer grossen Anzahl Posten. Ich habe das an einer Rechnung versucht, zu erklären. Heraus kam, dass die beste Erklärung war:
Einzelposten wurden intern als brutto-Beträge gespeichert. Ausgewiesen war dann etwa
15* Patchkabel a 0,59 c (errechnet aus Brutto, dann gerundet) macht Posten 8,85
Die Gesamt-Nettosumme berrechnete sich aber anscheinend aus der Summe aller Brutto-Einzelposten mal Anzahl, die danach erst in netto umgerechnet und gerundet wurden, während die Mwst und Gesamtbetrag anhand der Summe der Postenzeilen (netto) errechnet wurden…. ganz grober Murks. Mir fiel das irgendwann 2009 auf, beim Nachsehen in alten Rechnung war das aber auch mindestens seit 2004 schon so.
Ich habe sicherheitshalber mal mein Finanzamt gefragt, ob die denn solche Rechnungen zum Ziehen der Umsatzsteuer anerkennen – “Joah, kein Problem, so ein paar Cent Abweichung gibt es ja häufiger” – aber auch mal meinen Kundenberater dort drauf angesprochen. Das Problem war wohl bekannt, konnte aber erst im Laufe des nächsten Jahres entsorgt werden. Mittlerweile stimmen die Rechnungen, da fühlt man sich einer möglichen Umsatzsteuerprüfung besser gewachsen.
Ich habe es immer versucht Beträge in Integer(also in Cent, Rappen, Pfennig, …) abzubilden. Schon weil man gerade bei Übergängen in andere Systeme, in neue Versionen oder andere Sprachen nie vorher wissen kann, was schief geht. Leider habe ich das nicht bei allen Projekten geschafft. Rundungsprobleme gibt es dann nur noch bei Umrechnung in andere Währungen, die hat man aber bei Currency und BigDecimal genauso.
Ist C++ von heute nicht in der Lage älteres C++ zu kompilieren?
Existierende C–+Compiler sind nicht in der Lage, wenige Jahre älteren Code zu kompilieren, ja. Und alten bereits kompilierten Code linken kannst Du auch vergessen, weil auch das ABI (nebst Name-Mangeling) regelmäßig gändert wird. Also bitte auch alles mit dem Compiler vom gleichen Hersteller übersetzen. Aber Du mußt ja eh alles neukompilieren, wenn Du in einer Klasse intern etwas änderst, da C– keine Kapselung beherrscht.
Bei C hast Du all diese Probleme nicht. Da wird selbst bei der Einführung neuer Datentypen ein _Bool genommen und dann via #include auf bool gemappt.
(“wozu hibt es denn Standardisierungskommitees”)
Beim fraglichen Komitee sitzt eine Bude namens Microsoft mit drin und die will die nächste Version von Wischel Studio verkaufen. 😉
@ Hadmut
Ja, dein Problem ist mir schon klar. Hast du es mal spaßeshalber (hust) mit Postgres oder MySQL probiert?
Ich weiß nicht wie die Datenbankanbindung in Ruby aussieht, aber evtl. ist das ein Problem, dass nur in Verbindung mit SQLite auftritt.
@Hanz:
> Hast du es mal spaßeshalber (hust) mit Postgres oder MySQL probiert?
Nee, noch nicht, weil es um Daten geht, die auf Notebooks mit dabei sind und dateiweise kopiert werden, auf denen ich nicht überall einen Datenbankserver laufen lassen kann und will.
Aber das wäre ja auch keine systematische Lösung, sondern nur die Hoffnung, dass sie es da anders gemacht haben. Ich habe aber Zweifel, das sie im ActiveSupport-Quelltext ziemlich hemmungslos to_d aufrufen.
Außerdem will ich jetzt auch keinen Riesenaufstand machen, nur um deren Bugs zu workarounden. Ich hab den Bug reported. Die sollen den mal fixen.
@ Hadmut
Natürlich bekommst du hier keine Lösungen. Ich hab dir ja oben schon absichtlich ein paar ganz miese Flickschusterlösungen als Parodie hingeworfen.
Und darauf zu warten, dass das gefixt wird, ist ein Glücksspiel. Du hast keinen Wartungsvertrag oder ein sonstiges Motivationsinstrument und ohne recherchiert zu haben vermute ich, dass die Leute, die den Code betreuen um den es geht, das auch nur in ihrer Freizeit machen. Wenn da gerade keiner Zeit und Lust hat, passiert erst mal nichts.
Und nein, meine Frage nach dem Austausch der Datenbank zielte darauf ab den Fehler einzugrenzen, nicht eine Lösung zu finden. Wenn jemand SQLite verwendet anstatt eine richtige DB (oder MySQL) zu nehmen ist Letzteres in der Regel keine Option.
Und der Tipp int zu verwenden ist weniger dumm als du glaubst. Wenn alles fertig ist hilft das natürlich nicht mehr viel, aber mich hat die Erfahrung gelehrt, dass irgendwelcher Webkram eigentlich immer voll mit solchen Problemen ist. Und alle Datentypen die über int, float, bool und string hinausgehen sind ein Risiko, weil irgendwas kaputt ist, nicht vorgesehen oder fürchterlich umständlich. Außer bei PHP, da sind sogar elementarste Dinge komplett irre.
@Hanz: Der erste Heini vom Rails-Team hat den Bug-Report zugemacht, weil er ihn nicht kapiert hat. Er meint, dann dürfte ich eben keine Float verwenden, hat aber nicht geschnallt, dass nicht ich, sondern der sqlite3-Adapter und RoR Float verwenden.
Inzwischen hab ich auch noch etwas debuggt, und der Fehler beginnt in der ruby-sqlite3-Library. Für decimal(12,2) liefert die ein Float zurück. Und RoR bzw. activesupport, übernimmt das einfach und wandelt es per Methode to_d nach BigDecimal, wobei dann das Problem entsteht. Da zwischendrin ist der Wert mal kurzfristig ein Float und da kommt der Mist hinein.
Natürlich kann man Integer verwenden. Wenn man etwas neu schreibt. Aber eine bestehende Datenbank und Anwendung umzupfriemeln ist ein Wahnsinnsaufwand, und eigentlich nicht vertretbar, nur um einen Bug bzw. einen Designfehler zu umgehen. Dazu habe ich gerade auch keine Zeit, das würde ein größerer Akt. Ich kann da jetzt nicht Tage und Wochen an Entwicklungszeit reinstecken, zumal das unter RoR 2 ja fehlerfrei funktioniert hat.
Dann kommst Du aber auch vom hundersten ins tausendste. Man kann doch nicht den Bugs einer Library dadurch begegnen, dass man alle möglichen Tricks und Workarounds verwendet, nur um von einem Fehler in den nächsten zu laufen. Ein Programm aus lauter solchen Tricks zusammenzuflicken ist doch hundsmiserabler Programmierstil.
Und selbst, wenn man sich auf den Bockmist einlässt: Das heißt ja nicht, dass man es stillschweigend tun kann, man kann ja trotzdem drüber bloggen.
Mich stört eben, dass es so viele Opensource-Projekte gibt, die nicht besser, sondern immer schlechter werden. Thunderbird, Linux-Desktop, Ubuntu, usw. und jetzt eben auch das.
>> Wenn man mit Float arbeitet
>Himmel, Arsch und Zwirn!
Deswegen steht da auch
>>(ja, auch dann, wenn man es eigentlich nicht will oder glaubt)
Ich weiss schon den Unterschied *
Natürlich ist das absoluter Mist, da gehört kein Float dazwischen. Wenn das in einer dynamischen Library falsch ist, wird sich die defekte Version aber sicher noch eine Weile herumtreiben… oder man liefert die hoffentlich bald gefixte mit bzw. (Paketmanagement) fordert eine Minimalversion… das ist ja nochmal ein Spass für sich.
Ich denke gerade an einen Artikel, den ich über die Geschichte von Windows vor langer Zeit mal gelesen habe: Da wurde “bug-kompatibel” weiter entwickelt, damit die für 3.11 gebauten Programme, die sich auf undokumentierte (Fehl-)funktionen verlassen haben, weiter verwendet werden können. Mir hat das damals so einiges erklärt.
Ist es ein realistischer Plan, die Aufrufe der defekten Funktion nachträglich durch eine Rundung zu “heilen”? Immerhin sollte man die Aufrufe ja relativ schnell im Code finden können, wenn man die Ursache des Ärgers kennt, und dann eventuell kapseln?
P.S:
* und habe nach längerem Krampf auch das Papier, das mir das ausreichend erfolgreiche Überleben in einer Info-Fakultät attestiert, wenn schon nicht wirklich irgendwelche Fähigkeiten… deswegen bin ich überhaupt auf danisch.de und Forschungsmafia aufmerksam geworden: Ich habe recherchiert, wie ich am besten das Prüfungsamt zur Einhaltung der DPO bringe. Zum Glück hat ein gepfeffertes Schreiben mit etwas Andeutung von juristischer Kenntnis gereicht… wie so oft im Leben. Danke für die Wissensfülle in den Blogs!
@ Hadmut
LOL! Ich war mir sicher, dass sowas passieren muss und auch ausgerechnet dir!
Mit meiner Vermutung in Richtung der Datenbankanbindung lag ich dann ja auch goldrichtig.
Dass ints zu verwenden jetzt nichts mehr hilft hatte ich ja oben selbst schon geschrieben. Und ja, die Flickschusterei die man bei quasi allem betreiben muss, was mit “Web” zu tun hat ist hundsmiserabel. Ich habe mich auch schon mal ausgiebig darüber ausgekotzt, dass MySQL kein RDBMS sondern ein besserer Key-Value-Store ist, der angezündet gehört. Und ich beschwere mich auch nicht darüber, dass du mit dem Finger auf die Wunde zeigst, ganz im Gegenteil, ich lese das gerne.
Praktisch ist es halt oftmals so wie im Straßenverkehr auch. Man ist von Affen umgeben und muss irgendwie damit klar kommen, dass die keinen Abstand halten können, nicht blinken und auf der linken Spur bei 110 einschlafen, wenn die mittlere frei ist. Andere Straßen gibts meistens nicht und wenn man ihnen in die Reifen schießt verstehen sie auch nicht warum…
Wenn man also mit solchem Webschrott “arbeiten” muss bleibt einem nur Abstand zu halten und damit zu rechnen, dass selbst solche Dinge wie BigDecimal grundlos ausscheren. Ich halte es auch für fürchterlich, dass in dem Bereich so viel nur halbgar ist und jeder das als normal und akzeptabel ansieht. Man kommt aber halt manchmal um das Elend nicht herum…
Wenn du ein Programm ohne Wartungsaufwand willst, war Ruby und RoR aber auch sowieso keine gute Wahl. Dafür ändert sich zuviel zwischen den Versionen.
Zum spezifischen Bug: Das ruby-sqlite-gem gehört leider weder zu den bestfunktionierendesten noch den bestdokumentierten. Aber http://sqlite-ruby.rubyforge.org/sqlite3/faq.html#538670696 folgend könntest du schon prüfen, warum der Datentyp falsch umgewandelt wird, und ggf die Umwandlungstabelle anpassen. Denn genau die dürfte ja den Fehler verursachen (auch wenn ich bei sowas immer ActiveRecord zuerst im verdacht hätte).
@Hadmut:
“Es geht doch nicht darum, zu jedem Mist und Murks irgendeinen Workaround zu kennen und zu machen. Wo sind wir denn?”
In Deutschland!
…siehe große Bauvorhaben…
@Wolle:
“Ich habe es immer versucht Beträge in Integer(also in Cent, Rappen, Pfennig, …) abzubilden.”
Spätestens an der Tankstelle merkst Du dann, daß das beim Literpreis des Benzins nicht mehr passt.
@Hadmut:
“Weil Rails 2 aber nicht mehr richtig unterstützt wird und ich die App weiter brauche, habe ich gerade einige Zeit reingesteckt, sie auf Rails 3 umzustellen.”
Was heisst denn hier “nicht mehr richtig unterstützt wird”?
Hätte es das Rails 2 nicht noch getan?
Wieso war der Umstieg aus deiner Sicht überhaupt notwendig?
Oder wolltest Du nur mit der aktuellen Mode gehen?
Höhere Versionsnummer heisst ja auch in anderen Projekten nicht unbedingt, daß das erstrebenswerter ist.
@O.: Ist die letzte 9 an der Tanke nicht nur eine Simulation? Ich hab noch nie gesehen, dass auf meiner Tankrechnung 10tel Cent auftauchen, also völlig Banane!
@Wolle: Freilich tauchen auf Deiner Tankrechnung 10tel Cent auf – multipliziert nämlich mit der Menge Benzin, die Du getankt hast.
Für dich tut es mir leid, bei mir hebt es die Stimmung.
Dieses Gefasel über Uni-Tests, Agile und den Weg des Open-Source-Programmierers und dann bei einer einfachen Konvertierung verkackt, die man über einen Regressionstest sofort hätte sehen müssen. Ich bin dieses mitleidige Lächeln halt leid, wenn meine/unsere altertümlichen Testmethoden kritisiert werden. Im Unterschied machen wir unsere Tests und reden nicht nur drüber.
Leider kann man nicht in jedem Projekt “Eat your own Dogfood” installieren. Also die Gehaltsabrechnung über Ruby-On-Rails laufen lassen und wenn es nicht läuft gibt es halt kein Gehalt bis der Fehler behoben ist. Freut auch die Kollegen, wenn man ungetesteten Kram eincheckt.
Ruby ist eine schöne Sprache, weil sie Lisp sehr ähnlich ist, und wie Lisp zum funktionalen Programmieren einlädt, ohne es zu erzwingen. Aber Ruby hat halt eine aktive große Community die ziemlich im aktuellen Zeitgeist ist und entwickelt sich ständig weiter.
Ich persönlich bevorzuge Common Lisp: Ein Standard der weitestgehend stabil ist seit 1984, mit Bibliotheken die seit einem Jahrzehnt nur kleinere Bugfixes bekamen, und immernoch gut genug funktionieren, und verschiedenen Compilern die nach oben wie nach unten skalieren. Aber eben mit dem Nachteil, dass manche modernen Dinge (asynchrone I/O, Zugriff auf Dateien, Typisierung, Rechnen mit Systemzeiten) sich anfühlen wie eine rektale Amygdalektomie – weil hier Dinge standardisiert wurden, bevor sich andere Systeme schlussendlich durchgesetzt haben.
Würde ich sagen, Common Lisp sei besser als Ruby? Nein. Würde ich @Hadmut zu Common Lisp raten? Hm. Man braucht dafür viel Geduld und Frustrationstoleranz.
Zur Erweiterung des Geistes, und um zu sehen, dass viele der modernen Standards eben nicht Gottgegeben sind, und es teilweise sogar bessere Lösungen gab, die sich eben nicht durchgesetzt haben, und einen jetzt nerven wenn man in Lisp programmiert, aber oft sinnvoller sind als das Vorhandene … vielleicht.
Alles hat seinen Preis.
@Uxul: Geh mir bloß weg mit Lisp! Ein ziemlicher Schrott, gebaut für spezifische Probleme. Grausam zu lesen und allein die Klammerzählerei geht mir auf den Wecker.
Und Ruby für ähnlich zu Lisp zu halten: Wie kommst Du denn darauf? Ruby und Lisp sind sehr verschieden.
Es gibt noch ein weiteres Problem. Wenn man große Zahlen hat, können durch die Umwandlung nach Float signifikante Stellen verloren gehen. Der Fehler in Hadmuts Beispiel deutet auf ein 64Bit-Float (Double) hin. Dieser hat eine Genauigkeit von 15,95 Dezimalstellen. Hadmut verwendet 14. Runden ginge also noch, bei decimal(14,2), wäre es nicht mehr zu retten.
@Debe
Wie wäre es richtig? Netto Beträge addieren, dann MWSt., oder brutto Beträge addieren und dann MWSt.?
@Hadmut: Vielleicht ist dieses Angebot eine Lösung für dich? http://railslts.com/
Ohne Wertung des Geschäftsmodells, ich bin dort weder Kunde noch Nutzer von Rails.
Update: Ursache gefunden, siehe Update oben im Blog-Artikel.
Bevor du auf eine richtige Datenbank umschwenkst würde ich mal schauen ob nicht bspw. Firebird ein Ersatz ist. Ich glaube mich zu erinnern, dass das Ding Dezimaltypen unterstützt. (Ich weiß aber nicht, ob nicht auch mit Hirnschaden…)
Das ist zwar auch Arbeit das umzustricken, bliebe aber wenigstens eine embedded Lösung. Google sagt, dass es wohl RoR3 Adapter gibt.
zum Update:
erinnert mch an den alten Spruch: “Wenn Du estwas ordentlich gemacht haben willst, dann amch es selbst.”
Hat mir damals zu Turbo-Pascal-Zeiten oft geholfen, als zugekaufte Bibliotheken sich manchmal “komisch” verhalten haben.
Leider ist in der heutigen Zeit es nicht mehr ganz so einfach, alles selbst zu machen.
wieso nimmst du nciht blob oder ein int mit festem multiplikator/divisor
> wieso nimmst du nciht blob oder ein int mit festem multiplikator/divisor
Weil das Murks/Workaround/Geblubber und nicht der Gebrauch geeigneter Datentypen wäre, und weil die Sache nun über 5 Jahre lang genau so funktioniert hat, wie sie gebaut war. Und es ein Haufen arbeit wäre, das zu ändern.
Man kann doch nicht ständig nur in irgendwelchen Workarounds herumpfuschen, weil irgendwelche Libraries voller Fehler sind.
Als in-process-DB wäre HSQLDB (http://hsqldb.org) zu empfehlen. Kann man als Client-Server einsetzen – funktioniert aber auch prima in-process mit Dateien, die man hin- und herkopieren kann. Funktioniert auch “nur” im Java-Heap – für stupide JUnit-Tests ganz interessant. Kann auch decimal – sogar in “echt” 🙂
Wenn dieses “Steinchen-auf-Schienen” vernünftig ist, sollte es HSQLDB unterstützen.
Wie Du die Daten von A nach B bekommst? Hm… Damit vielleicht: http://kettle.pentaho.com/
Dezimalwerte als int darstellen? Mist. Einmal falsch gedacht und alle Kunden bekommen 100mal soviel vom Konto abgebucht, wie gewollt… Aua. Das einzige Mal, wo ich das wirklich gemacht habe, war zu MC68000-Zeiten, um einen Flug durch’s Apfelmännchen in Echtzeit zu berechnen und darzustellen – sah’ cool aus 🙂
@Robert: Na das fehlt mir jetzt gerade noch, eine Datenbank in Java zu verwenden, als ob ich noch mehr Probleme bräuchte. Noch Ruby und Java mischen. Nein, ich nehm da mal Postgres.
> Wie Du die Daten von A nach B bekommst?
Na, das ist doch bekannt. gem ‘yaml_db’ und dann dump und load.
Postgres – funzt auch gut.
Aber wenn ich die Probleme mit Ruby lese, lass’ ich davon besser die Finger und bleib’ in meiner JVM – die rechnet richtig 🙂
> Aber wenn ich die Probleme mit Ruby lese, lass’ ich davon besser die Finger und bleib’ in meiner JVM – die rechnet richtig
Hast Du ne Ahnung. Die JVM macht ne Menge Rechenfehler (Überläufe usw.), die Ruby mit sauberen Exceptions abfängt oder automatisch in längere Datentypen wandelt.
Na ja – in ein “int” passen 32 Bit, “long” 64, float 32, double 64, etc
Überläufe sind da (wie in C und allen C-“Varianten”) halt “normal”. Will man richtig große Zahlen, nimmt man BigInteger bzw BigDecimal. Die primitiven Datentypen sind halt primitiv (dafür schnell) – die anderen halt exakt (aber dafür langsam). Ob man den Überlauf von “byte” zu “long” mit Typkonvertierung braucht – ich weiss nicht.
Aber Überlauf und echte Rechenfehler sind zwei verschiedene Paar Schuhe. Da sollte man beim Coden schon drauf achten. Aber in einem Punkt geb’ ich Dir Recht – viele Leute können heutzutage nicht mal mehr einfachste Bit-Rechnung – oder wissen nicht, was “immutable” bedeutet – das ist schon traurig.
@hägar: Wenn die Rechnung an Gewerbliche geht, ist eine mögliche Variante, alle Einzelpreise mit glatten cent oder fixer Stellenzahl festzulegen. Die Nettosumme kann dann exakt errechnet werden (bzw in Summe auf ganze cent gerundet) , der Mwst-Betrag muss auf jeden Fall gerundet werden. Der Bruttowert ist dann eine Summe zweier sauberer Beträge…bzw auch gerundet, von 1,19*netto, die Rundung geht logischerweise in die selbe Richtung, unproblematisch. Ordentlicher geht kaum.
Wenn man allerdings psychologische Bruttopreise verwenden will, oder Einzelpreise brutto ausweisen für private Verbraucher, wirds schon bescheiden… das fängt damit an, dass nicht für jede ganze Zahl z (cents brutto) eine ganze Zahl n (cents netto) existiert mit z=runden(n*1.19). Man muss dann also mindestens auf zehntelcent…
Ich bin der Meinung, dass “Rechnungen” stimmen müssen, gerundete Zahlen sollen meiner Meinung nach nicht weiter verrechnet werden, um grössere Abweichungen zu vermeiden. Das wird eben schwer, wenn man in Zwischenschritten brutto rechnet, oder zu einer Bruttosumme den Nettopreis und Mwst. bestimmt, s.o.
Ich stimme Hadmut zu –
es ist ein Unding, dass etwas als “exakt” angepriesen wird, es aber dann nicht ist.
Und ich stimme auch darin zu, dass es ein Armutszeugnis ist, dass als Lösungen erwartet wird, mit Fehlern der Urzeit eben zu arbeiten, anstelle moderne korrekte Datentypen zu verwenden.
Und was die Geschwindigkeit angeht – das ist hier keine 3d-engine, da ist die Geschwindigkeit mal völlig wurscht.
> Ein ziemlicher Schrott, gebaut für spezifische Probleme.
Jo, in Adälographie ist es zum Beispiel ziemlich schlecht. Ansonsten gibt es eine Fülle von Bibliotheken, siehe zum Beispiel cliki.net und quicklisp.org.
> Und Ruby für ähnlich zu Lisp zu halten: Wie kommst Du denn darauf?
> Ruby und Lisp sind sehr verschieden.
Verschiedenheiten gibt es freilich. Zum Beispiel ist Ruby zwar weniger dynamisch als Lisp, aber dafür kann man Ruby nicht nativ compilieren. Und in Ruby ist das Schreiben von Anbindungen für C-Bibliotheken schwerer.
Aber ansonsten ist zum Beispiel das Objektsystem sehr ähnlich. Und viele Kleinigkeiten, die herauszusuchen und zu belegen ich mangels Zielsetzung jetzt zu faul bin, insbesondere, da ich bezweifle, dass du jemals ernsthaft versucht hast, mit Common Lisp etwas zu programmieren, und vermute, dass du zu den Leuten gehörst, die scheme, emacs lisp, common lisp, clojure und newLisp für dasselbe halten.
Mein Punkt war jedenfalls der: Wenn man etwas will, was abgehangen ist, muss man dafür den Preis zahlen, dass nicht jeder aktuelle Standard unterstützt wird. Drum finde ich eine allzu große “aktive Community” auch garnicht so gut wie es immer dargestellt wird.
Oft scheint es mir eher so zu sein, dass wenige Leute, die aber ein großes Interesse an der Funktionalität der Software haben, besser sind. Darum versuche ich, wenn ich produktiven Code schreibe, möglichst nur Dinge zu nutzen, die ich entweder notfalls selber maintainen kann, oder von einem großen Projekt am Leben erhalten werden. Ansonsten ist es oft besser, sich Dinge selber zu implementieren.
Vor einigen Jahren hatte ich an anderer Stelle das gleiche Problem beim Programmieren einer Buchhaltungssoftware, ab und zu waren Werte nicht gleich, die gleich sein sollten und auch so angezeigt wurden. Gemerkt wurde das erst bei Produktivdaten, unschön. Zu Lösen war mein Problem nur, durch Runden aller Beträge zuerst auf zwei Nachkommastellen und anschließendem Abschneiden des Wertes.