BigDecimals unit

An up-to-date version of this and other files can be found in my DelphiBigNumbers project on GitHub.

BigDecimals

Ten decimal places of π are sufficient to give the circumference of the earth to a fraction of an inch, and thirty decimal places would give the circumference of the visible universe to a quantity imperceptible to the most powerful microscope. — Simon Newcomb

Floating point arithmetic can be very useful for non-integer calculations. The fact that they are hardware supported makes them fast, but due to the fact that the currently usual IEEE-754 types Single (binary32) and Double (binary64) are not only limited in size, they are also limited in precision and range.

Despite the statement in the quote above, sometimes a very high precision is needed. One example are certain financial calculations. The exact calculation of annual rates might require a precision of as much as 2191 digits, as described on this website. The exact calculation of certain physical or mathematical constants like π or e might even require a much higher precision.

I already implemented something every Delphi user should have at its disposal, BigIntegers, and now I also implemented the logical next step: BigDecimals. They are multi-precision, decimal floating point types. A few years ago, I implemented a Delphi version of the .NET-compatible Decimal type. BigDecimal is the big version of that, with an almost unlimited precision and almost unlimited range.

BigDecimal has a range that can vary between -536,870,912 and +536,870,912, so the tiniest value that can be represented is 1 × 10-536,870,912 and the largest is at least 999999… × 10+536,870,911. The precision is around 105,000,000,000.

Usage

BigDecimals are easy to use. They are value types, so you don’t have to worry about memory management. They can be used like:

var
  A, B, C: BigDecimal;
begin
  A := '1.3456e-17';        // exact
  B := 1234.197;            // floating point approximation
  C := A + B * '3.0';
  Writeln(string(C));

While you can use a BigDecimal like a simple floating point type, you should note that their real memory consumption can be much higher. A BigDecimal only consists of a pointer and a few integers, so on your stack it won’t take up much memory, but the value it represents is allocated on the heap, and that can be many more bytes, depending on the precision of the number that is represented.

BigDecimals are immutable, so any expression or public function that modifies the value returns a new BigDecimal. The original is not modified.

Initialization

There are several ways to get values into a BigDecimal. You can use constructors, like

A := BigDecimal.Create(1.79e308);        // floating point value
B := BigDecimal.Create('1.79e308');      // string (exact)

or (implicit or explicit) conversion operators like

B := '3.141592653589793238462643383279'; // string
C := -17;                                // integral type
E := BigDecimal(6.345E-30);              // floating point type

Constructors

Although BigDecimal is a record type and record constructors are no real constructors, and the syntax used can deceive you into thinking a new instance is allocated somewhere, I think records should have them, for initialization. But note that, if A is a BigDecimal, it doesn’t matter if you do:

A := BigDecimal.Create('1.7');

Or just:

A.Create('1.7');

Both will do exactly the same and initialize A with the value 1.7.

The constructors defined are:

constructor Create(const UnscaledValue: BigInteger; Scale: Integer); overload; Creates a BigDecimal with given unscaled value and given scale. The sign is determined by the BigInteger.
constructor Create(const E: Extended); overload; Creates a BigDecimal with the same value as the given Extended parameter.
constructor Create(const D: Double); overload; Creates a BigDecimal with the same value as the given Double parameter.
constructor Create(const S: Single); overload; Creates a BigDecimal with the same value as the given Single parameter.
For parameters of type Extended, Double or Single, an exception of type EInvalidArgument is raised if the parameter contains a NaN or +/- infinity.
constructor Create(const S: string); overload; Creates a BigDecimal with the value that results from parsing the given string parameter.
For a parameter of type string, an exception of type EConvertError is raised if the string cannot be parsed to a valid BigDecimal.
constructor Create(const UnscaledValue: BigInteger); overload; Creates a BigDecimal with the same value as the given BigInteger parameter.
constructor Create(const U: UInt64); overload; Creates a BigDecimal with the same value as the given unsigned 64 bit integer parameter.
constructor Create(const I: Int64); overload; Creates a BigDecimal with the same value as the given signed 64 bit integer parameter.
constructor Create(U: UInt32); overload; Creates a BigDecimal with the same value as the given unsigned 32 bit integer parameter.
constructor Create(I: Int32); overload; Creates a BigDecimal with the same value as the given signed 32 bit integer parameter.

While it may seem convenient to use floating point types to initialize a BigDecimal, note that this will not always give you what you like. There is a difference between

MyBigDecimal := BigDecimal.Create(1.7);

and

MyBigDecimal := BigDecimal.Create('1.7');

The former will create a BigDecimal with the exact value of

1.70000000000000000004336808689942017736029811203479766845703125

This is due to the fact that most floating point numbers are only approximations of the decimal values you give them. The above is the exact value of closest possible Extended representation of the value 1.7, so that is the value the BigDecimal gets too. This is not the case for the latter piece of code, using a string. It will create a BigDecimal with the exact value parsed from the string, i.e. 1.7 (or, to be exact, 17 × 10−1).

Note that there are values a BigDecimal can not represent exactly either, for instance, you cannot express 1/3 exactly in decimal notation. But while Decimal and BigDecimal are good at representing decimal values exactly, floating point types can only represent multiples of powers of two exactly, so they are fine for 0.5 (or 1/2) and 0.375 (or 3/8), but not for, e.g. 0.1 or 1.7.

Another problem with floating point types is that the range of a floating point type is not equally spaced, i.e. the next representable value above 1.79e300 is not 1.79e300 + 1, it is approx. 1.79e300 + 1e264, and any value inbetween is rounded to one of these. This is not the case for BigDecimal. If you use BigDecimal, 1.79e300 + 1 = 1790000…0001 (middle part omitted for legibility), in other words, exactly 1 more than the original value. There are no gaps in the range of a BigDecimal.

More on floating point types in my article about them.

If you need accurate decimal values, avoid floating point types to initialize BigDecimals. Use strings or predefined values instead.

Implicit conversions and class functions

The following implicit conversion operators are defined. Implicit means that you don’t have to cast, but can, if you want to. So you can either do:

  MyBigDecimal := BigDecimal('3.141592');

or you can do:

  MyBigDecimal := '3.141592';

and the result will be exactly the same.

class operator Implicit(const E: Extended): BigDecimal; Returns a BigDecimal with the exact value of the given Extended parameter.
class operator Implicit(const D: Double): BigDecimal; Returns a BigDecimal with the exact value of the given Double parameter.
class operator Implicit(const S: Single): BigDecimal; Returns a BigDecimal with the exact value of the given Single parameter.
class operator Implicit(const S: string): BigDecimal; Returns a BigDecimal with the value parsed from the given string parameter.
class operator Implicit( const UnscaledValue: BigInteger): BigDecimal; Returns a BigDecimal with the value of the given BigInteger parameter.
class operator Implicit(const U: UInt64): BigDecimal; Returns a BigDecimal with the value of the given unsigned 64 bit integer parameter.
class operator Implicit(const I: Int64): BigDecimal; Returns a BigDecimal with the value of the given signed 64 bit integer parameter.

The string parameter can have the same contents as a floating point literal, i.e. it consists mainly of decimal digits ('0'..'9'), it can have at most one decimal separator ('.'), it can have one or more thousand separators (',', or, alternatively, ' '), an exponent part, consisting of an 'e' or 'E', an optional sign ('-' or '+'), and an exponent consisting of decimal digits. The thousand separators can be anywhere. They are simply ignored.

Examples of valid strings are '1', '1.034', '1,000,000.345', '1.79e308' and '3,456,456.4e-13'. But also '1,,,2,,3.4e-99', which is simply interpreted as '123.4e-99'.

Note, however, that trailing zeroes matter. So, while '1.00' and '1.0000' will compare as equal, they are different values. One has two, the other has four decimals, and BigDecimal remembers that. How this is done is explained later on (but know that the former is stored as 100 × 10-2 while the latter is stored as 10,000 × 10-4, so they have different internal representations).

Internals

Before I continue with the other methods and operators, I first want to talk a little about the internals.

A BigDecimal is a record type with only a few member fields. One is a BigInteger, which contains the significant digits of the BigDecimal. This is also called the unscaled value (internal field FValue). The other important member field is the scale (internal field FScale), which determines the power of 10 by which the unscaled value must be divided to get the nominal value of the BigDecimal. This means that a value like 1.79 is stored as an unscaled value of 179 and a scale of 2. In other words, the value is stored as 179 / 102 (which can also be seen as 179 × 10-2). As you see, a positive scale means dividing by a power of 10. But, unlike in my Decimal type, the scale can be negative too, and then you multiply by a power of 10. So 1.79e+308 is stored as an unscaled value of 179 too, but now with a scale of -306.

For what it’s worth, in many programming languages, including Delphi, 1.79e30 or 1.79e+30 are the usual source code representations of the floating point number 1.79 × 1030. E or e stand for “exponent”. Likewise, 3.45e-8 stands for 3.45 × 10−8 (or 0.0000000345).

The usage of a scale allows BigDecimals to have the same nominal values but different precisions. As I said before, 1.00 and 1.0000 compare both as exactly 1, but the former has a precision of 3, while the latter has a precision of 5 digits. The former is stored as 100 / 102, while the latter is stored as 10000 / 104.

The sign of the BigDecimal is simply the sign of the contained BigInteger.

Since BigInteger already knows how to add, subtract, multiply or divide, these operations are done by the BigIntegers. This does not mean that you can simply add two values with different scales. The scale must be adjusted to the larger of the two (by multiplying the BigInteger of the BigDecimal with the smallest scale by a power of 10 and adjusting the scale accordingly). Multiplication is similar: the BigIntegers are multiplied and the scales are added. Division is a little more difficult. You can’t simply divide the BigIntegers, because 1 div 3 returns 0, and you want something like 0.3333333…. That is where precision comes into play. The dividend is first multiplied by 10precision, then it is divided by the divisor and the result is then rounded towards the target scale as much as possible. The target scale is the difference between dividend.Scale and divisor.Scale. How the rounding is done depends on the rounding mode.

Rounding and precision

First, let’s define precision as it is used here. Precision is the number of decimal digits the unscaled value (i.e. the BigInteger) represents. This is equivalent to:

Precision := System.Math.Ceil(UnscaledValue.BitLength * Ln(2) / Ln(10));
if Precision = 0 then
  Precision := 1;

where N * Ln(2) / Ln(10) is equivalent to Log10(N). If the BigInteger is 0, then the result of the first line is 0 too, but that is seen as a precision of 1 anyway. So the precision of 1.79e+308 is only 3, not 308, because the BigInteger is 179 and that has only three digits.

Rounding is cutting off the least significant digits of a value to obtain a number with a lower precision. If the precision you need is higher than the current precison, you simply multiply the unscaled value by the necessary power of 10 and adjust the scale accordingly. No rounding is required. But if the required precision is lower, you must cut off digits at the right. But sometimes, you must adjust the unscaled value to do the required rounding.

BigDecimal defines an enumeration type RoundingMode, which governs how values are rounded, for instance after a division. It can can have the following values:

rmUp Rounds up, away from zero
rmDown Rounds down towards zero, i.e. it truncates the least significant digits
fmCeil Rounds towards positive infinity
rmFloor Rounds towards negative infinity
The following three modes round to the nearest digit, but if there is a tie, i.e. if the result is exactly halfway two next higher digits, they behave differently:
rmNearestUp Rounds to the nearest higher digit, but on a tie, it rounds to the nearest higher digit that is closer to zero
rmNearestDown Rounds to the nearest higher digit, but on a tie, it rounds to the nearest higher digit that is further away from zero
rmNearestEven Rounds to the nearest higher digit, but on a tie, it rounds towards the nearest higher even digit
The following mode should only be used if you know that no rounding will take place:
rmUnncessary Rounding is not necessary.
Raises an exception of type ERoundingNecessary if rounding turned out to be necessary after all.

As you may or may not have noticed, I based most of the interface of BigDecimal on the Java type of the same name. There, rounding and precision are stuck together in a MathContext class, which must be passed in in most circumstances where rounding or a precision are required. So, in Java, to do a simple division, you either hope that the division does not cause a never-ending recurrence of digits, e.g. what happens when you divide 1 by 3, or you pass a MathContext with the required precision and rounding mode. Java does not have operator overloading, so that looks like:

MathContext mc = new MathContext(20, RoundingMode.HALF_DOWN); // precision, rounding mode
BigDecimal monthly = total.add(fixed).divide(BigDecimal.valueOf(12), mc);
BigDecimal additional = monthly.multiply(BigDecimal.valueOf("1.19")).add(BigDecimal.valueOf("3.2"));

But I wanted operator overloading, and then you can’t pass a precision or a rounding mode alongside the operands. That is why I gave BigDecimal two class properties for this, aptly called DefaultPrecision and DefaultRoundingMode. These contain the defaults for all BigDecimal operations. So in Delphi, you do:

  BigDecimal.DefaultPrecision := 20;
  BigDecimal.DefaultRoundingMode := rmNearestDown;

  Monthly := (Total + Fixed) / 12;
  Additional := Monthly * 1.19 + 3.2;

Back to Usage

Mathematical operations

A type like this is of no use if you can’t calculate with it. BigDecimal defines the usual mathematical operators +, -, * and /. But it also defines div and mod. Of those two, the former returns an integral division result, the latter the remainder after that division. These operators have corresponding class functions as well, and the overloaded versions of these take rounding mode and precision parameters too. Here’s a table:

// Result := Left + Right; class operator Add( const Left, Right: BigDecimal): BigDecimal; Adds two BigDecimals. The new scale is Max(Left.Scale, Right.Scale).
An exception of type EOverflow is raised if the result would become too big.
// Result := Left – Right; class operator Subtract( const Left, Right: BigDecimal): BigDecimal; Subtracts two BigDecimals. The new scale is Max(Left.Scale, Right.Scale).
An exception of type EOverflow is raised if the result would become too big.
// Result := Left * Right; class operator Multiply( const Left, Right: BigDecimal): BigDecimal; Multiplies two BigDecimals. The new scale is Left.Scale + Right.Scale.
An exception of type EOverflow is raised if the result would become too big.
An exception of type EUnderflow is raised if the result would become too small.
// Result := Left / Right; class operator Divide( const Left, Right: BigDecimal): BigDecimal; Divides two BigDecimals.

Uses the default precision and rounding mode to obtain the result.

The target scale is Left.Scale - Right.Scale. The result will approach this target scale as much as possible by removing any excessive trailing zeros.
An exception of type EZeroDivide is raised if the divisor is zero.
An exception of type EOverflow is raised if the result would become too big.
An exception of type EUnderflow is raised if the result would become too small.
// Result := Left div Right; class operator IntDivide( const Left, Right: BigDecimal): BigDecimal; Divides two BigDecimals to obtain an integral result.
An exception of type EZeroDivide is raised if the divisor is zero.
An exception of type EOverflow is raised if the result would become too big.
An exception of type EUnderflow is raised if the result would become too small.
// Result := Left mod Right; class operator Modulus( const Left, Right: BigDecimal): BigDecimal; Returns the remainder after Left is divided by Right to an integral value.
An exception of type EZeroDivide is raised if the divisor is zero.
An exception of type EOverflow is raised if the result would become too big.
An exception of type EUnderflow is raised if the result would become too small.
// Result := -Value; class operator Negative( const Value: BigDecimal): BigDecimal; Negates the given BigDecimal.
// Result := +Value; class operator Positive( const Value: BigDecimal): BigDecimal; Called when a BigDecimal is preceded by a unary +. Currently a no-op.
// Result := Round(Value); class operator Round( const Value: BigDecimal): Int64; Rounds the given BigDecimal to an Int64.
An exception of type EConvertError is raised if the result is too large to fit in an Int64.
// Result := Trunc(Value); class operator Trunc( const Value: BigDecimal): Int64; Truncates (rounds down towards 0) the given BigDecimal to an Int64.
An exception of type EConvertError is raised if the result is too large to fit in an Int64.
class function Add( const Left, Right: BigDecimal): BigDecimal; overload; static; See operator Add
class function Subtract( const Left, Right: BigDecimal): BigDecimal; overload; static; See operator Subtract
class function Multiply( const Left, Right: BigDecimal): BigDecimal; overload; static; See operator Multiply
class function Divide( const Left, Right: BigDecimal): BigDecimal; overload; static; See operator Divide
class function Divide( const Left, Right: BigDecimal; Precision: Integer; ARoundingMode: RoundingMode): BigDecimal; overload; static; Like operator Divide, but uses the given Precision and RoundingMode
class function Divide( const Left, Right: BigDecimal; Precision: Integer): BigDecimal; overload; static; Like operator Divide, but uses the given Precision and the default rounding mode
class function Divide( const Left, Right: BigDecimal; ARoundingMode: RoundingMode): BigDecimal; overload; static; Like operator Divide, but uses the given RoundingMode and the default precision
class function Negate( const Value: BigDecimal): BigDecimal; overload; static; See operator Negative
class function Round( const Value: BigDecimal): Int64; overload; static; See operator Round
class function Round( const Value: BigDecimal; ARoundingMode: RoundingMode): Int64; overload; static; Like operator Round, but uses the given RoundingMode
// Result := Left – (Left div Right) * Right; class function Remainder( const Left, Right: BigDecimal): BigDecimal; static; See operator Modulus
class function Abs( const Value: BigDecimal): BigDecimal; overload; static; Returns the absolute value of the given BigDecimal.
class function Sqr( const Value: BigDecimal): BigDecimal; overload; static; Returns the square (Value * Value) of the given BigDecimal.
class function Sqrt( const Value: BigDecimal; Precision: Integer): BigDecimal; overload; static; Returns the square root of the given BigDecimal, using the given Precision.
class function Sqrt( const Value: BigDecimal): BigDecimal; overload; static; Returns the square root of the given BigDecimal, using the default precision.
class function IntPower( const Base: BigDecimal; Exponent, Precision: Integer): BigDecimal; overload; static; Returns Base raised to the integral power of Exponent, in the given Precision. This routine is optimized by limiting the precision of intermediate values.
An exception of type EIntPowerExponent is raised if the exponent is outside the range -9999999..9999999.
class function IntPower( const Base: BigDecimal; Exponent: Integer): BigDecimal; overload; static; Returns Base raised to the integral power of Exponent, in unlimited precision.
An exception of type EIntPowerExponent is raised if the exponent is outside the range -9999999..9999999.
function Abs: BigDecimal; overload; Returns the absolute value of the current BigDecimal.
function Int: BigDecimal; Returns a BigDecimal with any fraction (digits after the decimal point) removed from the current BigDecimal.
function Trunc: Int64; Returns a signed 64 bit integer with any fraction (digits after the decimal point) removed from the current BigDecimal.
function Frac: BigDecimal; Returns a BigDecimal containing only the fractional part (digits after the decimal point) of the current BigDecimal.
function Reciprocal(Precision: Integer): BigDecimal; overload; Returns the reciprocal of the current BigDecimal, using the given Precision.
An exception of type EZeroDivide is raised if the current BigDecimal is zero.
function Reciprocal: BigDecimal; overload; Returns the reciprocal of the current BigDecimal, using the default precision.
An exception of type EZeroDivide is raised if the current BigDecimal is zero.
function Sqrt(Precision: Integer): BigDecimal; overload; Returns the square root of the current BigDecimal, with the given precision
function Sqrt: BigDecimal; overload; Returns the square root of the current BigDecimal, with the default precision.
function Sqr: BigDecimal; overload; Returns the square (Self * Self) of the current BigDecimal.
function IntPower( Exponent, Precision: Integer): BigDecimal; overload; Returns the current BigDecimal raised to the integral power of Exponent, in the given Precision.
An exception of type EIntPowerExponent is raised if the exponent is outside the range -9999999..9999999.
function IntPower( Exponent: Integer): BigDecimal; overload; Returns the current BigDecimal raised to the integral power of Exponent, in unlimited precision.
An exception of type EIntPowerExponent is raised if the exponent is outside the range -9999999..9999999.

Comparison operations

All comparison operations base on the Compare function. Here they are:

// Result := (Left <= Right); class operator LessThanOrEqual( const Left, Right: BigDecimal): Boolean; Returns True only if Left is mathematically less than or equal to Right.
// Result := (Left < Right); class operator LessThan( const left, Right: BigDecimal): Boolean; Returns True only if Left is mathematically less than Right.
// Result := (Left >= Right); class operator GreaterThanOrEqual( const Left, Right: BigDecimal): Boolean; Returns True only if Left is mathematically greater than or equal to Right.
// Result := (Left > Right); class operator GreaterThan( const Left, Right: BigDecimal): Boolean; Returns True only if Left is mathematically greater than Right.
// Result := (Left = Right); class operator Equal( const Left, Right: BigDecimal): Boolean; Returns True only if Left is mathematically equal to Right.
// Result := (Left <> Right); class operator NotEqual( const Left, Right: BigDecimal): Boolean; Returns True only if Left is mathematically not equal to Right.
class function Compare( const Left, Right: BigDecimal): TValueSign; static; Returns 1 if Left is mathematically greater than Right, 0 if Left is mathematically equal to Right and -1 if Left is mathematically less than Right.
class function Max( const Left, Right: BigDecimal): BigDecimal; static; Returns the maximum of the two given BigDecimal values.
class function Min( const Left, Right: BigDecimal): BigDecimal; static; Returns the minimum of the two given BigDecimal values.

Just like in mathematics, Compare compares two values with different scale but same nominal value as equal, so

  X := BigDecimal.Compare('1.10', '1.1000');

returns 0, meaning equality. In the same sense,

  Y := BigDecimal('-1.2300') < BigDecimal('-1.23');

returns False.

In Java, if two BigDecimals have the same nominal value, but different scales, the equals() function returns false. And if you use ==, they must even be identical, i.e. have the same reference (address). To compare the numerical value of two BigDecimals, let’s call them a and b, you must do something like areTheyEqual = (a.compareTo(b) == 0);. This is not necessary for the Delphi BigDecimals described here. You can simply use = to compare two BigDecimals for numerical equality, even if they have different scales.

Explicit conversions

Explicit conversions were made to be silent, so if this is possible, they don’t raise exceptions. In the following piece of code (without BigDecimals):

var
  I64: Int64;
  I32: Integer;
begin
  I64 := -10000000000000;
  I32 := Integer(I64);

I32 ends up with a value of -1316134912, because that is the value of the low 32 bit part of the Int64. The same principle applies to BigDecimal, so instead of raising an exception because the value in the BigDecimal does not fit in the target type, a silent conversion is performed that makes it fit.

class operator Explicit( const Value: BigDecimal): Extended; Returns an Extended with the best possible approximation of the given BigDecimal value.

The conversion uses the default rounding mode.
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
class operator Explicit( const Value: BigDecimal): Double; Returns a Double with the best possible approximation of the given BigDecimal value.

The conversion uses the default rounding mode.
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
class operator Explicit( const Value: BigDecimal): Single; Returns a Single with the best possible approximation of the given BigDecimal value.

The conversion uses the default rounding mode.
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
class operator Explicit( const Value: BigDecimal): string; Returns a string representation of the given BigDecimal value.

This uses ToString, which generally returns the shortest possible string representation of the BigDecimal.
class operator Explicit( const Value: BigDecimal): BigInteger; Returns a BigInteger with the rounded value of the given BigDecimal.

The conversion uses the rounding mode rmDown, i.e. it truncates.
class operator Explicit( const Value: BigDecimal): UInt64; Returns an unsigned 64 bit integer with the rounded value of the given BigDecimal value.

The conversion uses the rounding mode rmDown, i.e. it truncates.
class operator Explicit( const Value: BigDecimal): Int64; Returns a signed 64 bit integer with the rounded value of the given BigDecimal value.

The conversion uses the rounding mode rmDown, i.e. it truncates.

String conversion

There are a few routines for conversion to and from a string. A valid string is like a valid floating point literal:

  • an optional sign ('+' or '-')
  • a significand, consisting of
    • an integral part, consisting of one or more decimal digits ('0'..'9')
    • an optional decimal point
    • an optional fractional part, consisting of one or more decimal digits
    • optional thousands separators or spaces — these are ignored
  • an optional exponent, consisting of
    • an exponent delimiter, either 'e' or 'E'
    • an optional sign ('+' or '-')
    • an exponent value, consisting of one or more decimal digits

Here are a few examples of valid (locale invariant) strings representing a BigDecimal:

string unscaled value scale string unscaled value scale
'0' 0 0 '0.00' 0 2
'179' 179 0 '-179' -179 0
'1.79e3' 179 -1 '1.79e+3' 179 -1
'17.9e+7' 179 -6 '17.0' 170 1
'17.9' 179 1 '0.00179' 179 5
'-1.79e-12' -179 14 '1,798.1e-4' 17981 5
'0e+7' 0 -7 '-0' 0 0
'123,456.78' 12345678 2 '1 234e+8' 1234 -8

Parsing

There are four routines for parsing a string into a BigDecimal. Two use the invariant format settings, where '.' is the decimal separator and ',' is the thousands separator. The other two use explicit TFormatSettings parameters, e.g. for Germany, where I live, I could use TFormatSettings.Create('de_DE').

class function TryParse( const S: string; const Settings: TFormatSettings; out Value: BigDecimal): Boolean; overload; static; Tries to parse the given string as a BigDecimal into Value, using the given format settings.

Returns True if the function was successful.
class function TryParse( const S: string; out Value: BigDecimal): Boolean; overload; static; Tries to parse the given string as a BigDecimal into Value, using the system invariant format settings.

Returns True if the function was successful.
class function Parse( const S: string; const Settings: TFormatSettings): BigDecimal; overload; static; Returns the BigDecimal with a value as parsed from the given string, using the given format settings.
An exception of type EConvertError is raised if the string cannot be parsed to a valid BigDecimal.
class function Parse( const S: string): BigDecimal; overload; static; Returns the BigDecimal with a value as parsed from the given string, using the system invariant format settings.
An exception of type EConvertError is raised if the string cannot be parsed to a valid BigDecimal.

Conversion to string

There are four routines that convert a BigDecimal to a string. The routines ToString generally return the shortest possible string representation. If necessary, the value is represented in scientific notation, i.e. with an exponent. So 1.79e30 is represented as '1.79e30'. The routines ToPlainString always use the plain notation and don’t use scientific. This means that a value of 1.79e30 is represented as '1790000000000000000000000000000'. But note that both representations do not lose precision, so if the precision requires thirty decimals, even ToString will represent all of them. An example:

var
  D1, D2: BigDecimal;
begin
  D1 := '1.7900000000000000000000e30'; // note: 20 trailing zeros
  D2 := '1.79e30';
  Writeln(Format('D1 = %s, D2 = %s', [D1.ToString, D2.ToString]));
  Writeln(Format('D1 = %s, D2 = %s', [D1.ToPlainString, D2.ToPlainString]));
end;

The output is

D1 = 1.7900000000000000000000e+30, D2 = 1.79e+30
D1 = 1790000000000000000000000000000, D2 = 1790000000000000000000000000000

In other words, '1.7900000000000000000000e+30' is the shortest representation D1.ToString can generate, because the trailing zeros are part of the precision and they can not be cut off.

There is a way to cut off the trailing zeros of a BigDecimal, using RemoveTrailingZeros. This is discussed later on.

function ToString: string; overload; Returns the short notation of the current BigDecimal in the system invariant format settings. If necessary, scientific notation is used.
Because this does not use TFormatSettings, the result is roundtrip, so it is a valid string that can be parsed using Parse() or TryParse().
function ToString( const Settings: TFormatSettings): string; overload; Returns the short notation of the current BigDecimal, using the given TFormatSettings to obtain the decimal point Char. If necessary, scientific notation is used.
function ToPlainString: string; overload; Returns a plain string of the current BigDecimal, using the system invariant format settings. This plain notation is sometimes called “decimal notation”, and represents the value without the use of exponents.
function ToPlainString( const Settings: TFormatSettings): string; overload; Returns a plain string of the current BigDecimal, using the given TFormatSettings to obtain the decimal point Char.

Miscellaneous functions

There are a number of functions that return information about a BigDecimal, or return modified versions of the original BigDecimal.

Rounding and scaling

Rounding and scaling are closely related. Scaling down often requires rounding. The following functions return rounded or truncated versions of the original values.

function RoundTo(Digits: Integer; ARoundingMode: RoundingMode): BigDecimal; overload; Rounds the current BigDecimal to a value with at most Digits fractional digits, using the given rounding mode. This is more or less equivalent to the RoundTo function for floating point types.
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
The System.Math.RoundTo function uses the floating point rounding mode equivalent of rmNearestEven, while System.Math.SimpleRoundTo uses the equivalent of rmNearestUp. This function is more versatile.
This is exactly equivalent to RoundToScale(-Digits, ARoundingMode).
function RoundTo(Digits: Integer): BigDecimal; overload; Rounds the current BigDecimal to a value with at most Digits fractional digits, using the default rounding mode.
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
function RoundToScale(NewScale: Integer; ARoundingMode: RoundingMode): BigDecimal; Rounds the current BigDecimal to a value with the given scale, using the given rounding mode.
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
function RoundToPrecision( APrecision: Integer): BigDecimal; overload; Rounds the current BigDecimal to a certain precision (number of significant digits).
An exception of type ERoundingNecessary is raised if a rounding mode rmUnnecessary was specified but rounding is necessary after all.
function RemoveTrailingZeros( TargetScale: Integer): BigDecimal; Returns a new BigDecimal with all trailing zeroes (up to the target scale) removed from the current BigDecimal. No significant digits will be removed and the numerical value of the result compares as equal to the original value.

TargetScale is the scale up to which trailing zeroes can be removed. It is possible that fewer zeroes are removed, but never more than necessary to reach the target scale.
Example: BigDecimal('1234.5678900000').RemoveTrailingZeros(6) results in BigDecimal('1234.567890').

Information

The following instance methods return information about the current BigDecimal.

function IsZero: Boolean; Returns True if the current BigDecimal’s value equals BigDecimal.Zero.
function Sign: TValueSign; Returns the sign of the current BigDecimal: -1 if negative, 0 if zero, 1 if positive.
function Precision: Integer; Returns the number of significant digits of the current BigDecimal.
function ULP: BigDecimal; Returns the unit of least precision of the current BigDecimal.

Properties

Predefined BigDecimals

The unit defines a few useful constants for often needed BigDecimal values. These should be used preferrably to newly created BigDecimals with the same values. That avoids duplication of the payload of the contained BigInteger.

class property MinusOne: BigDecimal; A BigDecimal representing -1.
class property Zero: BigDecimal; A BigDecimal representing 0.
class property One: BigDecimal; A BigDecimal representing 1.
class property Two: BigDecimal; A BigDecimal representing 2.
class property Ten: BigDecimal; A BigDecimal representing 10.
class property Half: BigDecimal; A BigDecimal representing 0.5.
class property OneTenth: BigDecimal; A BigDecimal representing 0.1.

System-wide defaults

Beside the already mentioned defaults for precision and rounding mode, there is also a default for the exponent delimiter used in string output. The default is 'e' and not, as in most other implementations, 'E', because to me, a lower case letter between digits that generally have the height of upper case letters, is more clearly visible than an upper case 'E'. In other words, I prefer '1.798765432102345e+30' over '1.798765432102345E+30', because I find it more readable.

class property DefaultRoundingMode: RoundingMode readwrite …; The rounding mode to be used if no specific mode is indicated, e.g. in expressions using overloaded operators.
class property DefaultPrecision: Integer readwrite …; The (maximum) precision to be used for e.g. division if the operation would otherwise result in a non-terminating decimal expansion, i.e. if there is no exact representable decimal result, e.g. when dividing BigDecimal.One / BigDecimal(3), resulting in the neverending 0.3333333…
class property ExponentDelimiter: Char readwrite …; The string to be used to delimit the exponent part in scientific notation output.
Currently, only 'e' and 'E' are allowed. Setting any other value will be ignored. The default is 'e'.

Access to internals

The fields of a BigDecimal can be accessed read-only:

property UnscaledValue: BigInteger read …; The unscaled value of the current BigDecimal. This is the BigInteger that contains the significant digits and the sign of the BigDecimal. To obtain the nominal value, it is then scaled (in powers of ten) by Scale.
property Scale: Integer read …; The scale of the current BigDecimal. This is the power of ten by which the UnscaledValue must be divided to get the nominal value of the BigDecimal. If it is positive, it represents the number of digits after the decimal point. A negative scale value stands for multiplying by a power of ten.

A negative scale my be a little hard to understand. Note that 1e+3, which has a precision of 1, will be represented by an UnscaledValue of 1, but to make it get the value of 1 × 103, the scale will have to be set to -3. Mathematically, that is the same as 1000, but internally, it isn’t. That is because 1e+3 has a precision of 1, while 1000 has a precision of 4.

Visualizer

Since I had to write a simple parser for the debugger visualizer for BigInteger anyway, I amended it to parse debug output for BigDecimal too, so now there is a debugger visualizer for both BigInteger and BigDecimal, in the same unit. See here how to get and install it.

Notes

FPrecision

Currently, there is a private FPrecision instance field. This is meant to serve as a cache for the Precision function. BigDecimals are immutable, so the value of the record never changes. The idea is that if FPrecision is 0, it is uninitialized and the Precision function must calculate the precision. But once that is done, it can’t change, so it could be stored in FPrecision and the function could return this cached value. The problem is, however, that unlike classes, records are not zeroed out on automatic initialization. So if, in the Precision function, I read a value that is not 0, I can’t be sure if it was calculated before, or if it is garbage resulting from previous use of the memory. All routines that initialize or modify the internals of a new BigDecimal currently initialize FPrecision to 0, but the uncertainty remains.

On the other hand, if you use an Integer or a Double without initializing it, you get garbage and undefined behaviour too. So I could blame undefined values of FPrecision on the same undefined behaviour you get when you don’t initialize variables of the built-in types and simply use FPrecision as it was intended.

Mathematics

Currently, there are no functions that provide higher mathematical functions (except Sqrt) for BigDecimals. I intend to write a new unit that provides functions like Cos, ArcTan, SinH, Ln, Exp or Pi with a set precision, but that is still in the planning stage. Such functions will probably be extremely slow, compared to hardware supported floating point, but much more accurate.

Number formatter

I also plan to implement a unit that provides a general number formatter. To this, you pass a record containing a string of digits, a scale and some more information, and it uses a format string like '#,##000.e+000', more or less like Delphi’s FloatToStr does, to format the output. I intend to make it able to format the built-in floating point types, my Decimal type and my BigDecimal type by default, but it should also be able to format other types, if you can pass the data in the required record.

Conclusion

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 at this e-mail address.

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

Standard Disclaimer for External Links

These links are being provided as a convenience and for informational purposes only; they do not constitute an endorsement or an approval of any of the products, services or opinions of the corporation or organization or individual. I bear no responsibility for the accuracy, legality or content of the external site or for that of subsequent links. Contact the external site for answers to questions regarding its content.

Disclaimer and Copyright

The coding examples presented here are for illustration purposes only. The author takes no responsibility for end-user use. All content herein is copyrighted by Rudy Velthuis, and may not be reproduced in any form without the author's permission. Source code written by Rudy Velthuis presented as download is subject to the license in the files.

Back to top