4get/oracles/calc.php

165 lines
4.4 KiB
PHP

<?php
include_once("oracles/base.php");
class calculator extends oracle {
public $info = [
"name" => "calculator"
];
public function check_query($q) {
// straight numerics should go to that oracle
if (is_numeric($q)) {
return false;
}
// all chars should be number-y or operator-y
$char_whitelist = str_split("1234567890.+-/*^%() ");
foreach (str_split($q) as $char) {
if (!in_array($char, $char_whitelist)) {
return false;
}
}
return true;
}
// a custom parser and calculator because FUCK YUO, libraries are
// gay.
public function generate_response($q)
{
$nums = str_split("1234567890.");
$ops = str_split("+-/*^%;");
$grouping = str_split("()");
$q = str_replace(" ", "", $q);
// backstop for the parser so it catches the last
// numeric token
$q .= ";";
// the following comments refer to this example input:
// 21+9*(3+2^9)+1
// 2-length lists of the following patterns:
// ["n" (umeric), <some number>]
// ["o" (perator), "<some operator>"]
// ["g" (roup explicit), <"(" or ")">]
// e.g. [["n", 21], ["o", "+"], ["n", 9], ["o", *],
// ["g", "("], ["n", 3], ["o", "+"], ["n", 2],
// ["o", "^"], ["n", 9], ["g", ")"], ["o", "+"],
// ["n", "1"]]
$tokens = array();
$dragline = 0;
foreach(str_split($q) as $i=>$char) {
if (in_array($char, $nums)) {
continue;
}
elseif (in_array($char, $ops) || in_array($char, $grouping)) {
// hitting a non-numeric implies everything since the
// last hit has been part of a number
$capture = substr($q, $dragline, $i - $dragline);
// prevent the int cast from creating imaginary
// ["n", 0] tokens
if ($capture != "") {
if (substr_count($capture, ".") > 1) {
return "";
}
array_push($tokens, ["n", (float)$capture]);
}
// reset to one past the current (non-numeric) char
$dragline = $i + 1;
// the `;' backstop is not a real token and this should
// never be present in the token list
if ($char != ";") {
array_push($tokens, [
($char == "(" || $char == ")") ? "g" : "o",
$char
]);
}
}
else {
return "";
}
}
// two operators back to back should fail
for ($i = 1; $i < count($tokens); $i++) {
if ($tokens[$i][0] == "o" && $tokens[$i-1][0] == "o") {
return "";
}
}
// no implicit multiplication
for ($i = 0; $i < count($tokens) - 1; $i++) {
if ($tokens[$i][0] == "n" && $tokens[$i+1] == ["g", "("]) {
return "";
}
}
//strategy:
// traverse to group open (if there is one)
// - return to start with the internals
// traverse to ^, attack token previous and after
// same but for *, then / then + then -
// poppers all teh way down
try {
return [
substr($q, 0, strlen($q)-1)." = " => $this->executeBlock($tokens)[0][1]
];
}
catch (\Throwable $e) {
if (get_class($e) == "DivisionByZeroError") {
return [
$q." = " => "Division by Zero Error!!"
];
}
return "";
}
}
public function executeBlock($tokens) {
if (count($tokens) >= 2 && $tokens[0][0] == "o" && $tokens[0][1] == "-" && $tokens[1][0] == "n") {
array_splice($tokens, 0, 2, [["n", -1 * (float)$tokens[1][1]]]);
}
if (count($tokens) > 0 && $tokens[0][0] == "o" || $tokens[count($tokens)-1][0] == "o") {
throw new Exception("Error Processing Request", 1);
}
if (in_array(["g", "("], $tokens)) {
$first_open = array_search(["g", "("], $tokens);
$enclosedality = 1;
for ($i = $first_open+1; $i < count($tokens); $i++) {
if ($tokens[$i][0] == "g") {
$enclosedality += ($tokens[$i][1] == "(") ? 1 : -1;
}
if ($enclosedality == 0) {
array_splice($tokens,
$first_open,
$i+1 - $first_open,
$this->executeBlock(
array_slice($tokens, $first_open+1, $i-1 - $first_open)
)
);
break;
}
}
}
$operators_in_pemdas_order = [
"^" => (fn($x, $y) => $x ** $y),
"*" => (fn($x, $y) => $x * $y),
"/" => (fn($x, $y) => $x / $y),
"%" => (fn($x, $y) => $x % $y),
"+" => (fn($x, $y) => $x + $y),
"-" => (fn($x, $y) => $x - $y)
];
foreach ($operators_in_pemdas_order as $op=>$func) {
while (in_array(["o", $op], $tokens)) {
for ($i = 0; $i < count($tokens); $i++) {
if ($tokens[$i] == ["o", $op]) {
array_splice(
$tokens,
$i-1,
3,
[["n", (string)($func((float)$tokens[$i-1][1], (float)$tokens[$i+1][1]))]]
);
}
}
}
}
return $tokens;
}
}
?>