Cinip: big numbers, arbitrary precision, conveniently
Introduction
PHP has an extension called BCMath, which allows the use of numbers of arbitrary size, with a configurable number of digits after the decimal point. However, it can force the programmer to use expressions which are painful to write and difficult to read.
Imagine that you need to compute the following expression:
Wouldn't it be nice if you could use a more readable, more PHP-like notation? Well, now you can, and this page describes how.
The necessary software is called Cinip, and its usage is very simple. (The name is an acronym for “Calculate Immense Numbers in PHP”, and I pronounce it as “SINN-ipp”.) Currently at version 1.1.0, it is effectively in the public domain, and available for download in tgz and zip formats. It requires PHP 5.3.0 or later.
The remainder of this document explains how to use the software; in what follows, a knowledge of BCMath is helpful, but not required.
A simple example
To solve the problem just described, we might use something like the following code. It performs the calculation given above and assigns its result to $x.
A longer example
The following is a complete program (which doesn't show Cinip to best advantage, since the arithmetic expressions are not very complicated). Note that the scale (the first argument to the static method get_func) is always zero, since we are dealing only with integers.
API
All of Cinip is contained in a single file (called cinip.php, but you can rename it if you like), which should be loaded with require_once or include_once. Everything in this file is defined in the namespace \kingfisher\cinip.
Among the things defined are two classes called parser and CinipException, and an interface called Exception. These three things constitute the API, and everything else should be considered private.
In a moment we will look at the methods of the class parser, but first we need to consider the static method get_func and its relationship to two other methods of parser.
get_func allows one to eliminate some clutter. Consider the following two pieces of code, which are essentially equivalent.
⋮
$parser = new cinip\parser(x1, x2, …, xm);
⋮
$z = eval($parser->to_bc(y1, y2, …, yn));
⋮
$f = cinip\parser::get_func(x1, x2, …, xm);
⋮
$z = eval($f(y1, y2, …, yn));
Clearly, “eval($f(…))” is more convenient to read and write than “eval($parser->to_bc(…))”.
Methods of the class parser
- to_bc($arithmetic_expression)
-
Parses $arithmetic_expression, transforms it into a new expression which uses BCMath functions, and returns this new expression. (The syntax rules for $arithmetic_expression are given below.)
Here is a small program which illustrates the use of to_bc.
<?phprequire_once "cinip.php";use kingfisher\cinip;$parser = new cinip\parser;$x = 1;$y = 2;$expressions = array('1 + 2','$x + $y','pow(PHP_INT_MAX, 20) / log(33, 10)','1e200 / 3e400',);foreach ($expressions as $e) {$new_expr = $parser->to_bc($e);$result = eval($new_expr);print"Original expression: $e\n" ."Transformed expression: " ."$new_expr\n" ."Result: $result\n\n";} - __construct( [ $scale = 60 [ , $perf_mode = parser::memoize ] ] )
-
$scale (which is analogous to PHP's bcmath.scale setting) is passed by Cinip to all BCMath functions which take a scale parameter (that is, all functions except bcmod). The scale can't be changed after an object has been created; if you need more precision, just create another parser object.
If you will be working only with integers, then setting $scale to 0 will make your calculations faster.
A feature of BCMath is worth highlighting: it appears that the scale is used, not just for the final result of a calculation, but also for the intermediate results. This can result in truncation, as shown by the following program.
<?php$result = bcmul('.13', '.13', 3);/*the correct answer,mathematically speaking,is 0.0169*/$result = ltrim($result, '0');if($result === '.016') {print "Truncation occurred.\n";} elseif($result === '.017') {print "Rounding occurred.\n";} else {print "Something completely " ."unexpected occurred.\n";}Note that bcmath.scale has no effect on Cinip.
$perf_mode can be one of two class constants, parser::memoize or parser::no_enhancement. The former tells the object to memoize the method to_bc (which is described below), the latter tells it not to.
Normally you should use parser::memoize, but if each of your expressions is evaluated only once, or you have a large number of distinct expressions and are worried about high memory usage, then it may be better to use parser::no_enhancement.
- get_func( [ $scale = 60 [ , $perf_mode = parser::memoize ] ] )
-
A static method. Has been fully described above.
- get_scale()
-
Returns the scale being used.
- get_perf_mode()
-
Returns the performance mode being used (one of parser::memoize or parser::no_enhancement).
Syntax rules for expressions
The syntax of expressions is a subset of that of PHP expressions. The following code gives a good overview of what may appear in a Cinip expression; it generates a valid expression and stores it in the variable $expression.
The following arithmetic operators are permitted:
They are implemented using the corresponding BCMath functions. (Note that, just as in PHP, “+” and “-” can be used as binary or as unary operators.)
The following comparison operators are permitted:
Nesting to arbitrary depth is, of course, permitted:
Any function can be used, but two functions are treated specially, namely sqrt and pow.
sqrt will be changed to bcsqrt. For pow, a run-time check will be made on the second argument; if it's an integer (that is, an integer in the mathematical sense; it need not have type integer), then bcpow will be used; otherwise pow will be used.
It's your responsibility to ensure that a function's arguments are appropriate. For example, the expression log(pow(2, 2000)) won't work, because 22000 is too large to be represented as a double-precision floating-point number.
Things that don't work
Description | Example of erroneous code |
---|---|
Using the -> operator to reference properties or methods of an object. |
$f = cinip\parser::get_func();
/*
Each of these 2
lines contains an
invalid expression
*/
$x = eval($f('$o->a'));
$x = eval($f('$o->f()'));
|
Referencing array elements. |
$a = range(1, 9);
$f = cinip\parser::get_func();
$x = eval($f('$a[0]'));
|
Class constants (but you can use PHP's constant function). |
<?php
require_once "cinip.php";
use kingfisher\cinip;
class C {
const N = 100;
}
$f = cinip\parser::get_func();
$x = eval($f('C::N'));
|
Certain kinds of interpolation into double-quoted strings. |
$f = cinip\parser::get_func();
$a = array();
$a["b"]["c"] = 42;
$x = eval($f('100 + "{$a["b"]["c"]}"'));
|
A name qualified with a namespace. |
$f = cinip\parser::get_func();
# both the following
# Cinip expressions
# are syntactically
# invalid
$y = eval($f('100 + xyz\\g()'));
$y = eval($f('100 + namespace\\g()'));
|
More on interpolation
Interpolation into double-quoted strings requires some explication. Cinip's idea of a double-quoted string is much simpler than PHP's, and can be explained using the following code.
Because of this difference, Cinip and PHP sometimes differ on where a double-quoted string ends. The result is that string interpolation will work if you use the simple syntax, but will not work for some uses of the complex syntax (these terms are from the PHP documentation).
The following program (expanded from a code snippet above) illustrates.
Interpolation is always done by PHP, not Cinip, with the time of interpolation being determined by the quoting used. The following program illustrates.