Introduction
In my last article[^], I eluded to the fact that there was another bug hiding in my code. Well, here it is! I guess it isn't precisely a bug, except under certain conditions, such as working with currency, in which precision is required.
Math Precision
One of the problems in the financial world is dealing with numeric precision. The double
data type doesn't quite cut it, as we shall see. This presents a problem in C++ which has no decimal
data type as found in C#. For example, the following C# code results in a "true" evaluation of n==201
:
decimal n=.3M;
n-=.099M;
n*=1000M;
if (n==201) ...
whereas using the double
type in C++ does not:
double n=.3;
n-=.099;
n*=1000;
if (n==201) ...
This is an important issue when dealing with financial math.
The Decimal Class
To solve this problem, I created a Decimal
class. Now, I looked high and low on Code Project and Google searches for something like this, and I didn't find anything, so if I missed a contribution by another person regarding this issue, then I apologize in advance.
class Decimal
{
public:
static void Initialize(int precision);
Decimal(void);
Decimal(AutoString num);
Decimal(const Decimal& d);
Decimal(const __int64 n);
Decimal(int intPart, int fractPart);
virtual ~Decimal(void);
Decimal operator+(const Decimal&);
Decimal operator-(const Decimal&);
Decimal operator*(const Decimal&);
Decimal operator/(const Decimal&);
Decimal operator +=(const Decimal&);
Decimal operator -=(const Decimal&);
Decimal operator *=(const Decimal&);
Decimal operator /=(const Decimal&);
bool operator==(const Decimal&) const;
bool operator!=(const Decimal&) const;
bool operator<(const Decimal&) const;
bool operator<=(const Decimal&) const;
bool operator>(const Decimal&) const;
bool operator>=(const Decimal&) const;
CString ToString(void) const;
double ToDouble(void) const;
protected:
__int64 n;
static int precision;
static __int64 q;
static char* pad;
};
This is a pretty basic implementation. A static Initialize
method is used to set up the desired precision of the class, for all instances of Decimal
. Internally, a few helper variables are initialized, which are used elsewhere for string
to Decimal
conversions and the multiplication and division operators:
void Decimal::Initialize(int prec)
{
precision=prec;
pad=new char[precision+1];
memset(pad, '0', precision);
pad[precision]='\0';
q=(__int64)pow(10.0, (double)prec);
}
A Microsoft specific 64 bit integer is used to maintain both integer and fractional components of the value, using the __int64
data type. This is a non-ANSII standard type. If you want a 96 bit integer instead, you can modify my class with PJ Naughter's 96 bit integer class found here [^].
String To __int64 Conversion
Decimal::Decimal(AutoString num)
{
AutoString intPart=num.LeftEx('.');
AutoString fractPart=num.RightEx('.');
fractPart+=&pad[strlen(fractPart)];
n=atoi(intPart);
n*=q;
n+=atoi(fractPart);
}
The conversion from a string to a 64 bit integer is interesting to look at as it reveals the internal workings of the class. First, the integer part and fractional parts are separated from the number. The AutoString
class is a CString
derived class and provides a bit nicer interface for these kind of things.
For example, given "123.456":
AutoString intPart=num.LeftEx('.');
AutoString fractPart=num.RightEx('.');
intPart="123"
fractPart="456"
Now let's say you've initialized the class with a precision of 4 digits past the decimal point. This creates a pad string of "0000" in the initialization function, which is used to determine how many zeros to append to the fractional string. In code:
fractPart+=&pad[strlen(fractPart)];
fractPart
is appended with a single "0" and becomes "4560".
Finally, the two components, the integer and fractional components, are combined by shifting (base 10) the integer component left by the fractional precision and adding the fractional component:
n=atoi(intPart);
n*=q;
n+=atoi(fractPart);
The result is a single integer which maintains both integer and fractional components. Because all Decimal
"numbers" are normalized in this way, the four basic operations (+, -, *, /) are trivial to implement.
__int64 To String Conversion
CString Decimal::ToString(void) const
{
char s[64];
__int64 n2=n/q;
int fract=(int)(n-n2*q);
sprintf(s, "%d.%0*d", (int)n2, precision, fract);
return s;
}
Again, this code reveals the internal workings of the Decimal
class. The 64 bit value is shifted right (base 10) by the precision and the integer component is extracted:
__int64 n2=n/q;
The fractional component is extracted by shifting left the integer component and subtracting from the original value:
int fract=(int)(n-n2*q);
And finally the string is constructed. Note the use of the *
directive which tells the printf
routine to determine the precision of the integer from the variable list:
sprintf(s, "%d.%0*d", (int)n2, precision, fract);
Usage
Decimal::Initialize(4);
double n=.3;
n-=.099;
n*=1000;
printf("n=%.04lf (int)n=%d\r\n", n, (int)n);
printf("n == 201 ? %s\r\n", n==201 ? "yes" : "no");
printf("n >= 201 ? %s\r\n", n>=201 ? "yes" : "no");
Decimal dec(".3");
dec-=Decimal(".099");
dec*=Decimal("1000");
printf("dec=%s\r\n", dec.ToString());
printf("dec == 201 ? %s\r\n", dec==Decimal("201") ? "yes" : "no");
printf("dec >= 201 ? %s\r\n", dec>=Decimal("201") ? "yes" : "no");
The above is an example usage and produces the following output:
Because the __int64
type is composed from two int
types (and decomposed into int
types when converted back to a string), it is limited in range to the same values as a signed four byte number, +/- 2^31, or +/-2,147,483,648.
Also, this code is not "internationalized".
References: