Created
June 19, 2011 01:09
-
-
Save Crindigo/1033648 to your computer and use it in GitHub Desktop.
Arbitrary-precision expression calculation w/ PHP & BCMath
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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