Last active
February 7, 2024 08:47
-
-
Save m3m0r7/a5a5af3eb8d118fae78f6a9e18657ec4 to your computer and use it in GitHub Desktop.
A calculator can calculates a sqrt function, factorial, power and usage big number calculation from user input that is written in PHP
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 | |
function dt($v) | |
{ | |
foreach ($v as $t) { | |
echo $t->value(); | |
} | |
echo "\n"; | |
} | |
interface OperatorCalculationInterface | |
{ | |
public static function name(): string; | |
public static function apply(BasicNumber $leftOperand, BasicNumber $rightOperand): BasicNumber; | |
} | |
interface CalculatorFunctionInterface | |
{ | |
public static function name(): string; | |
public static function apply(BasicNumber $number): BasicNumber; | |
public static function type(): int; | |
} | |
class BasicNumber | |
{ | |
public const USE_SCALAR = 0; | |
public const USE_BCMATH = 1; | |
protected static int $calculationType = self::USE_SCALAR; | |
protected string $number; | |
public function __construct(int|float|BasicNumber|string $number) | |
{ | |
if ($number instanceof BasicNumber) { | |
$this->number = $number->value(); | |
} elseif (is_int($number) || is_float($number)) { | |
$this->number = (string) $number; | |
} else { | |
$this->number = $number; | |
} | |
} | |
public static function setCalculationType(int $calculationType): void | |
{ | |
static::$calculationType = $calculationType; | |
} | |
public static function setScale(int $scale): void | |
{ | |
if (static::$calculationType === static::USE_BCMATH) { | |
bcscale($scale); | |
} | |
} | |
public function value(): string | |
{ | |
return preg_replace( | |
'/(\.\d+)0*/', | |
'$1', | |
preg_replace('/\.0+$/', '', (string) $this->number) | |
); | |
} | |
public function toInt(): int | |
{ | |
return (int) $this->number; | |
} | |
public function toIntOrFloat(): int|float | |
{ | |
if (str_contains($this->number, '.')) { | |
return (float) $this->number; | |
} | |
return (int) $this->number; | |
} | |
public function sqrt(): self | |
{ | |
if (static::$calculationType === static::USE_SCALAR) { | |
return new BasicNumber(sqrt($this->toIntOrFloat())); | |
} | |
return new BasicNumber(bcsqrt($this->number)); | |
} | |
public function factorial(BasicNumber $number): self | |
{ | |
return new BasicNumber(array_reduce( | |
$number->toInt() >= 2 | |
? range(2, $number->toInt()) | |
: [], | |
fn (string $factorial, int $value) => (new BasicNumber($factorial)) | |
->mul(new BasicNumber($value)), | |
'1', | |
)); | |
} | |
public function mul(BasicNumber $number): self | |
{ | |
if (static::$calculationType === static::USE_SCALAR) { | |
return new BasicNumber($this->toIntOrFloat() * $number->toIntOrFloat()); | |
} | |
return new BasicNumber( | |
bcmul( | |
$this->number, | |
$number->value() | |
) | |
); | |
} | |
public function add(BasicNumber $number): self | |
{ | |
if (static::$calculationType === static::USE_SCALAR) { | |
return new BasicNumber($this->toIntOrFloat() + $number->toIntOrFloat()); | |
} | |
return new BasicNumber( | |
bcadd( | |
$this->number, | |
$number->value() | |
) | |
); | |
} | |
public function sub(BasicNumber $number): self | |
{ | |
if (static::$calculationType === static::USE_SCALAR) { | |
return new BasicNumber($this->toIntOrFloat() - $number->toIntOrFloat()); | |
} | |
return new BasicNumber( | |
bcsub( | |
$this->number, | |
$number->value() | |
) | |
); | |
} | |
public function div(BasicNumber $number): self | |
{ | |
if (static::$calculationType === static::USE_SCALAR) { | |
return new BasicNumber($this->toIntOrFloat() / $number->toIntOrFloat()); | |
} | |
return new BasicNumber( | |
bcdiv( | |
$this->number, | |
$number->value() | |
) | |
); | |
} | |
public function exp(BasicNumber $number): self | |
{ | |
if (static::$calculationType === static::USE_SCALAR) { | |
return new BasicNumber($this->toIntOrFloat() ** $number->toIntOrFloat()); | |
} | |
return new BasicNumber( | |
bcpow( | |
$this->number, | |
$number->value() | |
) | |
); | |
} | |
public function __toString(): string | |
{ | |
return $this->value(); | |
} | |
} | |
class SqrtFunction implements CalculatorFunctionInterface | |
{ | |
public static function name(): string | |
{ | |
return 'sqrt'; | |
} | |
public static function type(): int | |
{ | |
return Token::FN_R; | |
} | |
public static function apply(BasicNumber $number): BasicNumber | |
{ | |
return $number->sqrt(); | |
} | |
} | |
class FactorialFunction implements CalculatorFunctionInterface | |
{ | |
public static function name(): string | |
{ | |
return '!'; | |
} | |
public static function type(): int | |
{ | |
return Token::FN_L; | |
} | |
public static function apply(BasicNumber $number): BasicNumber | |
{ | |
return $number->factorial($number); | |
} | |
} | |
class PlusCalculator implements OperatorCalculationInterface | |
{ | |
public static function name(): string | |
{ | |
return '+'; | |
} | |
public static function apply(BasicNumber $leftOperand, BasicNumber $rightOperand): BasicNumber | |
{ | |
return $leftOperand->add($rightOperand); | |
} | |
} | |
class MinusCalculator implements OperatorCalculationInterface | |
{ | |
public static function name(): string | |
{ | |
return '-'; | |
} | |
public static function apply(BasicNumber $leftOperand, BasicNumber $rightOperand): BasicNumber | |
{ | |
return $leftOperand->sub($rightOperand); | |
} | |
} | |
class MultiplyCalculator implements OperatorCalculationInterface | |
{ | |
public static function name(): string | |
{ | |
return '*'; | |
} | |
public static function apply(BasicNumber $leftOperand, BasicNumber $rightOperand): BasicNumber | |
{ | |
return $leftOperand->mul($rightOperand); | |
} | |
} | |
class DivideCalculator implements OperatorCalculationInterface | |
{ | |
public static function name(): string | |
{ | |
return '/'; | |
} | |
public static function apply(BasicNumber $leftOperand, BasicNumber $rightOperand): BasicNumber | |
{ | |
return $leftOperand->div($rightOperand); | |
} | |
} | |
class ExponentiationCalculator implements OperatorCalculationInterface | |
{ | |
public static function name(): string | |
{ | |
return '**'; | |
} | |
public static function apply(BasicNumber $leftOperand, BasicNumber $rightOperand): BasicNumber | |
{ | |
return $leftOperand->exp($rightOperand); | |
} | |
} | |
trait ExceptionCreatable | |
{ | |
public static function fromException(string $message, string ...$params): self | |
{ | |
return new static(sprintf($message, ...$params)); | |
} | |
} | |
class TokenizeExceptions extends RuntimeException | |
{ | |
use ExceptionCreatable; | |
} | |
class CalculatorRuntimeExceptions extends RuntimeException | |
{ | |
use ExceptionCreatable; | |
} | |
class ASTExceptions extends RuntimeException | |
{ | |
use ExceptionCreatable; | |
} | |
class TokenToNodeException extends ASTExceptions | |
{ | |
} | |
interface Node | |
{ | |
} | |
class NumNode implements Node | |
{ | |
protected BasicNumber $value; | |
public function __construct(protected Token $token) | |
{ | |
$this->value = new BasicNumber($this->token->value()); | |
} | |
public function value(): BasicNumber | |
{ | |
return $this->value; | |
} | |
} | |
class FnNode implements Node | |
{ | |
public function __construct(protected Token $fnToken, protected Node $node) | |
{ | |
} | |
public function funcName(): string | |
{ | |
return $this->fnToken->value(); | |
} | |
public function node(): Node | |
{ | |
return $this->node; | |
} | |
} | |
class OpNode implements Node | |
{ | |
public function __construct(protected Token $opToken, protected Node $leftOperand, protected Node $rightOperand) | |
{ | |
} | |
public function operator(): string | |
{ | |
return $this->opToken->value(); | |
} | |
public function left(): Node | |
{ | |
return $this->leftOperand; | |
} | |
public function right(): Node | |
{ | |
return $this->rightOperand; | |
} | |
} | |
class Token | |
{ | |
public const NUM = 1; | |
public const FN_L = 2; | |
public const FN_R = 4; | |
public const OP = 8; | |
public const P_OP = 16; | |
public const P_CL = 32; | |
public const UNKNOWN = -1; | |
protected string $name; | |
public function __construct(protected int $token, protected string $value) | |
{ | |
$this->name = (string) array_reduce( | |
array_keys($constants = (new ReflectionClass($this))->getConstants()), | |
fn (?string $current, string $keyName) => $constants[$keyName] === $token ? $keyName : $current, | |
'' | |
); | |
} | |
public function token(): int | |
{ | |
return $this->token; | |
} | |
public function value(): string | |
{ | |
return $this->value; | |
} | |
} | |
interface FilterInterface | |
{ | |
public function __invoke(array $tokens): array; | |
} | |
interface LexerInterface | |
{ | |
public function toTokens(): array; | |
} | |
class Lexer implements LexerInterface | |
{ | |
public function __construct(protected string $expression, protected array $calculators = [], protected array $functions = []) | |
{ | |
// Sort an order by descending | |
usort( | |
$this->calculators, | |
fn (string $a, string $b) => strlen($b) - strlen($a) | |
); | |
$this->expression = str_replace(' ', '', $this->expression); | |
} | |
public function toTokens(): array | |
{ | |
$tokens = []; | |
$len = strlen($this->expression); | |
// Tokenize number and operators (OP, NUM) | |
for ($i = 0; $i < $len; $i++) { | |
$res = $char = $this->expression[$i]; | |
if (ctype_digit($char) || ($char === '-' && ctype_digit($this->expression[$i + 1] ?? null))) { | |
$isStartedWithMinus = $char === '-'; | |
$floatedCounter = 0; | |
for ($i++; $i < $len && (ctype_digit($this->expression[$i]) || $this->expression[$i] === '.'); $i++) { | |
$res .= $this->expression[$i]; | |
if ($this->expression[$i] === '.') { | |
$floatedCounter++; | |
if (!ctype_digit($this->expression[$i - 1] ?? null) || $floatedCounter > 1) { | |
throw TokenizeExceptions::fromException( | |
'Found an illegal float value' | |
); | |
} | |
} | |
} | |
$i--; | |
$tokens[] = new Token(Token::NUM, $res); | |
} elseif ($char === '(') { | |
$tokens[] = new Token(Token::P_OP, $res); | |
} elseif ($char === ')') { | |
$tokens[] = new Token(Token::P_CL, $res); | |
} else { | |
$token = null; | |
foreach ($this->calculators as $calculatorName) { | |
if ($calculatorName === $char . substr($this->expression, $i + 1, strlen($calculatorName) - 1)) { | |
$token = new Token(Token::OP, $calculatorName); | |
$i += strlen($calculatorName) - 1; | |
break; | |
} | |
} | |
if ($token !== null) { | |
$tokens[] = $token; | |
} elseif ($char !== ' ') { | |
// No add space | |
$tokens[] = $res; | |
} | |
} | |
} | |
// Tokenize functions (FN_L, FN_R) | |
$newTokens = []; | |
$len = count($tokens); | |
for ($i = 0; $i < $len; $i++) { | |
$token = $tokens[$i]; | |
if ($token instanceof Token) { | |
$newTokens[] = $token; | |
continue; | |
} | |
for ($i++; $i < $len && !($tokens[$i] instanceof Token); $i++) { | |
$token .= $tokens[$i]; | |
} | |
$i--; | |
if (isset($this->functions[$token])) { | |
$newTokens[] = new Token( | |
$this->functions[$token], | |
$token | |
); | |
} else { | |
throw TokenizeExceptions::fromException( | |
'Failed tokenization because found an illegal token: %s', | |
$token | |
); | |
} | |
} | |
return $newTokens; | |
} | |
} | |
class AST | |
{ | |
protected Node $applyFunctionNode; | |
public function __construct(protected LexerInterface $lexer) | |
{} | |
public function toAST(array $applyFilters = []): array|Node | |
{ | |
return current( | |
$this->tokenToNode( | |
array_reduce( | |
$applyFilters, | |
fn (array $tokens, FilterInterface $filterClass) => $filterClass($tokens), | |
$this->lexer->toTokens(), | |
) | |
) | |
); | |
} | |
protected function tokenToNode(array $tokens): array|Node | |
{ | |
$nodes = []; | |
$len = count($tokens); | |
for ($i = 0; $i < $len; $i++) { | |
$token = $tokens[$i]; | |
switch ($token->token()) { | |
case Token::P_OP: | |
$values = []; | |
$parenthesesStacks = 1; | |
for ($i++; $i < $len; $i++) { | |
if ($tokens[$i]->token() === Token::P_OP) { | |
$parenthesesStacks++; | |
$values[] = $tokens[$i]; | |
continue; | |
} | |
if ($tokens[$i]->token() === Token::P_CL) { | |
$parenthesesStacks--; | |
if ($parenthesesStacks === 0) { | |
break; | |
} | |
} | |
$values[] = $tokens[$i]; | |
} | |
if ($parenthesesStacks !== 0) { | |
throw TokenToNodeException::fromException( | |
'The parentheses (`(` and `)`) is unmatched - you must close count of %d', | |
$parenthesesStacks | |
); | |
} | |
$i--; | |
$nodes = [ | |
...$nodes, | |
...$this->tokenToNode($values) | |
]; | |
break; | |
case Token::OP: | |
$currentNode = array_pop($nodes); | |
$nodes[] = new OpNode( | |
$token, | |
$currentNode, | |
current( | |
$this->tokenToNode(array_slice($tokens, ++$i)) | |
) | |
); | |
$i = $len; | |
break; | |
case Token::FN_L: | |
$currentNode = array_pop($nodes); | |
$nodes[] = new FnNode( | |
$token, | |
$currentNode, | |
); | |
break; | |
case Token::FN_R: | |
$nextToken = $tokens[$i + 1] ?? null; | |
if ($nextToken?->token() !== Token::P_OP) { | |
throw TokenToNodeException::fromException( | |
'%s (%s) is not expected - expected token: %s (actual: %s)', | |
'FN_R', | |
$token->value(), | |
')', | |
$nextToken?->value(), | |
); | |
} | |
$targetedTokens = []; | |
$currentPos = ++$i; | |
$parentheses = 1; | |
for ($i++; $i < $len; $i++) { | |
if ($tokens[$i]->token() === Token::P_OP) { | |
$parentheses++; | |
continue; | |
} | |
if ($tokens[$i]->token() === Token::P_CL) { | |
$parentheses--; | |
if ($parentheses === 0) { | |
$i++; | |
break; | |
} | |
continue; | |
} | |
} | |
$i--; | |
$nodes[] = new FnNode( | |
$token, | |
current( | |
$this->tokenToNode( | |
array_slice($tokens, $currentPos, $i - $currentPos + 1) | |
) | |
) | |
); | |
break; | |
case Token::NUM: | |
$nodes[] = new NumNode($tokens[$i]); | |
break; | |
} | |
} | |
return $nodes; | |
} | |
} | |
trait FactorMergeable | |
{ | |
public function merge(array $tokens, int $currentPos): array | |
{ | |
for ($parentheses = 0, $k = $currentPos - 1; $k > 0; $k--) { | |
if ($tokens[$k]->token() === Token::P_CL) { | |
$parentheses++; | |
continue; | |
} elseif ($tokens[$k]->token() === Token::P_OP) { | |
$parentheses--; | |
if ($parentheses === 0) { | |
break; | |
} | |
continue; | |
} elseif ($parentheses === 0) { | |
break; | |
} | |
} | |
for ($parentheses = 0, $j = $currentPos + 1; $j < count($tokens); $j++) { | |
if ($tokens[$j]->token() === Token::P_OP) { | |
$parentheses++; | |
continue; | |
} | |
if ($tokens[$j]->token() === Token::P_CL) { | |
$parentheses--; | |
if ($parentheses === 0) { | |
break; | |
} | |
continue; | |
} elseif ($tokens[$j]->token() === Token::FN_L || $tokens[$j]->token() === Token::FN_R) { | |
// Skip applying function by this case | |
continue; | |
} elseif ($parentheses === 0) { | |
break; | |
} | |
} | |
return [$k, $j]; | |
} | |
} | |
class FilterHigherPriorityOperator implements FilterInterface | |
{ | |
use FactorMergeable; | |
public function __construct(protected array $operators = []) | |
{ | |
} | |
public function __invoke(array $tokens): array | |
{ | |
for ($i = 0; $i < count($tokens); $i++) { | |
$token = $tokens[$i]; | |
if (in_array($token->value(), $this->operators, true)) { | |
[$start, $end] = $this->merge($tokens, $i); | |
$tokens = [ | |
...($appendedTokens = [ | |
...array_slice($tokens, 0, $start), | |
new Token(Token::P_OP, '('), | |
...array_slice($tokens, $start, ($end - $start) + 1), | |
new Token(Token::P_CL, ')'), | |
]), | |
...array_slice($tokens, $end + 1), | |
]; | |
$i = count($appendedTokens); | |
} | |
} | |
return $tokens; | |
} | |
} | |
class FilterRightMergeOperator implements FilterInterface | |
{ | |
use FactorMergeable; | |
public function __construct(protected array $operators = []) | |
{ | |
} | |
public function __invoke(array $tokens): array | |
{ | |
for ($i = count($tokens) - 1; $i >= 0; $i--) { | |
$token = $tokens[$i]; | |
if (in_array($token->value(), $this->operators, true)) { | |
[$start, $end] = $this->merge($tokens, $i); | |
$tokens = [ | |
...array_slice($tokens, 0, $start), | |
...($appendedTokens = [ | |
new Token(Token::P_OP, '('), | |
...array_slice($tokens, $start, ($end - $start) + 1), | |
new Token(Token::P_CL, ')'), | |
...array_slice($tokens, $end + 1) | |
]), | |
]; | |
$i = count($tokens) - count($appendedTokens); | |
} | |
} | |
return $tokens; | |
} | |
} | |
class CalculatorRuntime | |
{ | |
public const SUCCESS = 0; | |
public const FAILED = 1; | |
protected static array $calculators = []; | |
protected static array $functions = []; | |
public function __construct(protected $callable) | |
{ | |
} | |
public static function registerCalculator(string $className): void | |
{ | |
static::$calculators[$className::name()] = [$className, fn (BasicNumber $leftOperand, BasicNumber $rightOperand) => $className::apply($leftOperand, $rightOperand)]; | |
} | |
public static function registerFunction(string $className): void | |
{ | |
static::$functions[$className::name()] = [$className, fn (BasicNumber $value) => $className::apply($value)]; | |
} | |
public static function getRegisteredCalculators(): array | |
{ | |
return array_keys(static::$calculators); | |
} | |
public static function getTypesOnRegisteredFunctions(): array | |
{ | |
return array_reduce( | |
static::$functions, | |
fn (array $carry, $funcType) => [ | |
...$carry, | |
$funcType[0]::name() => $funcType[0]::type(), | |
], | |
[] | |
); | |
} | |
public function run(): int | |
{ | |
try { | |
$calculated = $this->process(($this->callable)()); | |
echo preg_replace( | |
'/(\.\d+)0*/', | |
'$1', | |
preg_replace('/\.0+$/', '', $calculated) | |
); | |
return static::SUCCESS; | |
} catch (Throwable $e) { | |
echo "{$e->getMessage()}\n\n"; | |
return $this->run(); | |
} | |
} | |
protected function process(Node $node) | |
{ | |
if ($node instanceof OpNode) { | |
$leftOperand = new BasicNumber($this->process($node->left())); | |
$rightOperand = new BasicNumber($this->process($node->right())); | |
[, $applyer] = static::$calculators[$node->operator()]; | |
return $applyer( | |
$leftOperand, | |
$rightOperand, | |
); | |
} elseif ($node instanceof FnNode) { | |
[, $applyer] = static::$functions[$node->funcName()]; | |
return $applyer( | |
new BasicNumber($this->process($node->node())), | |
); | |
} elseif ($node instanceof NumNode) { | |
return new BasicNumber($node->value()); | |
} | |
throw CalculatorRuntimeExceptions::fromException( | |
'Failed calculation because the runtime cannot process `%s` node of an expression', | |
get_class($node) | |
); | |
} | |
} | |
BasicNumber::setCalculationType(BasicNumber::USE_BCMATH); | |
BasicNumber::setScale(10); | |
CalculatorRuntime::registerCalculator(PlusCalculator::class); | |
CalculatorRuntime::registerCalculator(MinusCalculator::class); | |
CalculatorRuntime::registerCalculator(MultiplyCalculator::class); | |
CalculatorRuntime::registerCalculator(DivideCalculator::class); | |
CalculatorRuntime::registerCalculator(ExponentiationCalculator::class); | |
CalculatorRuntime::registerFunction(SqrtFunction::class); | |
CalculatorRuntime::registerFunction(FactorialFunction::class); | |
exit((new CalculatorRuntime(function () { | |
// tested: | |
// `echo 4*sqrt((5*4*3*2*1+2**8)-(4*3*2*1+2**8))+5 . "\n"`; | |
// `4*sqrt((5!+2**8)-(4!+2**8))+5` | |
echo "Enter an expression [e.g. 4*sqrt((5!+2**8)-(4!+2**8))]: "; | |
$expression = rtrim(fread(STDIN, 1024)); | |
return (new AST(new Lexer( | |
$expression, | |
CalculatorRuntime::getRegisteredCalculators(), | |
CalculatorRuntime::getTypesOnRegisteredFunctions()) | |
))->toAST([ | |
// | |
// We will get an expression as following which is an example: | |
// 1+2*3-4/5 | |
// | |
// But the expression cannot decide calculation priority. | |
// In the math, multiply and division operator to be higher priority than other operators. | |
// Thus the expression needs to transform as following: | |
// 1+(2*3)-(4/5) | |
// | |
// When you using exponential operator to be right assign. | |
// 4**3**2+1 | |
// the expression to be: | |
// (4**(3**2))+1 | |
// | |
new FilterHigherPriorityOperator([MultiplyCalculator::name(), DivideCalculator::name()]), | |
new FilterRightMergeOperator([ExponentiationCalculator::name()]), | |
]); | |
}))->run()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment