Contents

The numbers package

Introduction

The numbers package contains some classes and procedures for doing high-precision decimal arithmetic. Classes are provided for both decimal and rational numbers, as well as a simple complex number class.

Dec and Rat

The class numbers.Dec represents an arbitrary decimal number. Internally it stores a number as an integer multiplied by an exponent, so that the number represented is :-

   i * 10^e

The integer is always “normalized”, so that its least significant digit is never zero (unless the number itself is zero). So for example, 2000 is stored as 2 * 10^3, not 20 * 10^2 or 2000 * 10^0 or anything else. This means that two instances representing the same number will always have the same internal representation.

The numbers.Rat class represents a rational number. Like Dec, numbers are stored in a normalized form; in this case the fraction is reduced to its lowest terms, so that 25/10 say is stored as 5/2.

Rat and Dec both have helpful constructors which make constructing instances convenient. Dec accepts two integers representing integer and exponent :-

  Dec(123, -1)      # 12.3

Dec also accepts various single-parameter cases :-

  Dec(123)          # Integer converted to 123
  Dec(123.456)      # Real converted (via string) to 123.456 exactly.
  Dec("123e500")    # String converted to 1.23e+502
  Dec("1/4")        # String converted (via Rat) to 0.25
  Dec("1/3")        # Fails (see below)
  Dec(Dec(100))     # Creates copy of other instance
  Dec(Rat(1,10))    # Converts to 0.1
  Dec(Rat(1,3))     # Fails
  Dec("nonsense")   # Fails
  Dec([])           # Runtime error

Two of the above require further explanation. Firstly, the conversion of the real number 123.456. This will in fact first be converted to a string “123.456” using the standard icon string() function. Then that string is parsed into a Dec instance. This avoids any unpleasant problems concerning inaccurate binary representation of fractions. However, it does mean that only ten significant figures of the real number are converted (this being the precision used by the string() function).

Secondly, note that the constructor fails in the case of Dec("1/3"). This is because of course 1/3 cannot be represented as a finite decimal. (The Rat class does in fact provides a method, numbers.Rat.decimal(), to convert to Dec with a desired rounding).

The Rat constructor is also flexible. It also has a basic two-parameter form :-

  Rat(60,1024)      # 60/1024, normalized to 15/256

and a flexible one-parameter form :-

  Rat(123)         # 123/1
  Rat(123.456)     # 15432/125
  Rat("1 2/3")     # String parsed to 1 2/3 (ie 5/3)
  Rat("123e500")   # 12300....000/1 - a very big numerator
  Rat(Dec(123))    # 123/1
  Rat(Rat(1,3))    # Creates copy of other instance
  Rat("nonsense")  # Fails
  Rat([])          # Runtime error

Note that whereas not every Rat can be exactly converted to a Dec, the converse is not true :- every Dec can always be converted to a Rat. For example :-

  d := Dec("12398.32894894")
  d.rational()     # Returns Rat(12398 16447447/50000000)
  r := Rat("1324/3747")
  r.decimal()      # Fails, since it can't be converted exactly
  r.decimal(12)    # Returns Dec(0.353349346144), being r
                   # rounded to a precision of 12.

Complex numbers

The numbers.Cpx class represents a complex number. Its constructor takes two parameters, each of which should be convertible to either Rat or Dec. For example :-

  Cpx(1, 2)        # 1 + 2i.  (Both r and i are Dec)
  Cpx(".1", "2/3") # 0.1 + (2/3)i. (r is a Dec, i a Rat)
  Cpx(Rat(1,3), Rat(2, 3))  #  (1/3) + (2/3)i; both Rat

Arithmetic

Each of the three classes described above provides the basic set of four binary arithmetic operations, plus negation. For example :-

  r := Rat(1,3)
  r.add(2)        # 2 1/3
  r.sub(1)        # -2/3
  r.mul("7/4")    # 7/12
  r.div(3.5)      # 2/21
  r.neg()         # -1/3

In each binary operation, the parameter is first converted to Rat (using the constructor described above). The operation is then performed and a new Rat instance returned. Note that r is not modified. In fact, Rat and the two other classes are immutable; operations always create new instances rather than modifying the original.

The Dec class is slightly more complex, for a couple of reasons. Firstly, as noted above, only some rationals can be converted exactly to a decimal. So some conversions to Dec may fail and raise a runtime error :-

  d := Dec(123.4)
  d.add(2)        # 125.4  
  d.add("1/2")    # 123.9
  d.add("1/3")    # Runtime error: Decimal expected: Couldn't convert rational to an exact decimal

The second point is that division cannot always be performed exactly, and thus the div method of Dec takes a second optional parameter specifying how to round the result. If an integer is given, then this means round to that precision (the number of digits in the integer part) in the conventional way. (There are other options - see Rounding below). If the precision is omitted, and the division cannot be performed exactly, the the div method fails. For example :-

  d := Dec(123.4)
  d.div(2)        # Succeeds with 61.7
  d.div(3)        # Fails
  d.div(3, 6)     # Succeeds with 41.1333
  d.div(2, 2)     # Succeeds with 62

Note that the precision of the result of a division may be less than that requested, due to normalization. For example :-

  d := Dec(1111)
  d.div(37, 10)   # 30.02702703 - ten digits.
  d.div(37, 5)    # 30.027 - five digits.
  d.div(37, 6)    # 30.027 - also five digits.

What happened with the last one was that the integer and exponent of 300270 and -4 respectively were normalized to 30027 and -3.

The three other binary arithmetic methods of Dec also take a rounding parameter. These are equivalent to applying the round method to the result, so

  d := Dec(123.4)
  d.add(1.234, 4)        # 124.6
  d.add(1.234).round(4)  # ...the same

The reason for providing the round parameter to add (and sub) is that it is more efficient to combine the two operations if the two operands differ greatly in size. For example :-

  d := Dec("1.2345e60000")        # A very large number (but small precision).
  d := d.add(1)                   # Now large with a large precision; quite a slow operation
  d := d.round("10 up")           # Now Dec(1.234500001e+60000)

The add is quite slow because it involves creating a number with a very large precision (an integer part of 12345, followed by 59995 zeroes, followed by a 1). The round call then restores the number to a more manageable precision of 10 (see below for the meaning of the rounding of “10 up”).

Combining the add and round into one call speeds this up considerably, by avoiding the intermediate number :-

  d := Dec("1.2345e60000")        # A very large number (but small precision).
  d := d.add(1, "10 up")          # Now Dec(1.234500001e+60000)

The mul method also takes an optional rounding parameter, but this is just for consistency, and has no performance advantage over calling round separately.

Rounding

Dec provides several options for controlling how numbers are rounded. These are encapsulated in the numbers.Round class. Instances of this class contain three things :-

Rounding modes

There are six rounding modes, identified by constants in the Round class, as follows :-

The following diagram may help to show the differences between the first four modes. The dots represent numbers lying between 1 and -1, and the arrows show how the number would be rounded for each mode.

       UP     DOWN    CEIL-   FLOOR
                      ING  
  1 +-------+-------+-------+-------+
    |   ↑   |       |   ↑   |       |
    |   •   |   •   |   •   |   •   |
    |       |   ↓   |       |   ↓   |
 0  |-------|-------|-------|-------|
    |       |   ↑   |   ↑   |       |
    |   •   |   •   |   •   |   •   |
    |   ↓   |       |       |   ↓   |
-1  +-------+-------+-------+-------+

Specifying rounding

The Round class takes a string in its constructor to make it easy to create an instance. The following examples illustrate the form used :-

  "10"                    # Ten places of precision, mode HALF_UP (ie, conventional rounding).
  "10dp"                  # The same, to ten decimal places after the point.
  "8 down"                # Eight places of precision, mode DOWN (ie, truncate digits after the eighth place).
  "8dp hd"                # Eight decimal places, mode HALF_DOWN.

The strings recognized for the mode are :-

Note the use of a hyphen rather than an underscore in the last two.

When a Round parameter is needed, it is not necessary to actually call the constructor directly; rather a string (or integer) will be converted by the called method. The following examples illustrate this :-

  Dec(12.5).round(2)           # 13, conventional rounding to a precision of 2
  Dec(12.5).round("2 hd")      # 12, round halves down, a precision of 2
  Dec(12.501).round("2 hd")    # 13
  Dec(8.5).round("1 up")       # 9
  Dec(-8.5).round("1 up")      # -9
  Dec(123.499).round("4 t")    # 123.4, truncate (mode DOWN) at precision 4.
  Dec(123.987).round("0dp t")  # 123, truncate at 0 decimal places (ie, chop off fraction).

String formatting

Each of the three classes has a str method for formatting. These takes options similar to the util.Format.numeric_to_string() method. For example :-

  d := Dec("1234.5678")
  d.str()             # "1234.5678"
  d.str('e')          # "1.2345678e+3"
  d.str('e', 3)       # "1.235e+3"
  d.str('e', 12)      # "1.234567800000e+3"
  d.str(',+', 3)      # "+1,234.568"

Comparisons

Rat and Dec both have cmp methods which can be used to compare values; for example :-

  r1 := Rat(1, 3)
  r2 := Rat(1, 4)
  r1.cmp("=", r2)      # Fails
  r1.cmp(">", r2)      # Succeeds

cmp is implemented by evaluating the sign of the difference of the two numbers and then applying the first parameter to compare that result and zero; in other words the last line above means notionally sign(r1 - r2) > 0.

The first parameter to the cmp method can be anything convertible to a procedure. The above examples take advantage of Icon’s string to function conversion, so that

  "<"(1, 3)

has the same result as 1 < 3.

Arithmetic procedures

It may be noted that the Rat and Dec arithmetic methods are not quite symmetric in their operation. For example :-

  d := Dec(123.4)
  r := Rat(1,3)        # 1/3
  d.add(r)             # Runtime error: Decimal expected: Couldn't convert rational to an exact decimal
  r.add(d)             # Succeeds with a `Rat` 123 11/15.
  r.cmp("<", d)        # Succeeds
  d.cmp(">", r)        # ... but another runtime error

For this reason, the numbers package includes procedures which can be used instead of the instance methods to perform arithmetic operations. For example :-

  d := Dec(123.4)
  r := Rat(1,3)
  add(d, r)            # Succeeds with a `Rat` 123 11/15.
  add(r, d)            # ... the same
  cmp(r, "<", d)       # Succeeds
  cmp(d, ">", r)       # ... the same

The div procedure is also more flexible than the div methods. With no rounding specified, it will always give a precise result. For example :-

  d := Dec(123.4)
  div(d, 2)           # Succeeds with a `Dec` 61.7
  div(d, 3)           # Succeeds with a `Rat` 41 2/15
  div(d, d3, 6)       # Succeeds with a `Dec` 41.1333

Contrast the second case with the d.div(3) which would fail. By applying sensible conversions, div returns an exact result as a rational.

The arithmetic procedures are also used by the instance methods of the Cpx class, to give it more flexibility. For example :-

  c1 := Cpx("1/3", "3/7")     # (1/3) + (3/7)i
  c2 := Cpx(12.34, 11.9)      # 12.34 + 11.9i
  c1.div(c2)                  # 34550/1102071 + (34700/7714497)i
  c1.div(c2, 6)               # 0.0313501 + 0.00449802i

In the last case, the rounding parameter (6) makes div to convert the rational results to decimals with that precision.

Another difference between the arithmetic procedures and the methods is that the procedures will carry out a very basic simplification on the result. Complex numbers with a zero i are simplified into their real part, and rational numbers with a denominator of 1 are simplified into a Dec of their numerator. So for example, contrast :-

   r := Rat(1, 2)
   r.add(r)          # A `Rat`, 1/1
   add(r, r)         # A `Dec`, 1

   c := Cpx(0, 1)    # Just i
   c.mul(c)          # A `Cpx`, -1 + 0i
   mul(c, c)         # A `Dec`, -1

BigMath

The class ipl.numbers.BigMath provides static methods to evaluate common mathematical functions using Dec numbers. For example,

  BigMath.sin(1.25, 25)   # sin to 25 digits of precision - Dec(0.9489846193555862143484908)
  BigMath.pi(40)          # pi to 40 digits - Dec(3.141592653589793238462643383279502884197)
  BigMath.root("1.237747e670", 100, 60)   # 100th root of a very big number.

The precision specified is in fact just a Round parameter, so you could if you wish round the result in any of the ways described above.

Contents