SpreadQuiz/class/ExpressionBuilder.php
Epagris 0c44163bdf - automatic typecast to bool added
- searching by group's unique name added
2024-09-26 13:17:19 +02:00

183 lines
5.9 KiB
PHP

<?php
class FieldName {
public string $fieldName;
public function __construct(string $fieldName)
{
$this->fieldName = $fieldName;
}
}
class ExpressionBuilder
{
// Automatic typecast.
static function automaticTypecast(string $rval) : mixed
{
if (is_numeric($rval)) { // is it a numeric value?
if (((int)$rval) == ((double)$rval)) { // is it an integer?
return (int)$rval;
} else { // is it a float?
return (double)$rval;
}
} else if (in_array(strtolower($rval), ["true", "false"]) ) { // it's a boolean
return $rval === "true";
} else if (str_starts_with($rval, '"') && str_ends_with($rval, '"')){ // it's a string
return substr($rval, 1, strlen($rval) - 2); // strip leading and trailing quotes
} else { // it must be a column name
return new FieldName($rval);
}
}
// Divide expression into operands and operators.
static function splitCriterion(string $crstr): array|Closure
{
preg_match("/([<>=!]+|LIKE|NOT LIKE|IN|NOT IN|CONTAINS|NOT CONTAINS|BETWEEN|NOT BETWEEN|EXISTS)/", $crstr, $matches, PREG_OFFSET_CAPTURE);
// extract operator
$op = $matches[0][0];
$op_pos = $matches[0][1];
// extract operands
$left = trim(substr($crstr, 0, $op_pos));
$right = trim(substr($crstr, $op_pos + strlen($op), strlen($crstr)));
// automatic type conversion
if (str_starts_with($right, "[") && str_ends_with($right, "]")) { // is it an array?
$right = substr($right, 1, -1); // strip leading and trailing brackets
$elements = explode(",", $right); // extract array elements
$right = []; // re-init right value, since it's an array
foreach ($elements as $element) { // insert array elements
$element = trim($element);
if ($element !== "") {
$right[] = self::automaticTypecast($element);
}
}
} else { // it must be a single value
$right = self::automaticTypecast($right);
}
// handle "default" criteria and closures
if (is_a($right, FieldName::class)) { // field name
return function($a) use ($left, $right, $op): bool {
$X = &$a[$left];
$Y = &$a[$right->fieldName];
switch ($op) {
case "=": {
return $X == $Y;
}
case "!=": {
return $X != $Y;
}
case "<": {
return $X < $Y;
}
case ">": {
return $X > $Y;
}
case "<=": {
return $X <= $Y;
}
case ">=": {
return $X >= $Y;
}
default: {
return false;
}
}
};
} else { // default critera
return [$left, $op, $right];
}
}
// Build SleekDB query expression. Processes encapsulated expressions recursively as well.
static function buildQuery(string $filter): array
{
// skip empty filter processing
if (trim($filter) === "") {
return [];
}
// subfilters and operations
$subfilts = [];
$operations = [];
// buffer and scoring
$k = 0;
$k_prev = 0;
$buffer = "";
for ($i = 0; $i < strlen($filter); $i++) {
$c = $filter[$i];
// extract groups surrounded by parantheses
if ($c === "(") {
$k++;
} elseif ($c === ")") {
$k--;
}
// only omit parentheses at the top-level expression
if (!((($c === "(") && ($k === 1)) || (($c === ")") && ($k === 0)))) {
$buffer .= $c;
}
// if k = 0, then we found a subfilter
if (($k === 0) && ($k_prev === 1)) {
$subfilts[] = trim($buffer);
$buffer = "";
} elseif (($k === 1) && ($k_prev === 0)) {
$op = trim($buffer);
if ($op !== "") {
$operations[] = $op;
}
$buffer = "";
}
// save k to be used next iteration
$k_prev = $k;
}
// decide, whether further expansion of condition is needed
$criteria = [];
for ($i = 0; $i < count($subfilts); $i++) {
$subfilt = $subfilts[$i];
// add subcriterion
if ($subfilt[0] === "(") {
$criteria[] = self::buildQuery($subfilt);
} else {
$criteria[] = self::splitCriterion($subfilt);
}
// add operator
if (($i + 1) < count($subfilts)) {
$criteria[] = $operations[$i];
}
}
return $criteria;
}
// Build SleekDB ordering.
static function buildOrdering(string $orderby): array
{
// don't process empty order instructions
if ($orderby === "") {
return [];
}
// explode string at tokens delimiting separate order criteria
$ordering = [];
$subcriteria = explode(";", $orderby);
foreach ($subcriteria as $subcriterion) {
$parts = explode(":", $subcriterion); // fetch parts
$field_name = trim($parts[0], "\ \n\r\t\v\0\"'"); // strip leading and trailing quotes if exists
$direction = strtolower(trim($parts[1])); // fetch ordering direction
$ordering[$field_name] = $direction; // build ordering instruction
}
return $ordering;
}
}