Skip to content

Instantly share code, notes, and snippets.

@Crindigo
Created June 19, 2011 01:09
Show Gist options
  • Save Crindigo/1033648 to your computer and use it in GitHub Desktop.
Save Crindigo/1033648 to your computer and use it in GitHub Desktop.
Arbitrary-precision expression calculation w/ PHP & BCMath
<?php
/**
* bccalc
*
* @author Steven Harris
* @license MIT
*/
/**
* Calculates the result of the given mathematical expression, using bcmath
* for arbitrary-precision calculation.
*
* @staticvar array $binaryOps List of operators that require two operands.
*
* @param string $expr Expression in infix format. Supports +, -, *, /, %, ^, >>, <<.
* @param int $scale bcmath scale factor.
* @return string
*/
function bccalc($expr, $scale = null)
{
static $binaryOps = array('+', '-', '*', '/', '%', '^', '>>', '<<');
$pieces = _bccalc_infixToPostfix($expr);
$stack = array();
foreach ( $pieces as $p ) {
if ( is_numeric($p) ) {
if ( $p[0] == '0' ) {
if ( $p[1] == 'x' || $p[1] == 'X' ) {
$p = _bccalc_hexdec(substr($p, 2));
} else if ( preg_match('/^0[0-7]+$/', $p) ) {
$p = _bccalc_octdec(substr($p, 1));
}
}
$stack[] = $p;
continue;
}
if ( in_array($p, $binaryOps) ) {
if ( count($stack) < 2 ) {
return false;
}
$v2 = array_pop($stack);
$v1 = array_pop($stack);
if ( $p == '+' ) {
$stack[] = $scale === null ? bcadd($v1, $v2) : bcadd($v1, $v2, $scale);
} else if ( $p == '-' ) {
$stack[] = $scale === null ? bcsub($v1, $v2) : bcsub($v1, $v2, $scale);
} else if ( $p == '*' ) {
$stack[] = $scale === null ? bcmul($v1, $v2) : bcmul($v1, $v2, $scale);
} else if ( $p == '/' ) {
$stack[] = $scale === null ? bcdiv($v1, $v2) : bcdiv($v1, $v2, $scale);
} else if ( $p == '%' ) {
$stack[] = bcmod($v1, $v2);
} else if ( $p == '^' ) {
$stack[] = $scale === null ? bcpow($v1, $v2) : bcpow($v1, $v2, $scale);
} else if ( $p == '>>' ) {
$stack[] = bcdiv($v1, bcpow('2', $v2, 0), 0);
} else if ( $p == '<<' ) {
$stack[] = bcmul($v1, bcpow('2', $v2, 0), 0);
}
}
}
return $stack[0];
}
/**
* Translates an arbitrary-precision hexadecimal into a decimal.
*
* @staticvar array $hdmap Quick lookup table for a-f hex digits.
*
* @param string $hex
* @return string
*/
function _bccalc_hexdec($hex)
{
static $hdmap =
array('a' => '10', 'b' => '11', 'c' => '12', 'd' => '13', 'e' => '14', 'f' => '15');
$hex = strtolower($hex);
$len = strlen($hex);
$res = '0';
$mult = '1';
for ( $i = $len - 1; $i >= 0; $i-- ) {
$c = $hex[$i];
$val = isset($hdmap[$c]) ? $hdmap[$c] : $c;
$res = bcadd($res, bcmul($val, $mult, 0), 0);
$mult = bcmul($mult, '16', 0);
}
return $res;
}
/**
* Translates an arbitrary-precision octal into a decimal.
*
* @param string $oct
* @return string
*/
function _bccalc_octdec($oct)
{
$len = strlen($oct);
$res = '0';
$mult = '1';
for ( $i = $len - 1; $i >= 0; $i-- ) {
$res = bcadd($res, bcmul($oct[$i], $mult, 0), 0);
$mult = bcmul($mult, '8', 0);
}
return $res;
}
/**
* Translates an infix expression to a postfix expression.
*
* @staticvar array $operators List of valid operators.
* @staticvar array $leftAssoc List of operators with left association.
* @staticvar array $rightAssoc List of operators with right association.
* @staticvar array $prec List of operator precedences.
*
* @param string $expr
* @return array Tokens in postfix format.
*/
function _bccalc_infixToPostfix($expr)
{
static $operators = array('+', '-', '*', '/', '%', '^', '>>', '<<');
static $leftAssoc = array('+', '-', '*', '/', '%', '>>', '<<');
static $rightAssoc = array('^');
static $prec = array(
'+' => 2,
'-' => 2,
'*' => 3,
'/' => 3,
'%' => 3,
'^' => 4,
'>>' => 1,
'<<' => 1,
);
$tokens = _bccalc_tokenize($expr);
$output = array();
$opStack = array();
foreach ( $tokens as $tok ) {
$tok = trim($tok);
if ( empty($tok) ) {
continue;
}
if ( is_numeric($tok) ) {
$output[] = $tok;
} else if ( in_array($tok, $operators) ) {
while ( count($opStack) ) {
$top = end($opStack);
if ( !in_array($top, $operators) ) {
break;
}
if ( (in_array($tok, $leftAssoc) && $prec[$tok] <= $prec[$top])
|| (in_array($tok, $rightAssoc) && $prec[$tok] < $prec[$top]) ) {
array_pop($opStack);
$output[] = $top;
} else {
break;
}
}
$opStack[] = $tok;
} else if ( $tok == '(' ) {
$opStack[] = $tok;
} else if ( $tok == ')' ) {
while ( ($op = array_pop($opStack)) !== null ) {
if ( $op != '(' ) {
$output[] = $op;
} else {
break;
}
}
}
}
while ( ($op = array_pop($opStack)) !== null ) {
$output[] = $op;
}
return $output;
}
/**
* Tokenizes an infix expression into its component parts. It really sucks, if you're
* a 10-year-old child that can do it better, feel free to fork this and laugh at me.
*
* @param string $expr
* @return array
*/
function _bccalc_tokenize($expr)
{
$len = strlen($expr);
$tokens = array();
$tidx = 0;
$state = 'ws'; // ws, op, paren, num
$is_hex = false;
for ( $i = 0; $i < $len; $i++ ) {
$c = $expr[$i];
if ( !isset($tokens[$tidx]) ) {
$tokens[$tidx] = '';
}
if ( $c == '(' || $c == ')' ) {
$tidx++;
$tokens[$tidx] = $c;
$state = 'ws';
continue;
}
if ( $state == 'ws' ) { // white space
if ( ctype_space($c) ) {
continue;
} else if ( ctype_digit($c) || $c == '.' || ($c == '-' && (ctype_digit($expr[$i+1]) || $expr[$i+1] == '.')) ) {
$tidx++;
$tokens[$tidx] = $c;
$state = 'num';
} else {
$tidx++;
$tokens[$tidx] = $c;
$state = 'op';
}
} else if ( $state == 'num' ) { // in a number
if ( ctype_space($c) ) {
$tidx++;
$is_hex = false;
$state = 'ws';
} else if ( ctype_digit($c) || $c == '.' ) {
$tokens[$tidx] .= $c;
} else {
// check hexadecimal
if ( $tokens[$tidx] == '0' && ($c == 'x' || $c == 'X') ) {
$tokens[$tidx] .= $c;
$is_hex = true;
} else if ( $is_hex && ctype_xdigit($c) ) {
$tokens[$tidx] .= $c;
} else {
$tidx++;
$i--;
$state = 'op';
}
}
} else if ( $state == 'op' ) { // in an op
if ( ctype_space($c) ) {
$tidx++;
$state = 'ws';
} else if ( ctype_digit($c) || $c == '.' ) {
$tidx++;
$i--;
$state = 'num';
} else {
$tokens[$tidx] .= $c;
}
}
}
for ( $i = count($tokens) - 1; $i >= 0; $i-- ) {
if ( trim($tokens[$i]) == '' ) {
unset($tokens[$i]);
}
}
return array_values($tokens);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment