Skip to content

Instantly share code, notes, and snippets.

@m3m0r7
Last active February 7, 2024 08:47
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m3m0r7/a5a5af3eb8d118fae78f6a9e18657ec4 to your computer and use it in GitHub Desktop.
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
<?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