Currently Ledger stores values in float columns in the database. Float values have an inherently limited precision, which can become a problem when performing comparisons, rounding, or very precise arithmetic with them. The PHP documentation explains it like this:
Floating point numbers have limited precision. Although it depends on the system, PHP typically uses the IEEE 754 double precision format, which will give a maximum relative error due to rounding in the order of 1.11e-16. Non elementary arithmetic operations may give larger errors, and, of course, error propagation must be considered when several operations are compounded.
Additionally, rational numbers that are exactly representable as floating point numbers in base 10, like 0.1 or 0.7, do not have an exact representation as floating point numbers in base 2, which is used internally, no matter the size of the mantissa. Hence, they cannot be converted into their internal binary counterparts without a small loss of precision. This can lead to confusing results: for example, floor((0.1+0.7)*10) will usually return 7 instead of the expected 8, since the internal representation will be something like 7.9999999999999991118....
So never trust floating number results to the last digit, and do not compare floating point numbers directly for equality. If higher precision is necessary, the arbitrary precision math functions and gmp functions are available.
(from http://php.net/manual/en/language.types.float.php)
A serious bookkeeping program cannot be subject to these kinds of inconsistencies, so another approach is necessary.
GnuCash overcomes this problem by storing two integer values: a value and a denominator. For example, a value of 99.99 is stored like this:
value = 9999
denominator = 100
I suggest we move towards this kind of storage scheme with Ledger.
Comments
Comment #1
m.stentaThe Commerce module also uses integers for value storage, but they do not store a denominator value (yet?). They are currently discussing methods of price implementation for the upcoming 2.x branch here: #1125706: Price Implementation for Commerce 2.x
See my comment in that thread for a link to another related issue that arises from not using a denominator: http://drupal.org/node/1125706#comment-5717026
I'll be keeping an eye on their progress in that thread...
Comment #3
m.stentaGnuCash does some crazy complicated stuff to ensure proper rounding when working with numeric denominators. Check out the documentation for their gnc-numeric class: http://svn.gnucash.org/docs/HEAD/group__Numeric.html
I think it's good to plan for similar capabilities in Ledger, and I think we can do so in an iterative manner, starting with a denominator column in the {ledger_account_entry} table and support for it in the code that handles values.
Comment #4
m.stentaThis is done. Values are now stored in two database columns: value_num, and value_denom. Ledger account entries and ledger budgets perform calculations on load and save to convert back and forth, and the Views handlers perform the same on-the-fly calculations when querying the columns.
If you are currently running a dev version of Ledger, please see #1477546: [META] Ledger dev version release updates for instructions on how to update your database manually.
Comment #4.0
m.stentaAdded PHP quote citation link.