-
Notifications
You must be signed in to change notification settings - Fork 11
Decimals Unit
Note: The Decimals unit is not part of the DelphiBigNumbers library, and this article is only included for completeness.
To calculate non-integral numbers, most programming languages provide floating point types. These are usually fast, because they often are FPU-supported, but there are quite a few problems with their accuracy. More about the problems you can expect can be found in my article about Floating Point Numbers.
In Microsoft .NET, there is a type, called Decimal
that is not
FPU-supported, but very accurate in a defined range. The Decimals
unit
tries to emulate this type as well as possible, in Delphi (and a lot of
assembler). The internal format is exactly the same, but the routines
that manipulate the bits were entirely written by me.
Currently, this is a Win32 type only. Most important routines are in 32 bit assembler. I intend to write a 64 bit assembler version of these routines too, and even a “PUREPASCAL” (i.e. no assembler) version.
Unlike the FPU-supported types, this type uses a decimal “exponent”, although in this case the exponent is more like a scaling factor, and has a range of 0..28. The mantissa is binary, but much larger than that of the FPU types: it contains 96 bits. The sign bit is stored separately. The exact format is like this:
Bits | Function | Comments |
---|---|---|
000..095 | Mantissa | Unsigned 96 bit integer |
096..111 | Reserved | Must be 0 |
112..116 | Scale | Range 0..28, where 0 stands for × 100 and 28 stands for × 10−28 |
117..126 | Reserved | Must be 0 |
127 | Sign bit | 1 = negative, 0 = positive |
Unlike most other floating point types, this type has a different format for values like 0.1 and 0.1000. The first value is stored as mantissa 1 and a scaling factor of 1 (i.e. as 1 × 10−1). The second is stored as 1000 with a scaling factor of 4 (i.e. as 1000 × 10−4), etc. In other words, this type “remembers”, as much as possible, the number of decimals entered. Of course, if you multiply or divide, you will get more decimals.
Although values like 0.123 and 0.1230000 are stored differently, the comparison routines and operators recognize them as equal.
The Decimal type is a record type. That enables it to be used like any other numeric type. It has operators for comparison, simple arithmetic like addition, subtraction, multiplication and division, and conversion operators to and from other numeric types.
A simple code example:
var
A, B, C: Decimal;
begin
A := '1.2345'; // I said it was a little simple
B := '3.49';
C := A + B;
Writeln('C = ', C.ToString);
end;
The output is, as expected:
C = 4.7245
Initialization is best done using one of the conversion operators. If you want a specific format, with a specific number of decimals, use the conversion operator that converts from a string:
myDecimal := '1.3456';
If you want to control the exact contents of the Decimal
, you can use
the following constructor:
constructor Create(Lo, Mid, Hi: Longword; IsNegative: Boolean; Scale: Byte);
To get the exact contents, you can use the GetBits
function.
Additionally to the functions and operators discussed above, Decimal
has a full set of conversion functions and the usual mathematic
functions, like Abs
, Floor
, etc. There is a ToString
function that
converts the Decimal
to a string. The other ToString
function that
takes a Format string is not functional yet, but is being worked on.
The simple DecimalDevelopment
program shows a few ways to use the
type.
As I already mentioned, the type is not FPU-supported. This means it is
a lot slower than the normal FPU types like Single
, Double
or
Extended
. I did my best to make the routines as fast as I could, using
assembler everywhere it made sense, but 96 bit multiplications or
divisions are not really fast, even when they are done in assembler.
This produces the desired results, but I’m glad about any suggestion to
speed up the type. Just
e-mail
me.
I managed to make the basic math operations (addition, subtraction,
multiplication and division) quite a bit faster. Division is about 8
times as fast as it used to be, addition and subtraction are about twice
as fast and multiplication is also a little faster (there was not a lot
I could do to optimize multiplication anymore). Division uses a totally
different algorithm (basecase
, see source) and all of the four
operations profit from better scaling routines.
The version from May 21 appeared to be still buggy. So I took some inspiration from some tests I had seen from the Mono project, and did something similar: I created a simple C# program that uses an array with a range of decimal values to calculate the basic operations on each against each other. The result values are written as a Pascal include file that can immediately be included in a Delphi program that runs the same tests using my Decimal code, and checks the results against the generated result arrays. The code of the result generator is included in the decimals.zip file, and so are the test program and the generated include file. The C# program can be compiled with the freely available Microsoft Visual C# Express or a regular Visual C#.
I changed the code of the Decimals unit thus that now, no error should be generated anymore. In other words, the results produced should be exactly the same as those produced by .NET.
If you want to enhance the test program with your own values, be sure to simply add the values to the C# program and generate a new include file. That can then be used – as is – by the Delphi test program. If you still find any discrepancies, I’d love to hear about them.
I hope this code is useful to you. If you use some of it, please credit me. If you modify or improve the unit, please send me the modifications.
I may improve or enhance the unit myself, and I will try to post changes here. But this is not a promise. Please don’t request features.
Rudy Velthuis