The BigNumber Intrinsic Class

TADS 2 provided only integer arithmetic.  While this was largely adequate for writing interactive fiction, the need nonetheless arises on occasion for more powerful arithmetic features than integer math provides, particularly floating point values (numbers involving a fractional part), greater numeric range (the ability to represent very large or very small numbers), and greater precision (the ability to represent fine differences between values).  In adventure game programming, it is usually possible to simulate these effects using integer arithmetic only, but this is considerably more work than it would be with a more powerful math package.


TADS 3 addresses this occasional need with a new intrinsic class called BigNumber, which provides high- precision floating-point arithmetic.  BigNumber can represent values with enormous precision, storing up to 65,000 decimal digits in a value; and can represent a huge range of values, with absolute values up to 1032,767 and down to 10-32,767.   These limits are so high that, for all practical purposes, calculations will never encounter them.  Furthermore, the BigNumber type can store values with whatever precision is actually required for each particular value, up to the limits; a program can use this flexibility to strike the balance it requires between numerical precision and performance.  (For reasons that are probably obvious, the more precision a BigNumber value stores, the more memory it uses and the more time it takes to perform calculations with the number.  BigNumber lets the programmer determine how much precision to use, so that the programmer can balance the degree of numerical precision against the cost in performance.)

 

Note that the BigNumber type is not the "double-precision" or IEEE floating point type with which readers with a programming background will be familiar.  The BigNumber type is superior in many ways to double-precision values:

 

·         BigNumber is portable to any computer, and behaves exactly the same way on every computer.  Doubles tend to have subtle but sometimes vexing variations from one computer to another.

·         BigNumber uses a decimal rather than binary encoding, which means that any value that one can write in a decimal format can be represented exactly without rounding error.  Many common decimal values turn into repeating sequences when expressed in binary (much as 1/3 cannot be exactly represented in decimal: the 3's repeat forever in 0.33333…).  This can lead to surprising results when rounding errors accumulate in calculations that, to all appearances, should be exact.

·         BigNumber allows for effectively unlimited precision and much greater range than doubles, which have a fixed precision that cannot be changed by the programmer.  This allows, for example, calculations involving very large integers, or precise calculations where very large and very small values are combined.

 

An advantage of double-precision values over the BigNumber representation is better performance, partly because arithmetic is simpler with a fixed-precision type, but mostly because doubles are implemented in hardware on many platforms.  This seems a reasonable sacrifice, though; the kinds of applications that require high-speed numerics probably wouldn't find TADS to be an ideal choice of language anyway.

Working with BigNumber Values

You must include the system header file <bignum.h> in your source files to use the BigNumber class.  This file defines the BigNumber class interface.

 

To create a BigNumber value, use the new operator, passing the value for the number either as an integer or as a character string.  You can optionally specify the precision to use for the value; if you don't specify a precision, the system infers a precision from the value.

 

  x = new BigNumber(100);
  x = new BigNumber(100, 10); // set precision to 10 digits
  y = new BigNumber('3.14159265');
  z = new BigNumber('1.06e-30');
  z = new BigNumber('1.06e-30', 8); // precision is 8 digits

 

If you specify a string value, you can use a decimal point, and you can also use an 'E' to specify a base-ten exponent.  So, the fourth value above should be read as 1.06 x 10-30.

 

You can also create a BigNumber object simply by using a numeric constant that contains a decimal point or an exponent (or both):

 

  x = 1.075;

 

You can perform addition, subtraction, multiplication, division, and negation on BigNumber values using the standard operators.  You can also use integer values with BigNumber values in calculations, although the BigNumber value must always be the first operand in an expression involving both a BigNumber and an integer.

 

  x = y + z;
  x = (y + z) * (y – z) / 2;

 

Similarly, you can compare BigNumber values using the normal comparison operators:

 

  if (x > y) ...

 

You can convert a BigNumber to a string using the toString() function in the "tads-gen" intrinsic function set.  You can exercise more control over formatting using the formatString() method of the BigNumber class itself.

 

You can convert a BigNumber to a regular integer value using the toInteger() function from the "tads-gen" function set.  Note that toInteger() throws an error if passed a BigNumber value that is too large to represent as a 32-bit integer, which can store values from 2,147,483,647 to –2,147,483,647.  toInteger() rounds numbers with fractional parts to the nearest integer.

 

You cannot use operators other than those listed above with BigNumber values.  You cannot use a BigNumber as an operand for any of the bitwise operators (&, |, ~).  You also cannot use a BigNumber with the integer modulo operator ("%"), but you can obtain similar functionality from the divideBy() method.

 

You cannot use BigNumber values in function and method calls that require integer arguments.  You must explicitly convert a BigNumber value to an integer with the toInteger() function if you want to pass it to a method or function that takes an integer value; the compiler does not perform these conversions for you automatically.

 

Because BigNumber values are, for most purposes, simply object references, you can use them where you can use other objects; you can, for example, store a BigNumber in a list, or assign it to an object property.

Compiler Support

Although BigNumber is not a native type in the T3 VM (it is simply an intrinsic class, which plugs into the VM using the VM's type extension mechanism), the TADS 3 compiler has some minimal support for the type.  In particular, the compiler recognizes BigNumber constant values in this format:

 

   [ digits ] . [ digits ] [ E|e [+|-] digits ]

 

In other words, the compiler recognizes any numeric constant that includes a decimal point (a period) or an exponent (specified with an 'e' or 'E') as a BigNumber constant.  When you include a number in this format in your source code, the compiler will automatically create and initialize a BigNumber object for you with the given value.  So, the following two statements are equivalent:

 
   x = new BigNumber('3.14159265');
   y = 3.14159265;

 

The compiler uses the same rules as the new operator for parsing the number's value and precision.

 

Note that the compiler does not currently perform constant folding on BigNumber values.  This means that expressions like the following will result in a run-time calculation being performed:

 

   #define PI 3.14159265
   x = PI/4;

Intrinsic Class Methods

The BigNumber class provides a number of methods for manipulating values.  Note that all of the methods that perform calculations return new BigNumber values.  A BigNumber object's value is immutable once the object is created, so all calculations performed on these objects return new objects representing the result values.

 

Note that these functions are all methods called on a BigNumber object, so to calculate the absolute value of a BigNumber value x, we would code this:

 

  y = x.getAbs();
 

Some of the methods take an argument giving a value to be combined with the target number.  For example, to get the remainder of dividing 10 by 3, we'd write this:

 

  x = new BigNumber('10.0000');
  y = new BigNumber('3.00000');
  rem = x.divideBy(y)[2];  // second list item is remainder

 

arccosine() – returns the arccosine (the number whose cosine is this value), as a value in radians, of the number.  This function is mathematically meaningful only for input values from –1 to +1; this function throws a run-time exception if the input value is outside of this range.

 

arcsine() – returns the arcsine (the number whose sine is this value), as a value in radians, of the number.  This function is mathematically meaningful only for input values from –1 to +1; this function throws a run-time exception if the input value is outside of this range.

 

arctangent() – returns the arctangent (the number whose tangent is this value), as a value in radians, of the number.

 

copySignFrom(x) – returns a number containing the same absolute value as this number, but with the sign of x replacing the original value's sign.

 

cosh() – computes the hyperbolic cosine of the number and returns the result.

 

cosine() – computes the trigonometric cosine of the number (interpreted as a radian value) and returns the result.  Refer to the description of sine() for notes on how the input precision affects the calculation.

 

degreesToRadians() – converts the value from radians to degrees and returns the number of degrees.  This simply multiplies the value by (pi/180).

 

divideBy(x) – computes the integer quotient of dividing this number by x, and returns a list with two elements.  The first element is a BigNumber value giving the integer quotient, and the second element is a BigNumber value giving the remainder of the division, which is a number rem satisfying the relationship dividend = quotient*divisor + remainder.

 

Note that the quotient returned from divideBy() is not necessarily equal to the whole part of the result of the division ("/") operator applied to the same values.  If the precision of the result (which is, as with all calculations, equal to the larger of the precisions of the operands) is insufficient to represent exactly the integer quotient result, the quotient returned from this function will be rounded differently from the quotient returned by the division operator.  The division operator always rounds its result to the nearest

 

equalRound(num) – determine if this value is equal to num after rounding.  This is equivalent to the "==" operator if the numbers have the same precision, but if one number is more precise than the other, this rounds the more precise of the two values to the precision of the less precise value, then compares the values.  The "==" operator makes an exact comparison, effectively extending the precision of the less precise value by adding imaginary zeroes to the end of the number.

 

expE() – returns the result of raising e, the base of the natural logarithm, to the power of this number.

 

formatString(maxDigits, flags?, wholePlaces?, fracDigits?, expDigits?, leadFiller?) – Formats the number, returning a string with the result.  All of the arguments after maxDigits are optional.

 

maxDigits specifies the maximum number of digits to display in the formatted number; this is an upper bound only, and doesn't force a minimum number of digits.  If necessary, the function uses scientific notation to make the number fit in the requested number of digits.

 

wholePlaces specifies the minimum number of places to show before the decimal point; if the number doesn't fill all of the requested places, the function inserts leading spaces (before the sign character, if any). 

 

fracDigits specifies the number of digits to display after the decimal point.  This specifies the maximum to display, and also the minimum; if the number doesn't have enough digits to display, the method adds trailing zeroes, and if there are more digits than fracDigits allows, the method rounds the value for display. 

 

expDigits is the number of digits to display in the exponent; leading zeroes are inserted if necessary to fill the requested number of places.

 

Each of wholePlaces, fracDigits, and expDigits can be specified as –1, which tells the method to use the default value, which is simply the number of digits actually needed for the respective parts.

 

leadFiller, if specified, gives a string that is used instead of spaces to fill the beginning of the string, if required to satisfy the wholePlaces argument.  This argument is ignored if its value is nil.  If a string value is provided for this argument, the characters of the string are inserted, one at a time, to fill out the wholePlaces requirement; if the end of the string is reached before the full set of padding characters is inserted, the function starts over again at the beginning of the string.  For example, to insert alternating asterisks and pound signs, you would specify '*#' for this argument.

 

flags is a combination of the following bit-flag values (combined with the bit-wise OR operator, '|'):

 

 

getAbs() – returns a number containing the absolute value of this number.  (This function could be easily coded from a comparison and negation, but the method implementation is more efficient.)

 

getCeil() – "ceiling": returns a number containing the least integer greater than this number.  For example, the ceiling of 2.2 is 3.  Note that for negative numbers, the least integer above a number has a smaller absolute value, so the ceiling of –1.6 is –1.

 

getE(digits) – returns the value of e (the base of the natural logarithm) to the given number of digits of precision.  This is a static method, so you can call this method directly on the BigNumber class itself:

 

    x = BigNumber.getE(10);

 

The BigNumber class internally caches the value of e to the highest precision calculated during the program's execution, so this routine only needs to compute the value when it is called with a higher precision than that of the cached value.

 

getFloor() – "floor": returns a number containing the greatest integer less than this number.

 

getFraction() – returns a number containing only the fractional part of this number (the digits after the decimal point).

 

getPi(digits) – returns the value of pi to the given number of digits of precision.  This is a static method, so you can call this method directly on the BigNumber class itself:

 

    x = BigNumber.getPi(10);

 

The BigNumber class internally caches the value of pi to the highest precision calculated during the program's execution, so this routine only needs to compute the value when it is called with a higher precision than that of the cached value.

 

getPrecision() – returns the number of digits of precision that this number stores.  The return value is of type integer.

 

getScale() – returns a value of type integer giving the base-10 scale of this number.  If the return value is positive, it indicates the number of digits before the decimal point in the decimal representation of the number.  If the return value is zero, it indicates that the number has no whole part, and that the first digit after the decimal point is non-zero (so the number is greater than or equal to 0.1, and less than 1.0).  If the return value is negative, it indicates that the number has no whole part, and gives the number of digits of zeroes that immediately follow the decimal point before the first non-zero digit.  If the value of the number is exactly zero, the return value is 1.

 

getWhole() – returns a number containing only the whole part of this number (the digits before the decimal point).

 

isNegative() – returns true if the number is less than zero, nil if the number is greater than or equal to zero.

 

log10() – returns the base-10 logarithm of the number.

 

logE() – returns the natural logarithm of the number.  The logarithm of a non-positive number is not a real number, so this function throws a run-time exception if the number is less than or equal to zero.

 

negate() – returns a number containing the arithmetic negative of this number.

 

radiansToDegrees() – converts the value from degrees to radians and returns the number of radians.  This simply multiplies the value by (180/pi).

 

raiseToPower(y) – computes the value of this number raised to the power y and returns the result.  If the value of the target number is negative, then y must be an integer: if x < 0, we can rewrite xy as (-1) y (-x) y, and we know that –x > 0 because x < 0.  The result of raising –1 to a non-integer exponent cannot be represented as a real number, hence this function throws an error if the target number is negative.  Note also that raising zero to any power yields 1, and raising any value to the power 0 yields 1, but the special case of 00 is mathematically undefined, so the function throws an error for this case.

 

roundToDecimal(places) – returns a number rounded to the given number of digits after the decimal point.  The new number has the same precision as this number, but all of the digits after the given number of places after the decimal point will be set to zero, and the last surviving digit will be rounded.  If places is zero, this simply rounds the number to an integer.  If places is less than zero, this rounds the number to a power of ten: roundToDecimal(-1) rounds to the nearest multiple of ten, roundToDecimal(-2) rounds to the nearest multiple of 100, and so on.  Note that the precision of the result is the same as the precision of the original value; rounding merely affects the value, not the stored precision.

 

scaleTen(x) – returns a new number containing the value of this number scaled by 10x.  If x is positive, this multiplies this number's value by ten x times (so if x = 3, the result is this number's value times 1000).  If x is negative, this divides this number's value by ten x times.  (This is more efficient than explicitly multiplying by ten, because it simply adjusts the number's internal scale factor.)

 

setPrecision(digits) – returns a new number with the same value as this number but with the specified number of digits of precision.  If digits is higher than this number's precision, the new value is simply extended with zeroes in the added trailing digits.  If digits is lower than this number's precision, the value is rounded to the given number of digits of precision.

 

sine() – computes the trigonometric sine of the number (interpreted as a radian value) and returns the result.

 

Note that the input value must be expressed in radians.  If you are working in degrees, you can convert to radians by multiplying your degree values by (pi/180), since 180 degress equals pi radians.  For convenience, you can use the degreesToRadians() function to perform this conversion.

 

Note also that this remainder calculation's precision is limited by the precision of the original number itself, so a very large number with insufficient precision to represent at least a few digits after the decimal point (1.234e27, for example) will encounter a possibly significant amount of rounding error, which will affect the accuracy of the result.  This should almost never be a problem in practice, because there is usually little reason to compute angle values outside of plus or minus a few times pi, but users should keep this in mind if they are using very large numbers and the trigonometric functions yield unexpected or inaccurate results.

 

sinh() – computes the hyperbolic sine of the number and returns the result.

 

sqrt() – returns the square root of the number.  If the number is negative, this function throws a run-time exception.

 

tangent() – computes the trigonometric tangent of the number (interpreted as a radian value) and returns the result.  Refer to the description of sine() for notes on how the input precision affects the calculation.

 

Note that the tangent of (2n+1)*pi/2, where n is any integer, (i.e., any odd multiple of pi/2) is undefined, and that the limit approaching these values is plus or minus infinity.  The BigNumber class internally calculates the tangent as the sine divided by the cosine, and as a result it is possible to generate a divide-by-zero exception by evaluating the tangent at one of these values.  However, in most cases, because the input value cannot be exactly an odd multiple of pi/2 (because it isn't even theoretically possible to represent pi exactly with a finite number of decimal digits), the tangent will return a number with a very large absolute value.

 

tanh() – computes the hyperbolic tangent of the number and returns the result.

Precision and Scale

Each floating-point value that BigNumber represents has two important attributes apart from its value: precision and scale.  For the most part, these are internal attributes that you can ignore; however, in certain cases, it's useful to know how BigNumber uses these internally.

 

The scale of a BigNumber value is a multiplier that determines how large the number really is.  A BigNumber value stores a scale so that a very large or very small number can be represented compactly, without storing all of the digits that would be necessary to write out the number in decimal format.  This is the same idea as writing a number in scientific notation, which represents a number as a value between 1 and 10 multiplied by ten raised to a power; for example, we could write four hundred fifty billion as 450,000,000,000, or more compactly in scientific notation as 4.5e11 (the "e" means "times ten to the power of the number that follows", so this means "4.5 times 1011"; note that 1011 is one hundred billion).  When we write a number in scientific notation, we need only write the significant digits, and can elide the trailing zeroes of a very large number.  We can also use scientific notation to write numbers with very small absolute values, by using a negative exponent: 9.7e-9 is 9.7 times 10-9; 10-9 is 1/109, or one one-billionth.

 

The precision of a BigNumber value is simply the number of decimal digits that the value actually stores.  A number's precision determines how many distinct values it can have; the higher the precision, the more values it can store, and hence the finer the distinctions it can make between adjacent representable values.  The precision is independent of the scale; if you create a BigNumber value with only one digit of precision, it's not limited to representing the values –9 through +9, because the scale can allow it take on larger or smaller values.  So, you can represent arbitrarily large values regardless of a number's precision; however, the precision limits the number of distinct values the number can represent, so, for example, with one digit of precision, the next representable value after 8000 is 9000.

 

When you create a BigNumber value, you can explicitly assign it a precision by passing a precision specifier to the constructor.  If you don't specify a precision, BigNumber will use a default precision.  If you create a BigNumber value from an integer, the default precision is 32 digits.  If you create a BigNumber value from a string, the precision is exactly enough to store the value's significant digits.  A significant digit is a non-zero digit, or a zero that follows a non-zero digit.  Here are some examples:

 

 

When you use numbers in calculations, the result is almost every case has the same precision as the value operated upon; in the case of calculations involving two or more operands, the result has precision equal to the greatest of the precisions of the operands.  For example, if you add a number with three digits of precision to a number with eight digits of precision, the result will have eight digits of precision.  This has the desirable effect of preserving the precision of your values in arithmetic, so that the precision you choose for your input data values is carried forward throughout your calculations.  For example, consider this calculation:

 

  x = new BigNumber('3.1415');
  y = new BigNumber('0.000111');
  z = x + y;

 

The exact arithmetic value of this calculation would be 3.1416111, but this is not the value that ends up in z, because the precision of the operands limits the precision of the result.  The precision of x is 5, because it is created from a string with five significant digits.  The precision of y is 3.  The result of the addition will have a precision of 5, because that is the larger of the two input precisions.  So, the result value stored in z will be 3.1416 – the additional two digits of y are dropped, because they cannot be represented in the result value's 5 digits of precision.

 

Precision limitations are fairly intuitive when the precision lost is after the decimal point, but note that digits can also be dropped before a decimal point.  Consider this calculation:

 

  x = new BigNumber('7.25e3');
  y = new BigNumber('122');
  z = x + y;

 

The value of x is 7.25e3, or 7250; this value has three digits of precision.  The value of y also has three digits of precision.  The exact result of the calculation is 7372, but the value stored in z will be 7370: the last digit of y is dropped because the result doesn't have enough precision to represent it.

 

Note that calculations will in most cases round their result values when they must drop precision from operand values.  For example:

 

  x = new BigNumber('7.25e3');
  y = new BigNumber('127');
  z = x + y;

 

The exact result would be 7377, but the value stored in z will be 7380: the last digit of y is dropped, but the system rounds up the last digit retained because the dropped digit is 5 or higher (in this case, 7).

Sample Application: Exorcising Parasitic Users

Apart from the obvious numerical applications, the BigNumber package can be useful if your computer is taken over by a hostile, parasitic, pure-energy life form capable of transferring itself between human and computer hosts.  A quick survey of the literature will reveal that the best tactic against this type of infection is to command the computer to calculate a transcendental number (pi, for example) to the last digit.  Computers are unable to comprehend the concept of irrational numbers, since they are so very rational, so such a calculation so preoccupies the computer that the unwanted life form is unable to get any CPU time and eventually flees the computer in frustration.

 

While BigNumber doesn't provide a way of computing transcendental numbers "to the last digit," you can still consume copious amounts of CPU time by asking it to compute a value to a large finite number of digits, such as

 

    x = BigNumber.getPi(512);

 

This will consume about twenty minutes on a Pentium III at 400 MHz.  If the parasitic entity is especially tenacious, or tries to shut down life support, it might be necessary to go out to a thousand or so digits.

 

Note that this technique isn't effective against conventional computer viruses, so users should ensure that the problem can't be addressed with standard anti-virus software before attempting this solution.