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