formula: nested key access

This commit is contained in:
Yuri Kuznetsov
2025-03-27 16:34:19 +02:00
parent ff045692d0
commit 7e49e6491d
4 changed files with 194 additions and 24 deletions

View File

@@ -45,7 +45,7 @@ class VariableGetValueByKeyType extends BaseFunction
}
$name = $this->evaluate($args[0]);
$key = $this->evaluate($args[1]);
$keys = $this->evaluate($args[1]);
if (!is_string($name)) {
$this->throwBadArgumentValue(1, 'string');
@@ -57,13 +57,21 @@ class VariableGetValueByKeyType extends BaseFunction
$reference =& $this->getVariables()->$name;
return $this->getByKey($reference, $key);
$value = null;
foreach ($keys as $key) {
$value =& $this->getByKey($reference, $key);
$reference =& $value;
}
return $value;
}
/**
* @throws Error
*/
private function getByKey(mixed &$reference, mixed $key): mixed
private function &getByKey(mixed &$reference, mixed $key): mixed
{
if (!is_array($reference) && !$reference instanceof stdClass) {
throw new Error("Cannot access by key of variable that is non-array and non-object.");
@@ -86,7 +94,10 @@ class VariableGetValueByKeyType extends BaseFunction
throw new Error("Cannot get array item value by non-existent key.");
}
return $reference[$key];
/** @noinspection PhpUnnecessaryLocalVariableInspection */
$value =& $reference[$key];
return $value;
}
if (!is_string($key)) {
@@ -101,6 +112,9 @@ class VariableGetValueByKeyType extends BaseFunction
throw new Error("Cannot get object item value by non-existent key.");
}
return $reference->$key;
/** @noinspection PhpUnnecessaryLocalVariableInspection */
$value =& $reference->$key;
return $value;
}
}

View File

@@ -45,7 +45,7 @@ class VariableSetKeyValueType extends BaseFunction
}
$name = $this->evaluate($args[0]);
$key = $this->evaluate($args[1]);
$keys = $this->evaluate($args[1]);
$value = $this->evaluate($args[2]);
if (!is_string($name)) {
@@ -58,7 +58,17 @@ class VariableSetKeyValueType extends BaseFunction
$reference =& $this->getVariables()->$name;
$this->setByKey($reference, $key, $value);
foreach ($keys as $i => $key) {
if ($i === count($keys) - 1) {
$this->setByKey($reference, $key, $value);
return;
}
$referenceValue =& $this->getByKey($reference, $key);
$reference =& $referenceValue;
}
}
/**
@@ -98,4 +108,54 @@ class VariableSetKeyValueType extends BaseFunction
$reference->$key = $value;
}
/**
* @throws Error
*/
private function &getByKey(mixed &$reference, mixed $key): mixed
{
if (!is_array($reference) && !$reference instanceof stdClass) {
throw new Error("Cannot access by key of variable that is non-array and non-object.");
}
if (is_array($reference)) {
if (!is_int($key)) {
throw new Error("Cannot get array item value by non-integer key.");
}
if ($key < 0) {
throw new Error("Cannot get array item value by key that is less than zero.");
}
if ($key > count($reference) - 1) {
throw new Error("Cannot get array item value by key that is out of array end.");
}
if (!array_key_exists($key, $reference)) {
throw new Error("Cannot get array item value by non-existent key.");
}
/** @noinspection PhpUnnecessaryLocalVariableInspection */
$value =& $reference[$key];
return $value;
}
if (!is_string($key)) {
throw new Error("Cannot get object item value by non-string key.");
}
if ($key === '') {
throw new Error("Cannot get object item value by empty string key.");
}
if (!property_exists($reference, $key)) {
throw new Error("Cannot get object item value by non-existent key.");
}
/** @noinspection PhpUnnecessaryLocalVariableInspection */
$value =& $reference->$key;
return $value;
}
}

View File

@@ -1361,23 +1361,21 @@ class Parser
$isArrayAppend = false;
$isKeyValue = false;
$keyExpression = '';
$keyPath = [];
if (str_ends_with($firstPart, '[]')) {
$variable = substr($firstPart, 1, -2);
$isArrayAppend = true;
} else if (str_ends_with($firstPart, ']') && str_contains($firstPart, '[')) {
$bracketPosition = strpos($firstPart, '[');
$bracketPosition = strpos($firstPart, '[') ?: 0;
$variable = substr($firstPart, 1, $bracketPosition - 1);
$keyExpression = trim(substr($firstPart, $bracketPosition + 1, -1));
$keyPart = trim(substr($firstPart, $bracketPosition));
$keyPath = array_map(fn ($it) => $this->split($it), $this->splitKeys($keyPart));
$isKeyValue = true;
if ($keyExpression === '') {
throw new SyntaxError("No expression inside brackets.");
}
}
if ($variable === '' || !preg_match($this->variableNameRegExp, $variable)) {
@@ -1394,7 +1392,7 @@ class Parser
if ($isKeyValue) {
return new Node('variableSetKeyValue', [
new Value($variable),
$this->split($keyExpression),
new Node('list', $keyPath),
$this->split($secondPart)
]);
}
@@ -1415,7 +1413,7 @@ class Parser
$isIncrement = false;
$isDecrement = false;
$isKeyValue = false;
$keyExpression = '';
$keyPath = [];
if (str_ends_with($expression, '++')) {
$isIncrement = true;
@@ -1428,16 +1426,12 @@ class Parser
$value = rtrim(substr($value, 0, -2));
} else if (str_ends_with($expression, ']') && str_contains($expression, '[')) {
$bracketPosition = strpos($expression, '[');
$bracketPosition = strpos($expression, '[') ?: 0;
$value = substr($expression, 1, $bracketPosition - 1);
$keyExpression = trim(substr($expression, $bracketPosition + 1, -1));
$keyPart = trim(substr($expression, $bracketPosition));
$keyPath = array_map(fn ($it) => $this->split($it), $this->splitKeys($keyPart));
$isKeyValue = true;
if ($keyExpression === '') {
throw new SyntaxError("No expression inside brackets.");
}
}
if ($value === '' || !preg_match($this->variableNameRegExp, $value)) {
@@ -1459,10 +1453,83 @@ class Parser
if ($isKeyValue) {
return new Node('variableGetValueByKey', [
new Value($value),
$this->split($keyExpression),
new Node('list', $keyPath),
]);
}
return new Variable($value);
}
/**
* @return string[]
* @throws SyntaxError
*/
private function splitKeys(string $expression): array
{
$modifiedExpression = '';
$this->processString($expression, $modifiedExpression, $statementList, true);
$expressionLength = strlen($modifiedExpression);
$parenthesisCounter = 0;
$bracketCounter = 0;
$output = [];
/** @var array{int, int}[] $indexPairs */
$indexPairs = [];
$startIndex = -1;
for ($i = 0; $i < $expressionLength; $i++) {
$value = $modifiedExpression[$i];
if ($value === '(') {
$parenthesisCounter++;
} else if ($value === ')') {
$parenthesisCounter--;
} else if ($value === '[') {
$bracketCounter++;
} else if ($value === ']') {
$bracketCounter--;
}
if (
$value === '[' &&
$parenthesisCounter === 0 &&
$bracketCounter === 1
) {
$startIndex = $i;
}
if (
$value === ']' &&
$parenthesisCounter === 0 &&
$bracketCounter === 0
) {
$indexPairs[] = [$startIndex + 1, $i];
$startIndex = -1;
}
}
foreach ($indexPairs as $i => $pair) {
if ($i > 0) {
if ($indexPairs[$i - 1][1] !== $pair[0] - 2) {
throw new SyntaxError("Nested brackets must have no gaps in between.");
}
}
$itemExpression = trim(substr($expression, $pair[0], $pair[1] - $pair[0]));
if ($itemExpression === '') {
throw new SyntaxError("No expression inside brackets.");
}
$output[] = $itemExpression;
}
return $output;
}
}

View File

@@ -1989,4 +1989,33 @@ class EvaluatorTest extends TestCase
/** @noinspection PhpUnhandledExceptionInspection */
$this->evaluator->process($expression);
}
public function testArrayGetSetNested(): void
{
$expression = "
\$a = list(list(0, 1));
\$a[0][2] = 2;
\$a[0][2];
";
/** @noinspection PhpUnhandledExceptionInspection */
$result = $this->evaluator->process($expression);
$this->assertEquals(2, $result);
}
public function testObjectGetSetNested(): void
{
$expression = "
\$o = object\\create();
\$o['a'] = object\\create();
\$o['a']['a1'] = '1';
\$o['a']['a1'];
";
/** @noinspection PhpUnhandledExceptionInspection */
$result = $this->evaluator->process($expression);
$this->assertEquals('1', $result);
}
}