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.
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.
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
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.
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 :-
There are six rounding modes, identified by constants in the Round
class, as follows :-
UP
- round away from zero.DOWN
- round towards zero (ie, simple truncation).CEILING
- round towards positive infinity.FLOOR
- round towards negative infinity.HALF_UP
- conventional rounding, towards nearest neighbour, with halves rounded up.HALF_DOWN
- like HALF_UP
, but round halves down.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 +-------+-------+-------+-------+
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).
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"
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
.
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
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.