Discussion of this topic on the list has been going on for a good long
while and it doesn't appear that we are going to reach a true
consensus.

With that in mind, I submit this proposal for modifying gnucash to use
exact quantities to represent numeric values.  I understand that my
approach will not please everyone.  Flames will be ignored.  I am
interested in, and am looking for, any criticism that suggests or
demonstrates that the proposal below is incomplete, that it will
generate incorrect results, or that it is unimplementable for some
reason I have not foreseen.  I'm sure there are problems with this
proposal and I want to find them.

I have implemented a significant part of this proposal already as a
testbed for some of the ideas.  If no major objections are raised, I
will begin to add the features discussed below into gnucash in the
very-soon timeframe.

b.g.

--------------------------------------------------------------

Proposal for modifying gnucash to use exact quantities
v1.0, 21 July 2000

Bill Gribble <[EMAIL PROTECTED]>

Table of contents
-----------------

  1. Introduction 
  2. gnc_commodity : representing currencies and other commodities 
  3. gnc_numeric   : representing exact numbers 


1. Introduction
---------------

It has become clear that gnucash can no longer use double-precision
floating point values to represent monetary quantities.  We must
represent monetary quantities and other exact financial numbers in a
way that does not accumulate residual error and respects the financial
world's constraints on the smallest units of financial transactions.
For example, US banks do not allow transactions or account balances
that have a non-integer number of hundredths of dollars.

We need to represent monetary quantities exactly, and all arithmetic
operations involving monetary quantities must be carried out exactly.
Where these two goals conflict (for example, if the exact result of a
financial computation would result in a monetary quantity that has
fractional parts of the smallest possible transaction unit) we must
preserve information and exactness *and* obey the constraints placed
on the quantities.

The necessary changes to gnucash to implement this are quite
widespread, but fall into two main categories:

  - We must make tradable commodities such as currencies and
    securities first-class objects.  Currently, gnucash has no
    representation of any properties of a currency or commodity,
    instead using just the currency symbol or stock ticker symbol as a
    string.  Currency compatibility is checked by a string equality
    predicate.  We must be able to determine, at minimum, the smallest
    fractional amount of a currency unit that can be used in
    transactions (for example, 1/100 of a US dollar).

    Also, we currently have a namespace collision between currency
    symbols and stock ticker symbols.  For example, the ISO currency
    symbol FRF (French francs) is shared by the NYSE ticker symbol FRF
    (the France Growth Fund).  Since shares of the France Growth Fund
    are indistinguishable from French francs in the current version of
    gnucash, all I can say is we are lucky that no one using gnucash
    in France owns any France Growth Fund.  A quick check of a few
    common currencies shows that the Australian dollar (AUD) is also
    NYSE Automatic Data.  This problem *must* be resolved by a better
    representation for tradable commodities.

  - monetary amounts must be represented as exact quantities,
    denominated in the smallest transactional unit of the commodity
    that they are counting.  Other exact values, such as quoted
    prices, must also be represented exactly.  All arithmetic
    operations using exact quantities in gnucash must be changed to
    use exact arithmetic.  

For the most part, the data structures and APIs required to get these
two things done are simple and straightforward.  The bulk of the
problem in converting gnucash to use them is finding a sufficient
supply of elbow grease to get the changes implemented.


2. gnc_commodity : representing currencies and other commodities
----------------------------------------------------------------

The gnc_commodity data type represents information about a particular
tradable commodity.  Commodities include currencies, stock and mutual
fund shares, and other tradable items such as pork bellies, soy beans,
and gold bricks.

Requirements: 

  1. gnc_commodity knows about the smallest possible transactional
     unit for trading the commodity (for example, 1/100 of a US Dollar
     or 1/1000 of a mutual fund share).

  2. gnc_commodity knows the full name of the commodity
     ("International Business Machines", "US Dollars"), plus any
     mnemonic or nickname that is used for trading or quoting purposes
     ("IBM", "USD"), and the namespace in which the name is unique
     ("NYSE", "ISO-4217").

  3. gnc_commodity knows any (internationalized) currency symbol or
     other amount-display prefix character(s) to be used when
     displaying amounts denominated in the commodity.

Proposed API:

    Strings are copied if passed as arguments, newly allocated if
    returned.  gnc_commodity objects must be created with
    gnc_commodity_new and are passed as pointers.

  /* create a gnc_commodity object */ 
  gnc_commodity * gnc_commodity_new(char * fullname,
                                    char * mnemonic,
                                    char * namespace,
                                    char * currency_symbol,
                                    guint32 elements_per_unit);

  void gnc_commodity_delete(gnc_commodity * c);
  
  /* get information about the commodity. */
  char * gnc_commodity_get_fullname(gnc_commodity * c);
  char * gnc_commodity_get_mnemonic(gnc_commodity * c);
  char * gnc_commodity_get_namespace(gnc_commodity * c);     
  char * gnc_commodity_get_symbol(gnc_commodity * c);  
  guint32 gnc_commodity_get_elements(gnc_commodity * c);

  /* compare two gnc_commodity elements for equivalence */ 
  int gnc_commodity_equiv(gnc_commodity * a, gnc_commodity * b);

Discussion: 

This proposal does not explicitly handle the problems associated with
stock splits, currency revaluations, or smallest-currency-unit
denomination changes.  Those issues will be addressed with another
proposal, still not complete, which will make changes to gnucash's
handling of currency trading and other commodity-exchange
transactions.  At that time, the gnc_commodity structure will probably
be changed to some degree.  However, the problems are sufficiently
complicated that I think it's appropriate to punt for the moment.

The gnc_commodity structures necessary to represent the ISO currencies
and whatever other commodities the user's accounts represent should be
created at app startup time from either an installed database (for the
ISO currencies) or a user-specific datafile.

The proposed API is adequate for the current round of fixes, AFAICT,
shouldn't get in the way when we make improvements, and meets the
requirements stated above.


3. gnc_numeric : representing exact numbers 
-------------------------------------------

The gnc_numeric data type represents exact quantities such as monetary
values and prices.  Its purpose is to provide an arithmetic engine for
dealing with exact quantities which can be used to handle financial
computations in gnucash.  


Requirements: [modified from a posting to the list] 

  1. Maximum magnitude.  We should be able to represent very large
     monetary sums as exact integer quantities of
     smallest-currency-units.  A rule of thumb is that we should be
     able to represent national budgets to the SCU
     (smallest-currency-unit) with several orders of magnitude to
     spare, even in very small-valued currencies.

  2. Genericity.  We must be able to handle fixed-point operations
     including addition, subtraction, multiplication, and division for
     numbers in different units.  SCU values must be allowed to be
     non-decimal fractions of the currency (such as share prices in
     64ths of a dollar, or old-style English pence/shilling/pound 
     currency units).
 
     We must be flexible enough to allow a fixed-point "number of
     shares" (perhaps represented as an integer number of
     thousandths-of-a-share) to be multiplied by a fixed-point "price
     per share" (perhaps represented as an integer number of
     64ths-of-a-dollar) to get a fixed-point "total transaction value"
     (perhaps represented as an integer number of pennies).

  3. Rounding/truncation control.  When performing multiplication and
     division between fixed-point quantities, rounding/truncation may
     be performed if the desired denomination of the output differs
     from the input (as in the price * num = total example above).
     There must be a mechanism to control when and how
     rounding/truncation is done, if there are several choices.

  4. Intermediate results.  If an overall computation does not
     overflow the bounds of the representation, the computation must
     be performed correctly and without an error flag even if
     intermediate results might overflow.  Where overflow is possible
     in intermediate results, an extended-range format must be used
     temporarily to store intermediate results.
     
  5. Input/output conversion.  There must be an interface API which
     will allow for conversion to and from common C data types
     with a mechanism for signaling overflow/underflow and 
     loss-of-information in the conversion.
    
  6. Compatibility with existing codebase.  The implementation of 
     a new numeric format must be compatible with both the C and
     Scheme portions of gnucash.

  7. Abstraction.  Since we have made at least one fundamental mistake
     in specifying the original representation of monetary values in
     gnucash, we have to assume that we may make others.  We are going
     to have to do significant surgery on gnucash to remove instances
     of double math using built-in operators; by all means, let's
     replace *all* such math with a c-function-based API that can have
     its implementation repaired at a later time, even if some simple
     operations (such as addition) can be implemented directly as C
     operations on "long long" (if that happens to end up as the
     underlying representation).

  8. Fixed storage size.  Considering that the gnucash engine may at
     some time have a database backend, it's important to have a
     numeric format that has a fixed storage size and (optimally) can
     be directly operated on by SQL operators.


Proposed API:

    gnc_numeric values are stack-allocated and passed/returned by
    value wherever possible.  Error returns from the
    gnc_numeric_*_with_error functions are allocated by the caller and
    filled in by the function.  

    --------------------------------- 
    Creation: make a gnc_numeric from a specified 'number' of items in
    a specified 'denomination' (number of items per unit count).

  gnc_numeric gnc_numeric_create(gint64 num, gint64 denom);


    ---------------------------------
    Arithmetic API: arithmetic functions take two arguments in addition
    to their operands, the denomination of the result (the number of 
    smallest-units per unit value) and an argument specifying how to
    round or truncate on conversion between denominations.  

    Special values for the 'denom' argument cause the use of 
    calculated denominations: 

      GNC_DENOM_EXACT   : Choose a denominator for output that 
                          allows the result of the arithmetic operation
                          to be expressed exactly, but don't bother to
                          try to find an 'optimal' denomination.
      GNC_DENOM_REDUCE  : Use common-factor elimination to find the 
                          smallest possible denomination that allows
                          the result of the operation to be expressed 
                          exactly.
      GNC_DENOM_LCD     : Compute the least common multiple of the 
                          operand denominations and use that for the 
                          output denomination 
      GNC_DENOM_FIXED   : Use the denomination of the inputs as the 
                          denomination of the output; if the input 
                          denominations are different, signal an error. 

    Values for the 'how' argument describe how to convert values from
    one denomination to another if the conversion cannot be done exactly:

      GNC_RND_FLOOR     : use the next-most-negative integer to the 
                          correct value 
      GNC_RND_CEIL      : use the next-most-positive integer to the 
                          correct value 
      GNC_RND_TRUNC     : remove (truncate) any fractional part and 
                          use the integer part 
      GNC_RND_ROUND     : round to the nearest integer, rounding 
                          an exact fraction of one-half downward.
      GNC_RND_ROUND_HALF_UP : round to the nearest integer, rounding
                          an exact fraction of one-half upward.  
      GNC_RND_NEVER     : never discard information, and signal an
                          error if the result of an operation cannot 
                          be represented exactly in the specified 
                          denomination

    The gnc_numeric_*_with_error functions set their 'error' argument
    to the exact difference between the returned value (which is
    identically the same as would be returned by the non-'with_error'
    version of the function) and an exact result as would be computed
    with a GNC_DENOM_EXACT denomination.

  /* arithmetic operations */
  gnc_numeric gnc_numeric_add(gnc_numeric a, gnc_numeric b, 
                              gint64 denom, gnc_numeric_round_t how);
  gnc_numeric gnc_numeric_sub(gnc_numeric a, gnc_numeric b, 
                              gint64 denom, gnc_numeric_round_t how);
  gnc_numeric gnc_numeric_mul(gnc_numeric a, gnc_numeric b, 
                              gint64 denom, gnc_numeric_round_t how);
  gnc_numeric gnc_numeric_div(gnc_numeric a, gnc_numeric b, 
                              gint64 denom, gnc_numeric_round_t how);
  
  /* arithmetic functions with exact error returns */
  gnc_numeric gnc_numeric_add_with_error(gnc_numeric a, gnc_numeric b,
                                         gint64 denom, gnc_numeric_round_t how,
                                         gnc_numeric * error);
  gnc_numeric gnc_numeric_sub_with_error(gnc_numeric a, gnc_numeric b,
                                         gint64 denom, gnc_numeric_round_t how,
                                         gnc_numeric * error);
  gnc_numeric gnc_numeric_mul_with_error(gnc_numeric a, gnc_numeric b,
                                         gint64 denom, gnc_numeric_round_t how,
                                         gnc_numeric * error);
  gnc_numeric gnc_numeric_div_with_error(gnc_numeric a, gnc_numeric b,
                                         gint64 denom, gnc_numeric_round_t how,
                                         gnc_numeric * error);
      
    ---------------------------------
    Conversion API:  Functions to convert gnc_numeric values between
    one denomination and another, or to convert to/from C double 
    floating point values: 

  /* change the denominator of a gnc_numeric value */
  gnc_numeric gnc_numeric_convert(gnc_numeric in, gint64 denom, 
                                  gnc_numeric_round_t how);
  
  gnc_numeric gnc_numeric_convert_with_error(gnc_numeric in, gint64 denom, 
                                             gnc_numeric_round_t how,
                                             gnc_numeric * error);
  
  /* reduce by GCF elimination */
  gnc_numeric gnc_numeric_reduce(gnc_numeric in);
    
  /* convert to and from floating-point values */
  gnc_numeric double_to_gnc_numeric(double in, gint64 denom,  
                                    gnc_numeric_round_t how);
  double      gnc_numeric_to_double(gnc_numeric in);
  

    --------------------------------- 
    Error API: Errors in arguments and arithmetic operations are
    signaled by a special return value from the function, and can be
    tested by the function gnc_numeric_check(gnc_numeric in).
    gnc_numeric_check returns 0 if there is no error condition,
    otherwise :

  #define GNC_ERROR_OK            0
  #define GNC_ERROR_ARG          -1  /* argument has negative/0 denominator */
  #define GNC_ERROR_OVERFLOW     -2  /* operation would overflow */
  #define GNC_ERROR_DENOM_DIFF   -3  /* FIXED specified but denoms differ */
  
  int gnc_numeric_check(gnc_numeric a);


    --------------------------------- 
    Storage: gnc_numeric values are stored as a pair of gint64 
    (64-bit integers).

    struct _gnc_numeric {
      gint64 num;
      gint64 denom;
    }; 

   
Discussion: 

The gnc_numeric type as described above meets all the requirements of
an exact numeric representation for gnucash.  Requirement 4,
Intermediate Results, is an implementation requirement, and the
current testbed implementation of this proposal does not fully meet it
(it's possible to overflow in some circumstances).  However, the API
described above does not need to change to repair the implementation.


_______________________________________________
gnucash-devel mailing list
[EMAIL PROTECTED]
http://www.gnumatic.com/cgi-bin/mailman/listinfo/gnucash-devel

Reply via email to