|
|
@@ -85,11 +85,11 @@ enum CellExpressionTokenType {
|
|
85
|
85
|
* `Function`.
|
|
86
|
86
|
*/
|
|
87
|
87
|
enum CellExpressionOperation {
|
|
88
|
|
- /** Arg is `number` */
|
|
|
88
|
+ /** Arg is `float` */
|
|
89
|
89
|
case Number;
|
|
90
|
90
|
/** Arg is `string` without quotes */
|
|
91
|
91
|
case String;
|
|
92
|
|
- /** Arg is `boolean` */
|
|
|
92
|
+ /** Arg is `bool` */
|
|
93
|
93
|
case Boolean;
|
|
94
|
94
|
/** Arg is `CellAddress` */
|
|
95
|
95
|
case Reference;
|
|
|
@@ -154,8 +154,11 @@ class CellExpressionSet {
|
|
154
|
154
|
/** @$CellAddress[] */
|
|
155
|
155
|
$expressionAddressQueue = [];
|
|
156
|
156
|
$range = new CellAddressRange(new CellAddress(0, 0), new CellAddress($colCount - 1, $rowCount - 1));
|
|
157
|
|
- foreach ($range->cellsIn($this->grid) as $address) {
|
|
158
|
|
- $cell = $this->grid->cellAt($address);
|
|
|
157
|
+ foreach ($range->cellsIn($this->grid) as $addressStr => $cell) {
|
|
|
158
|
+ $address = CellAddress::fromString($addressStr);
|
|
|
159
|
+ if ($address === null) {
|
|
|
160
|
+ error_log("Couldn't parse address string {$addressStr}!");
|
|
|
161
|
+ }
|
|
159
|
162
|
$value = $cell->originalValue;
|
|
160
|
163
|
if ($value->type != CellValue::TYPE_FORMULA) {
|
|
161
|
164
|
$cell->outputValue = $value;
|
|
|
@@ -235,8 +238,8 @@ class CellExpressionSet {
|
|
235
|
238
|
*/
|
|
236
|
239
|
private function enqueueFilledBlanks(CellExpression $expression, array &$addresses) {
|
|
237
|
240
|
foreach ($expression->fillRanges ?? [] as $range) {
|
|
238
|
|
- foreach ($range->cellsIn($this->grid) as $filledAddress) {
|
|
239
|
|
- $filledCell = $this->grid->cellAt($filledAddress);
|
|
|
241
|
+ foreach ($range->cellsIn($this->grid) as $filledAddressStr => $filledCell) {
|
|
|
242
|
+ $filledAddress = CellAddress::fromString($filledAddressStr);
|
|
240
|
243
|
if ($filledCell->originalValue->type === CellValue::TYPE_BLANK &&
|
|
241
|
244
|
(!$filledCell->outputValue ||
|
|
242
|
245
|
$filledCell->outputValue->type === CellValue::TYPE_BLANK)) {
|
|
|
@@ -309,8 +312,8 @@ class CellExpressionSet {
|
|
309
|
312
|
case CellExpressionOperation::Range: {
|
|
310
|
313
|
$range = $expr->arguments[0];
|
|
311
|
314
|
$values = [];
|
|
312
|
|
- foreach ($range->cellsIn($this->grid) as $rAddress) {
|
|
313
|
|
- $cell = $this->grid->cellAt($rAddress);
|
|
|
315
|
+ foreach ($range->cellsIn($this->grid) as $rAddressStr => $cell) {
|
|
|
316
|
+ $rAddress = CellAddress::fromString($rAddressStr);
|
|
314
|
317
|
if ($rAddress->equals($address)) continue;
|
|
315
|
318
|
$val = $this->grid->outputValueAt($rAddress);
|
|
316
|
319
|
if ($val === null) {
|
|
|
@@ -653,7 +656,7 @@ class CellExpressionSet {
|
|
653
|
656
|
}
|
|
654
|
657
|
$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
|
|
655
|
658
|
for ($i = 0; $i < sizeof($evaled) - 1; $i += 2) {
|
|
656
|
|
- $test = $evaled[i].booleanValue();
|
|
|
659
|
+ $test = $evaled[$i].booleanValue();
|
|
657
|
660
|
if ($test === null) {
|
|
658
|
661
|
throw new CellEvaluationException("IFS expects a boolean for argument " . ($i + 1));
|
|
659
|
662
|
}
|
|
|
@@ -964,7 +967,7 @@ class CellExpression {
|
|
964
|
967
|
public ?string $qualifier;
|
|
965
|
968
|
|
|
966
|
969
|
/**
|
|
967
|
|
- * Optional format override. One of `number`, `currency`, `percent`.
|
|
|
970
|
+ * Optional format override. One of `"number"`, `"currency"`, `"percent"`.
|
|
968
|
971
|
*/
|
|
969
|
972
|
public ?string $outputType = null;
|
|
970
|
973
|
|
|
|
@@ -1005,9 +1008,9 @@ class CellExpression {
|
|
1005
|
1008
|
* @return ?CellExpression parsed expression, or `null` if it failed
|
|
1006
|
1009
|
*/
|
|
1007
|
1010
|
public static function parse(string $expression, CellAddress $address): ?CellExpression {
|
|
1008
|
|
- $tokens = $this->expressionToTokens($expression);
|
|
|
1011
|
+ $tokens = self::expressionToTokens($expression);
|
|
1009
|
1012
|
if (sizeof($tokens) == 0) return null;
|
|
1010
|
|
- $expr = $this->expressionFromTokens($tokens, $address);
|
|
|
1013
|
+ $expr = self::expressionFromTokens($tokens, $address);
|
|
1011
|
1014
|
$expr->location = $address;
|
|
1012
|
1015
|
return $expr;
|
|
1013
|
1016
|
}
|
|
|
@@ -1047,10 +1050,7 @@ class CellExpression {
|
|
1047
|
1050
|
}
|
|
1048
|
1051
|
|
|
1049
|
1052
|
private function clone(): CellExpression {
|
|
1050
|
|
- $cp = new CellExpression();
|
|
1051
|
|
- $cp->op = $this->op;
|
|
1052
|
|
- $cp->arguments = array($this->arguments);
|
|
1053
|
|
- $cp->qualifier = $this->qualifier;
|
|
|
1053
|
+ $cp = new CellExpression($this->op, array($this->arguments), $this->qualifier);
|
|
1054
|
1054
|
$cp->outputType = $this->outputType;
|
|
1055
|
1055
|
$cp->outputDecimals = $this->outputDecimals;
|
|
1056
|
1056
|
$cp->fillRanges = $this->fillRanges !== null ? array($this->fillRanges) : null;
|
|
|
@@ -1089,17 +1089,17 @@ class CellExpression {
|
|
1089
|
1089
|
*/
|
|
1090
|
1090
|
public static function expressionToTokens(string $text): array {
|
|
1091
|
1091
|
$tokens = [];
|
|
1092
|
|
- $pos = [0];
|
|
1093
|
|
- $this->skipWhitespace($text, $pos);
|
|
1094
|
|
- if (mb_substr($text, $pos[0], 1) == '=') {
|
|
|
1092
|
+ $pos = 0;
|
|
|
1093
|
+ self::skipWhitespace($text, $pos);
|
|
|
1094
|
+ if (mb_substr($text, $pos, 1) === '=') {
|
|
1095
|
1095
|
// Ignore equals
|
|
1096
|
|
- $pos[0]++;
|
|
|
1096
|
+ $pos++;
|
|
1097
|
1097
|
}
|
|
1098
|
|
- $this->skipWhitespace($text, $pos);
|
|
|
1098
|
+ self::skipWhitespace($text, $pos);
|
|
1099
|
1099
|
$l = mb_strlen($text);
|
|
1100
|
|
- while ($pos[0] < $l) {
|
|
1101
|
|
- array_push($tokens, $this->readNextToken($text, $pos));
|
|
1102
|
|
- $this->skipWhitespace($text, $pos);
|
|
|
1100
|
+ while ($pos < $l) {
|
|
|
1101
|
+ array_push($tokens, self::readNextToken($text, $pos));
|
|
|
1102
|
+ self::skipWhitespace($text, $pos);
|
|
1103
|
1103
|
}
|
|
1104
|
1104
|
return $tokens;
|
|
1105
|
1105
|
}
|
|
|
@@ -1111,28 +1111,28 @@ class CellExpression {
|
|
1111
|
1111
|
*/
|
|
1112
|
1112
|
private static function readNextToken(string $text, int &$pos): CellExpressionToken {
|
|
1113
|
1113
|
// Single char tokens
|
|
1114
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '==', CellExpressionTokenType::Equal)) return $token;
|
|
1115
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '!=', CellExpressionTokenType::Unequal)) return $token;
|
|
1116
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '<=', CellExpressionTokenType::LessThanEqual)) return $token;
|
|
1117
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '>=', CellExpressionTokenType::GreaterThanEqual)) return $token;
|
|
1118
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '<', CellExpressionTokenType::LessThan)) return $token;
|
|
1119
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '>', CellExpressionTokenType::GreaterThan)) return $token;
|
|
1120
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '!', CellExpressionTokenType::Not)) return $token;
|
|
1121
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '+', CellExpressionTokenType::Plus)) return $token;
|
|
1122
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '-', CellExpressionTokenType::Minus)) return $token;
|
|
1123
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '*', CellExpressionTokenType::Multiply)) return $token;
|
|
1124
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '/', CellExpressionTokenType::Divide)) return $token;
|
|
1125
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, ',', CellExpressionTokenType::Comma)) return $token;
|
|
1126
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '(', CellExpressionTokenType::OpenParen)) return $token;
|
|
1127
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, ')', CellExpressionTokenType::CloseParen)) return $token;
|
|
1128
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, ':', CellExpressionTokenType::Colon)) return $token;
|
|
1129
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, ';', CellExpressionTokenType::Semicolon)) return $token;
|
|
1130
|
|
- if ($token = $this->readNextSimpleToken($text, $pos, '&', CellExpressionTokenType::Ampersand)) return $token;
|
|
|
1114
|
+ if ($token = self::readNextSimpleToken($text, $pos, '==', CellExpressionTokenType::Equal)) return $token;
|
|
|
1115
|
+ if ($token = self::readNextSimpleToken($text, $pos, '!=', CellExpressionTokenType::Unequal)) return $token;
|
|
|
1116
|
+ if ($token = self::readNextSimpleToken($text, $pos, '<=', CellExpressionTokenType::LessThanEqual)) return $token;
|
|
|
1117
|
+ if ($token = self::readNextSimpleToken($text, $pos, '>=', CellExpressionTokenType::GreaterThanEqual)) return $token;
|
|
|
1118
|
+ if ($token = self::readNextSimpleToken($text, $pos, '<', CellExpressionTokenType::LessThan)) return $token;
|
|
|
1119
|
+ if ($token = self::readNextSimpleToken($text, $pos, '>', CellExpressionTokenType::GreaterThan)) return $token;
|
|
|
1120
|
+ if ($token = self::readNextSimpleToken($text, $pos, '!', CellExpressionTokenType::Not)) return $token;
|
|
|
1121
|
+ if ($token = self::readNextSimpleToken($text, $pos, '+', CellExpressionTokenType::Plus)) return $token;
|
|
|
1122
|
+ if ($token = self::readNextSimpleToken($text, $pos, '-', CellExpressionTokenType::Minus)) return $token;
|
|
|
1123
|
+ if ($token = self::readNextSimpleToken($text, $pos, '*', CellExpressionTokenType::Multiply)) return $token;
|
|
|
1124
|
+ if ($token = self::readNextSimpleToken($text, $pos, '/', CellExpressionTokenType::Divide)) return $token;
|
|
|
1125
|
+ if ($token = self::readNextSimpleToken($text, $pos, ',', CellExpressionTokenType::Comma)) return $token;
|
|
|
1126
|
+ if ($token = self::readNextSimpleToken($text, $pos, '(', CellExpressionTokenType::OpenParen)) return $token;
|
|
|
1127
|
+ if ($token = self::readNextSimpleToken($text, $pos, ')', CellExpressionTokenType::CloseParen)) return $token;
|
|
|
1128
|
+ if ($token = self::readNextSimpleToken($text, $pos, ':', CellExpressionTokenType::Colon)) return $token;
|
|
|
1129
|
+ if ($token = self::readNextSimpleToken($text, $pos, ';', CellExpressionTokenType::Semicolon)) return $token;
|
|
|
1130
|
+ if ($token = self::readNextSimpleToken($text, $pos, '&', CellExpressionTokenType::Ampersand)) return $token;
|
|
1131
|
1131
|
// Other tokens
|
|
1132
|
|
- if ($token = $this->readNextAddressToken($text, $pos)) return $token;
|
|
1133
|
|
- if ($token = $this->readNextNameToken($text, $pos)) return $token;
|
|
1134
|
|
- if ($token = $this->readNextNumberToken($text, $pos)) return $token;
|
|
1135
|
|
- if ($token = $this->readNextStringToken($text, $pos)) return $token;
|
|
|
1132
|
+ if ($token = self::readNextAddressToken($text, $pos)) return $token;
|
|
|
1133
|
+ if ($token = self::readNextNameToken($text, $pos)) return $token;
|
|
|
1134
|
+ if ($token = self::readNextNumberToken($text, $pos)) return $token;
|
|
|
1135
|
+ if ($token = self::readNextStringToken($text, $pos)) return $token;
|
|
1136
|
1136
|
$ch = mb_substr($text, $pos, 1);
|
|
1137
|
1137
|
throw new CellSyntaxException("Unexpected character \"{$ch}\" at {$pos}");
|
|
1138
|
1138
|
}
|
|
|
@@ -1166,25 +1166,25 @@ class CellExpression {
|
|
1166
|
1166
|
$address = '';
|
|
1167
|
1167
|
$isName = true;
|
|
1168
|
1168
|
if ($ch == '$') {
|
|
1169
|
|
- $address += $ch;
|
|
|
1169
|
+ $address .= $ch;
|
|
1170
|
1170
|
$isName = false;
|
|
1171
|
1171
|
$p++;
|
|
1172
|
1172
|
}
|
|
1173
|
|
- $col = $this->readChars($text, $p, fn($s) => $this->isLetter($s), 1, 2);
|
|
|
1173
|
+ $col = self::readChars($text, $p, fn($s) => self::isLetter($s), 1, 2);
|
|
1174
|
1174
|
if ($col === null) return null;
|
|
1175
|
|
- $address += $col;
|
|
|
1175
|
+ $address .= $col;
|
|
1176
|
1176
|
$ch = mb_substr($text, $p, 1);
|
|
1177
|
1177
|
if ($ch == '$') {
|
|
1178
|
|
- $address += $ch;
|
|
|
1178
|
+ $address .= $ch;
|
|
1179
|
1179
|
$isName = false;
|
|
1180
|
1180
|
$p++;
|
|
1181
|
|
- $row = $this->readChars($text, $p, $this->isDigit, 1);
|
|
|
1181
|
+ $row = self::readChars($text, $p, fn($ch) => self::isDigit($ch), 1);
|
|
1182
|
1182
|
if ($row === null) return null;
|
|
1183
|
|
- $address += $row;
|
|
|
1183
|
+ $address .= $row;
|
|
1184
|
1184
|
} else {
|
|
1185
|
|
- $row = $this->readChars($text, $p, $this->isDigit, 0);
|
|
|
1185
|
+ $row = self::readChars($text, $p, fn($ch) => self::isDigit($ch), 0);
|
|
1186
|
1186
|
if ($row === null) return null;
|
|
1187
|
|
- $address += $row;
|
|
|
1187
|
+ $address .= $row;
|
|
1188
|
1188
|
}
|
|
1189
|
1189
|
$pos = $p;
|
|
1190
|
1190
|
return new CellExpressionToken(
|
|
|
@@ -1194,7 +1194,7 @@ class CellExpression {
|
|
1194
|
1194
|
|
|
1195
|
1195
|
private static function readNextNameToken(string $text, int &$pos): ?CellExpressionToken {
|
|
1196
|
1196
|
$p = $pos;
|
|
1197
|
|
- $name = $this->readChars($text, $p, fn($s) => $this->isLetter($s), 1);
|
|
|
1197
|
+ $name = self::readChars($text, $p, fn($s) => self::isLetter($s), 1);
|
|
1198
|
1198
|
if ($name === null) return null;
|
|
1199
|
1199
|
$pos = $p;
|
|
1200
|
1200
|
if (CellAddress::isAddress($name)) {
|
|
|
@@ -1205,13 +1205,13 @@ class CellExpression {
|
|
1205
|
1205
|
|
|
1206
|
1206
|
private static function readNextNumberToken(string $text, int &$pos): ?CellExpressionToken {
|
|
1207
|
1207
|
$ch = mb_substr($text, $pos, 1);
|
|
1208
|
|
- if (!$this->isDigit($ch)) return null;
|
|
|
1208
|
+ if (!self::isDigit($ch)) return null;
|
|
1209
|
1209
|
$l = mb_strlen($text);
|
|
1210
|
1210
|
$numStr = $ch;
|
|
1211
|
1211
|
$pos++;
|
|
1212
|
1212
|
while ($pos < $l) {
|
|
1213
|
1213
|
$ch = mb_substr($text, $pos, 1);
|
|
1214
|
|
- if ($this->isDigit($ch)) {
|
|
|
1214
|
+ if (self::isDigit($ch)) {
|
|
1215
|
1215
|
$pos++;
|
|
1216
|
1216
|
$numStr .= $ch;
|
|
1217
|
1217
|
} else {
|
|
|
@@ -1222,10 +1222,10 @@ class CellExpression {
|
|
1222
|
1222
|
$ch = mb_substr($text, $pos, 1);
|
|
1223
|
1223
|
if ($ch == '.') {
|
|
1224
|
1224
|
$numStr .= $ch;
|
|
1225
|
|
- $pos[0]++;
|
|
|
1225
|
+ $pos++;
|
|
1226
|
1226
|
while ($pos < $l) {
|
|
1227
|
|
- $ch = mb_substr($text, $pos[0], 1);
|
|
1228
|
|
- if ($this->isDigit($ch)) {
|
|
|
1227
|
+ $ch = mb_substr($text, $pos, 1);
|
|
|
1228
|
+ if (self::isDigit($ch)) {
|
|
1229
|
1229
|
$pos++;
|
|
1230
|
1230
|
$numStr .= $ch;
|
|
1231
|
1231
|
} else {
|
|
|
@@ -1245,7 +1245,7 @@ class CellExpression {
|
|
1245
|
1245
|
$l = mb_strlen($text);
|
|
1246
|
1246
|
$inEscape = false;
|
|
1247
|
1247
|
while ($pos < $l) {
|
|
1248
|
|
- $ch = mb_substr($text, $pos[0], $pos[0] + 1);
|
|
|
1248
|
+ $ch = mb_substr($text, $pos, 1);
|
|
1249
|
1249
|
$pos++;
|
|
1250
|
1250
|
if ($inEscape) {
|
|
1251
|
1251
|
$inEscape = false;
|
|
|
@@ -1259,7 +1259,7 @@ class CellExpression {
|
|
1259
|
1259
|
} elseif ($ch == '"') {
|
|
1260
|
1260
|
return new CellExpressionToken(CellExpressionTokenType::String, $str);
|
|
1261
|
1261
|
} else {
|
|
1262
|
|
- $str += $ch;
|
|
|
1262
|
+ $str .= $ch;
|
|
1263
|
1263
|
}
|
|
1264
|
1264
|
}
|
|
1265
|
1265
|
throw new CellSyntaxException('Unterminated string');
|
|
|
@@ -1284,11 +1284,11 @@ class CellExpression {
|
|
1284
|
1284
|
while ($p < $l && ($maximumLength === null || $sl < $maximumLength)) {
|
|
1285
|
1285
|
$ch = mb_substr($text, $p, 1);
|
|
1286
|
1286
|
if (!$charTest($ch)) break;
|
|
1287
|
|
- $s += $ch;
|
|
|
1287
|
+ $s .= $ch;
|
|
1288
|
1288
|
$sl++;
|
|
1289
|
1289
|
$p++;
|
|
1290
|
1290
|
}
|
|
1291
|
|
- if ($p < $l && $charTest(mb_substr($text, $p, $p + 1))) {
|
|
|
1291
|
+ if ($p < $l && $charTest(mb_substr($text, $p, 1))) {
|
|
1292
|
1292
|
return null;
|
|
1293
|
1293
|
}
|
|
1294
|
1294
|
if ($minimumLength !== null && $sl < $minimumLength) {
|
|
|
@@ -1324,9 +1324,9 @@ class CellExpression {
|
|
1324
|
1324
|
* @return ?CellExpression
|
|
1325
|
1325
|
*/
|
|
1326
|
1326
|
public static function expressionFromTokens(array $tokens, CellAddress $address): ?CellExpression {
|
|
1327
|
|
- if ($expr = $this->tryExpressionAndFormat($tokens, 0, sizeof($tokens) - 1, $address)) return $expr;
|
|
1328
|
|
- if ($expr = $this->tryExpressionAndFill($tokens, 0, sizeof($tokens) - 1, $address)) return $expr;
|
|
1329
|
|
- if ($expr = $this->tryExpression($tokens, 0, sizeof($tokens) - 1, $address)) return $expr;
|
|
|
1327
|
+ if ($expr = self::tryExpressionAndFormat($tokens, 0, sizeof($tokens) - 1, $address)) return $expr;
|
|
|
1328
|
+ if ($expr = self::tryExpressionAndFill($tokens, 0, sizeof($tokens) - 1, $address)) return $expr;
|
|
|
1329
|
+ if ($expr = self::tryExpression($tokens, 0, sizeof($tokens) - 1, $address)) return $expr;
|
|
1330
|
1330
|
return null;
|
|
1331
|
1331
|
}
|
|
1332
|
1332
|
|
|
|
@@ -1340,11 +1340,11 @@ class CellExpression {
|
|
1340
|
1340
|
private static function tryExpressionAndFormat(array $tokens, int $start,
|
|
1341
|
1341
|
int $end, CellAddress $address): ?CellExpression {
|
|
1342
|
1342
|
for ($t = $start + 1; $t < $end; $t++) {
|
|
1343
|
|
- if ($tokens[t]->type == CellExpressionTokenType::Semicolon) {
|
|
1344
|
|
- $expr = $this->tryExpressionAndFill($tokens, $start, $t - 1, $address) ??
|
|
1345
|
|
- $this->tryExpression($tokens, $start, $t - 1, $address);
|
|
|
1343
|
+ if ($tokens[$t]->type == CellExpressionTokenType::Semicolon) {
|
|
|
1344
|
+ $expr = self::tryExpressionAndFill($tokens, $start, $t - 1, $address) ??
|
|
|
1345
|
+ self::tryExpression($tokens, $start, $t - 1, $address);
|
|
1346
|
1346
|
if ($expr === null) return null;
|
|
1347
|
|
- $format = $this->tryFormat($tokens, $t + 1, $end, $address);
|
|
|
1347
|
+ $format = self::tryFormat($tokens, $t + 1, $end, $address);
|
|
1348
|
1348
|
if ($format === null) return null;
|
|
1349
|
1349
|
[ $expr->outputType, $expr->outputDecimals ] = $format;
|
|
1350
|
1350
|
return $expr;
|
|
|
@@ -1367,7 +1367,7 @@ class CellExpression {
|
|
1367
|
1367
|
if (!$tokens[$end]->type->isPotentialName()) return null;
|
|
1368
|
1368
|
$name = mb_strtoupper($tokens[$end]->content);
|
|
1369
|
1369
|
if ($name != 'FILL') return null;
|
|
1370
|
|
- $exp = $this->tryExpression($tokens, $start, $end - 1, $address);
|
|
|
1370
|
+ $exp = self::tryExpression($tokens, $start, $end - 1, $address);
|
|
1371
|
1371
|
$columnIndex = $address->columnIndex;
|
|
1372
|
1372
|
$exp->fillRanges = [
|
|
1373
|
1373
|
new CellAddressRange(new CellAddress($columnIndex, -1), new CellAddress($columnIndex, -1)),
|
|
|
@@ -1416,22 +1416,22 @@ class CellExpression {
|
|
1416
|
1416
|
*/
|
|
1417
|
1417
|
private static function tryExpression(array $tokens, int $start, int $end,
|
|
1418
|
1418
|
CellAddress $address): CellExpression {
|
|
1419
|
|
- if ($expr = $this->tryParenExpression($tokens, $start, $end, $address)) return $expr;
|
|
1420
|
|
- if ($expr = $this->tryNumber($tokens, $start, $end, $address)) return $expr;
|
|
1421
|
|
- if ($expr = $this->tryString($tokens, $start, $end, $address)) return $expr;
|
|
1422
|
|
- if ($expr = $this->tryBoolean($tokens, $start, $end, $address)) return $expr;
|
|
1423
|
|
- if ($expr = $this->tryFunction($tokens, $start, $end, $address)) return $expr;
|
|
1424
|
|
- if ($expr = $this->tryRange($tokens, $start, $end, $address)) return $expr;
|
|
1425
|
|
- if ($expr = $this->tryReference($tokens, $start, $end, $address)) return $expr;
|
|
1426
|
|
- if ($expr = $this->tryInfix($tokens, $start, $end, $address)) return $expr;
|
|
1427
|
|
- if ($expr = $this->tryUnary($tokens, $start, $end, $address)) return $expr;
|
|
|
1419
|
+ if ($expr = self::tryParenExpression($tokens, $start, $end, $address)) return $expr;
|
|
|
1420
|
+ if ($expr = self::tryNumber($tokens, $start, $end, $address)) return $expr;
|
|
|
1421
|
+ if ($expr = self::tryString($tokens, $start, $end, $address)) return $expr;
|
|
|
1422
|
+ if ($expr = self::tryBoolean($tokens, $start, $end, $address)) return $expr;
|
|
|
1423
|
+ if ($expr = self::tryFunction($tokens, $start, $end, $address)) return $expr;
|
|
|
1424
|
+ if ($expr = self::tryRange($tokens, $start, $end, $address)) return $expr;
|
|
|
1425
|
+ if ($expr = self::tryReference($tokens, $start, $end, $address)) return $expr;
|
|
|
1426
|
+ if ($expr = self::tryInfix($tokens, $start, $end, $address)) return $expr;
|
|
|
1427
|
+ if ($expr = self::tryUnary($tokens, $start, $end, $address)) return $expr;
|
|
1428
|
1428
|
throw new CellSyntaxException("Invalid expression");
|
|
1429
|
1429
|
}
|
|
1430
|
1430
|
|
|
1431
|
1431
|
/**
|
|
1432
|
1432
|
* @param CellExpressionToken[] $tokens
|
|
1433
|
|
- * @param number $start
|
|
1434
|
|
- * @param number $end
|
|
|
1433
|
+ * @param int $start
|
|
|
1434
|
+ * @param int $end
|
|
1435
|
1435
|
* @param CellAddress $address
|
|
1436
|
1436
|
* @return CellExpression|null
|
|
1437
|
1437
|
*/
|
|
|
@@ -1449,13 +1449,13 @@ class CellExpression {
|
|
1449
|
1449
|
if ($parenLevel < 0) return null;
|
|
1450
|
1450
|
}
|
|
1451
|
1451
|
if ($parenLevel != 0) return null;
|
|
1452
|
|
- return $this->tryExpression($tokens, $start + 1, $end - 1, $address);
|
|
|
1452
|
+ return self::tryExpression($tokens, $start + 1, $end - 1, $address);
|
|
1453
|
1453
|
}
|
|
1454
|
1454
|
|
|
1455
|
1455
|
/**
|
|
1456
|
1456
|
* @param CellExpressionToken[] $tokens
|
|
1457
|
|
- * @param number $start
|
|
1458
|
|
- * @param number $end
|
|
|
1457
|
+ * @param int $start
|
|
|
1458
|
+ * @param int $end
|
|
1459
|
1459
|
* @param CellAddress $address
|
|
1460
|
1460
|
* @return CellExpression|null
|
|
1461
|
1461
|
*/
|
|
|
@@ -1473,8 +1473,8 @@ class CellExpression {
|
|
1473
|
1473
|
|
|
1474
|
1474
|
/**
|
|
1475
|
1475
|
* @param CellExpressionToken[] $tokens
|
|
1476
|
|
- * @param number $start
|
|
1477
|
|
- * @param number $end
|
|
|
1476
|
+ * @param int $start
|
|
|
1477
|
+ * @param int $end
|
|
1478
|
1478
|
* @param CellAddress $address
|
|
1479
|
1479
|
* @return CellExpression|null
|
|
1480
|
1480
|
*/
|
|
|
@@ -1489,8 +1489,8 @@ class CellExpression {
|
|
1489
|
1489
|
|
|
1490
|
1490
|
/**
|
|
1491
|
1491
|
* @param CellExpressionToken[] $tokens
|
|
1492
|
|
- * @param number $start
|
|
1493
|
|
- * @param number $end
|
|
|
1492
|
+ * @param int $start
|
|
|
1493
|
+ * @param int $end
|
|
1494
|
1494
|
* @param CellAddress $address
|
|
1495
|
1495
|
* @return CellExpression|null
|
|
1496
|
1496
|
*/
|
|
|
@@ -1506,8 +1506,8 @@ class CellExpression {
|
|
1506
|
1506
|
|
|
1507
|
1507
|
/**
|
|
1508
|
1508
|
* @param CellExpressionToken[] $tokens
|
|
1509
|
|
- * @param number $start
|
|
1510
|
|
- * @param number $end
|
|
|
1509
|
+ * @param int $start
|
|
|
1510
|
+ * @param int $end
|
|
1511
|
1511
|
* @param CellAddress $address
|
|
1512
|
1512
|
* @return CellExpression|null
|
|
1513
|
1513
|
*/
|
|
|
@@ -1519,20 +1519,20 @@ class CellExpression {
|
|
1519
|
1519
|
$qualifier = $tokens[$start]->content;
|
|
1520
|
1520
|
if ($tokens[$start + 1]->type != CellExpressionTokenType::OpenParen) return null;
|
|
1521
|
1521
|
if ($tokens[$end]->type != CellExpressionTokenType::CloseParen) return null;
|
|
1522
|
|
- $argList = $this->tryArgumentList($tokens, $start + 2, $end - 1, $address);
|
|
|
1522
|
+ $argList = self::tryArgumentList($tokens, $start + 2, $end - 1, $address);
|
|
1523
|
1523
|
if ($argList === null) return null;
|
|
1524
|
1524
|
return new CellExpression(CellExpressionOperation::Function, $argList, $qualifier);
|
|
1525
|
1525
|
}
|
|
1526
|
1526
|
|
|
1527
|
1527
|
/**
|
|
1528
|
1528
|
* @param CellExpressionToken[] $tokens
|
|
1529
|
|
- * @param number $start
|
|
1530
|
|
- * @param number $end
|
|
|
1529
|
+ * @param int $start
|
|
|
1530
|
+ * @param int $end
|
|
1531
|
1531
|
* @param CellAddress $address
|
|
1532
|
1532
|
* @return CellExpression[]|null
|
|
1533
|
1533
|
*/
|
|
1534
|
1534
|
private static function tryArgumentList(array $tokens, int $start, int $end,
|
|
1535
|
|
- CellAddress $address): ?CellExpression {
|
|
|
1535
|
+ CellAddress $address): ?array {
|
|
1536
|
1536
|
$count = $end - $start + 1;
|
|
1537
|
1537
|
if ($count == 0) return [];
|
|
1538
|
1538
|
$parenDepth = 0;
|
|
|
@@ -1558,17 +1558,17 @@ class CellExpression {
|
|
1558
|
1558
|
// Convert token ranges to expressions
|
|
1559
|
1559
|
$args = [];
|
|
1560
|
1560
|
foreach ($argTokens as $argToken) {
|
|
1561
|
|
- $arg = $this->tryExpression($tokens, $argToken[0], $argToken[1], $address);
|
|
|
1561
|
+ $arg = self::tryExpression($tokens, $argToken[0], $argToken[1], $address);
|
|
1562
|
1562
|
if ($arg === null) return null;
|
|
1563
|
|
- $args.push($arg);
|
|
|
1563
|
+ array_push($args, $arg);
|
|
1564
|
1564
|
}
|
|
1565
|
1565
|
return $args;
|
|
1566
|
1566
|
}
|
|
1567
|
1567
|
|
|
1568
|
1568
|
/**
|
|
1569
|
1569
|
* @param CellExpressionToken[] $tokens
|
|
1570
|
|
- * @param number $start
|
|
1571
|
|
- * @param number $end
|
|
|
1570
|
+ * @param int $start
|
|
|
1571
|
+ * @param int $end
|
|
1572
|
1572
|
* @param CellAddress $address
|
|
1573
|
1573
|
* @return CellExpression|null
|
|
1574
|
1574
|
*/
|
|
|
@@ -1589,8 +1589,8 @@ class CellExpression {
|
|
1589
|
1589
|
|
|
1590
|
1590
|
/**
|
|
1591
|
1591
|
* @param CellExpressionToken[] $tokens
|
|
1592
|
|
- * @param number $start
|
|
1593
|
|
- * @param number $end
|
|
|
1592
|
+ * @param int $start
|
|
|
1593
|
+ * @param int $end
|
|
1594
|
1594
|
* @param CellAddress $address
|
|
1595
|
1595
|
* @return CellExpression|null
|
|
1596
|
1596
|
*/
|
|
|
@@ -1619,8 +1619,8 @@ class CellExpression {
|
|
1619
|
1619
|
|
|
1620
|
1620
|
/**
|
|
1621
|
1621
|
* @param CellExpressionToken[] $tokens
|
|
1622
|
|
- * @param number $start
|
|
1623
|
|
- * @param number $end
|
|
|
1622
|
+ * @param int $start
|
|
|
1623
|
+ * @param int $end
|
|
1624
|
1624
|
* @param CellAddress $address
|
|
1625
|
1625
|
* @return CellExpression|null
|
|
1626
|
1626
|
*/
|
|
|
@@ -1637,9 +1637,9 @@ class CellExpression {
|
|
1637
|
1637
|
$parenLevel--;
|
|
1638
|
1638
|
} elseif ($parenLevel == 0 && $i > $start && $i < $end) {
|
|
1639
|
1639
|
$op = $tokens[$i]->type->name;
|
|
1640
|
|
- $priority = $this->infixPriority[$op] ?? false;
|
|
|
1640
|
+ $priority = self::infixPriority[$op] ?? false;
|
|
1641
|
1641
|
if ($priority === false) continue;
|
|
1642
|
|
- array_push($candidates, [ 'priority' => priority, 'i' => $i ]);
|
|
|
1642
|
+ array_push($candidates, [ 'priority' => $priority, 'i' => $i ]);
|
|
1643
|
1643
|
}
|
|
1644
|
1644
|
}
|
|
1645
|
1645
|
usort($candidates, fn($a, $b) => $a['priority'] - $b['priority']);
|
|
|
@@ -1647,9 +1647,9 @@ class CellExpression {
|
|
1647
|
1647
|
foreach ($candidates as $candidate) {
|
|
1648
|
1648
|
try {
|
|
1649
|
1649
|
$i = $candidate['i'];
|
|
1650
|
|
- $operand1 = $this->tryExpression($tokens, $start, $i - 1, $address);
|
|
|
1650
|
+ $operand1 = self::tryExpression($tokens, $start, $i - 1, $address);
|
|
1651
|
1651
|
if ($operand1 === null) continue;
|
|
1652
|
|
- $operand2 = $this->tryExpression($tokens, $i + 1, $end, $address);
|
|
|
1652
|
+ $operand2 = self::tryExpression($tokens, $i + 1, $end, $address);
|
|
1653
|
1653
|
if ($operand2 === null) continue;
|
|
1654
|
1654
|
$bestCandidate = $candidate;
|
|
1655
|
1655
|
break;
|
|
|
@@ -1662,7 +1662,7 @@ class CellExpression {
|
|
1662
|
1662
|
if ($bestCandidate === null) {
|
|
1663
|
1663
|
return null;
|
|
1664
|
1664
|
}
|
|
1665
|
|
- $i = $bestCandidate->i;
|
|
|
1665
|
+ $i = $bestCandidate['i'];
|
|
1666
|
1666
|
switch ($tokens[$bestCandidate['i']]->type) {
|
|
1667
|
1667
|
case CellExpressionTokenType::Plus:
|
|
1668
|
1668
|
return new CellExpression(CellExpressionOperation::Add, [ $operand1, $operand2 ]);
|
|
|
@@ -1692,8 +1692,8 @@ class CellExpression {
|
|
1692
|
1692
|
|
|
1693
|
1693
|
/**
|
|
1694
|
1694
|
* @param CellExpressionToken[] $tokens
|
|
1695
|
|
- * @param number $start
|
|
1696
|
|
- * @param number $end
|
|
|
1695
|
+ * @param int $start
|
|
|
1696
|
+ * @param int $end
|
|
1697
|
1697
|
* @param CellAddress $address
|
|
1698
|
1698
|
* @return CellExpression|null
|
|
1699
|
1699
|
*/
|
|
|
@@ -1707,7 +1707,7 @@ class CellExpression {
|
|
1707
|
1707
|
];
|
|
1708
|
1708
|
foreach ($ops as $op) {
|
|
1709
|
1709
|
if ($tokens[$start]->type != $op[0]) continue;
|
|
1710
|
|
- $operand = $this->tryExpression($tokens, $start + 1, $end, $address);
|
|
|
1710
|
+ $operand = self::tryExpression($tokens, $start + 1, $end, $address);
|
|
1711
|
1711
|
if ($operand === null) return null;
|
|
1712
|
1712
|
return new CellExpression($op[1], [ $operand ]);
|
|
1713
|
1713
|
}
|
|
|
@@ -1744,12 +1744,12 @@ class CellAddress {
|
|
1744
|
1744
|
* Whether the column should remain unchanged when transposed. This is
|
|
1745
|
1745
|
* symbolized by prefixing the column name with a `$` (e.g. `$C3`).
|
|
1746
|
1746
|
*/
|
|
1747
|
|
- public bool $isColumnFixed = false;
|
|
|
1747
|
+ public bool $isColumnFixed;
|
|
1748
|
1748
|
|
|
1749
|
1749
|
/**
|
|
1750
|
1750
|
* Zero-based column index.
|
|
1751
|
1751
|
*/
|
|
1752
|
|
- public int $columnIndex = -1;
|
|
|
1752
|
+ public int $columnIndex;
|
|
1753
|
1753
|
|
|
1754
|
1754
|
/**
|
|
1755
|
1755
|
* Letter code for the column.
|
|
|
@@ -1760,12 +1760,12 @@ class CellAddress {
|
|
1760
|
1760
|
* Whether the row should remain unchanged when transposed. This is
|
|
1761
|
1761
|
* symbolized by prefixing the row number with a `$` (e.g. `C$3`).
|
|
1762
|
1762
|
*/
|
|
1763
|
|
- public bool $isRowFixed = false;
|
|
|
1763
|
+ public bool $isRowFixed;
|
|
1764
|
1764
|
|
|
1765
|
1765
|
/**
|
|
1766
|
1766
|
* Zero-based row index.
|
|
1767
|
1767
|
*/
|
|
1768
|
|
- public int $rowIndex = -1;
|
|
|
1768
|
+ public int $rowIndex;
|
|
1769
|
1769
|
|
|
1770
|
1770
|
/**
|
|
1771
|
1771
|
* One-based row number. This is the human-facing row number.
|
|
|
@@ -1775,7 +1775,7 @@ class CellAddress {
|
|
1775
|
1775
|
/**
|
|
1776
|
1776
|
* Whether this address has both a definite column and row.
|
|
1777
|
1777
|
*/
|
|
1778
|
|
- public function isResolved(): bool { return $this->columnIndex >= 0 && $this->rowIndex >= 0; }
|
|
|
1778
|
+ public bool $isResolved;
|
|
1779
|
1779
|
|
|
1780
|
1780
|
/**
|
|
1781
|
1781
|
* @param int $columnIndex 0-based column index
|
|
|
@@ -1797,6 +1797,7 @@ class CellAddress {
|
|
1797
|
1797
|
$this->rowIndex = $rowIndex;
|
|
1798
|
1798
|
$this->isColumnFixed = $isColumnFixed;
|
|
1799
|
1799
|
$this->isRowFixed = $isRowFixed;
|
|
|
1800
|
+ $this->isResolved = ($columnIndex >= 0 && $rowIndex >= 0);
|
|
1800
|
1801
|
$this->name = self::formatAddress($columnIndex, $rowIndex, $isColumnFixed, $isRowFixed);
|
|
1801
|
1802
|
}
|
|
1802
|
1803
|
|
|
|
@@ -1828,13 +1829,13 @@ class CellAddress {
|
|
1828
|
1829
|
* @param CellAddress $relativeFrom original address of the formula
|
|
1829
|
1830
|
* @param CellAddress $relativeTo address where the formula is being
|
|
1830
|
1831
|
* repeated
|
|
1831
|
|
- * @param boolean $resolveToRow whether to fill in a row number if this
|
|
|
1832
|
+ * @param bool $resolveToRow whether to fill in a row number if this
|
|
1832
|
1833
|
* address doesn't have one
|
|
1833
|
1834
|
* @return CellAddress|null resolved address, or `null` if out of bounds
|
|
1834
|
1835
|
*/
|
|
1835
|
1836
|
public function transpose(CellAddress $relativeFrom, CellAddress $relativeTo,
|
|
1836
|
1837
|
bool $resolveToRow = true): ?CellAddress {
|
|
1837
|
|
- if (!$relativeFrom->isResolved() || !$relativeTo->isResolved()) {
|
|
|
1838
|
+ if (!$relativeFrom->isResolved || !$relativeTo->isResolved) {
|
|
1838
|
1839
|
throw new CellEvaluationException("Can only transpose to and from resolved addresses");
|
|
1839
|
1840
|
}
|
|
1840
|
1841
|
$newColumnIndex = $this->columnIndex;
|
|
|
@@ -1846,7 +1847,7 @@ class CellAddress {
|
|
1846
|
1847
|
if (!$this->isResolved && $resolveToRow) {
|
|
1847
|
1848
|
$newRowIndex = $relativeFrom->rowIndex;
|
|
1848
|
1849
|
}
|
|
1849
|
|
- if ($newRowIndex != -1 && !$this->isRowAbsolute) {
|
|
|
1850
|
+ if ($newRowIndex != -1 && !$this->isRowFixed) {
|
|
1850
|
1851
|
$rowDelta = $relativeTo->rowIndex - $relativeFrom->rowIndex;
|
|
1851
|
1852
|
$newRowIndex += $rowDelta;
|
|
1852
|
1853
|
}
|
|
|
@@ -1891,7 +1892,7 @@ class CellAddress {
|
|
1891
|
1892
|
$ACodepoint = ord('A');
|
|
1892
|
1893
|
$remaining = $columnIndex;
|
|
1893
|
1894
|
do {
|
|
1894
|
|
- $letters = chr(ACodepoint + (remaining % 26)) . $letters;
|
|
|
1895
|
+ $letters = chr($ACodepoint + ($remaining % 26)) . $letters;
|
|
1895
|
1896
|
$remaining = floor($remaining / 26);
|
|
1896
|
1897
|
} while ($remaining > 0);
|
|
1897
|
1898
|
}
|
|
|
@@ -1904,7 +1905,7 @@ class CellAddress {
|
|
1904
|
1905
|
if ($isColumnFixed && $columnIndex >= 0) $addr .= '$';
|
|
1905
|
1906
|
if ($columnIndex >= 0) $addr .= self::columnIndexToLetters($columnIndex);
|
|
1906
|
1907
|
if ($isRowFixed && $rowIndex >= 0) $addr .= '$';
|
|
1907
|
|
- if ($rowIndex >= 0) $addr .= "${rowIndex + 1}";
|
|
|
1908
|
+ if ($rowIndex >= 0) $addr .= ($rowIndex + 1);
|
|
1908
|
1909
|
return $addr;
|
|
1909
|
1910
|
}
|
|
1910
|
1911
|
|
|
|
@@ -1914,7 +1915,7 @@ class CellAddress {
|
|
1914
|
1915
|
* @param string $address cell address string
|
|
1915
|
1916
|
* @param ?CellAddress $relativeTo address to resolve relative
|
|
1916
|
1917
|
* addresses against
|
|
1917
|
|
- * @param boolean $throwIfInvalid whether to throw an error if address
|
|
|
1918
|
+ * @param bool $throwIfInvalid whether to throw an error if address
|
|
1918
|
1919
|
* is invalid
|
|
1919
|
1920
|
* @return ?CellAddress address, if parsable
|
|
1920
|
1921
|
* @throws CellEvaluationException if the address is invalid and
|
|
|
@@ -1922,7 +1923,7 @@ class CellAddress {
|
|
1922
|
1923
|
*/
|
|
1923
|
1924
|
public static function fromString(string $address, ?CellAddress $relativeTo=null,
|
|
1924
|
1925
|
bool $throwIfInvalid=false): ?CellAddress {
|
|
1925
|
|
- if (!mb_eregi('^(\\$?)([A-Z]{1,2}?)((?:\\$(?=[0-9]))?)([0-9]*)$/', $address, $groups)) {
|
|
|
1926
|
+ if (!mb_eregi('^(\\$?)([A-Z]{1,2}?)((?:\\$(?=[0-9]))?)([0-9]*)$', $address, $groups)) {
|
|
1926
|
1927
|
if ($throwIfInvalid) throw new CellEvaluationException("Bad address \"{$address}\"", '#REF');
|
|
1927
|
1928
|
return null;
|
|
1928
|
1929
|
}
|
|
|
@@ -1930,7 +1931,7 @@ class CellAddress {
|
|
1930
|
1931
|
$letters = mb_strtoupper($groups[2]);
|
|
1931
|
1932
|
$isRowFixed = ($groups[3] == '$');
|
|
1932
|
1933
|
$numbers = $groups[4];
|
|
1933
|
|
- $columnIndex = $this->lettersToColumnIndex($letters);
|
|
|
1934
|
+ $columnIndex = self::lettersToColumnIndex($letters);
|
|
1934
|
1935
|
$rowIndex = (mb_strlen($numbers) == 0) ? -1 : intval($numbers) - 1;
|
|
1935
|
1936
|
if ($columnIndex < 0 && $relativeTo !== null) $columnIndex = $relativeTo->columnIndex;
|
|
1936
|
1937
|
if ($rowIndex < 0 && $relativeTo !== null) $rowIndex = $relativeTo->rowIndex;
|
|
|
@@ -1957,7 +1958,7 @@ class CellAddressRange {
|
|
1957
|
1958
|
* @param CellAddress $toCell
|
|
1958
|
1959
|
*/
|
|
1959
|
1960
|
public function __construct(CellAddress $fromCell, CellAddress $toCell) {
|
|
1960
|
|
- if ($fromCell->isResolved() != $toCell->isResolved()) {
|
|
|
1961
|
+ if ($fromCell->isResolved != $toCell->isResolved) {
|
|
1961
|
1962
|
throw new CellEvaluationException("Cannot mix resolved and unresolved cell addresses in range: {$fromCell->name} and {$toCell->name}");
|
|
1962
|
1963
|
}
|
|
1963
|
1964
|
$this->minColumnIndex = min($fromCell->columnIndex, $toCell->columnIndex);
|
|
|
@@ -1971,14 +1972,14 @@ class CellAddressRange {
|
|
1971
|
1972
|
}
|
|
1972
|
1973
|
|
|
1973
|
1974
|
/**
|
|
1974
|
|
- * Creates an iterator for every `CellAddress` in this range within the
|
|
|
1975
|
+ * Creates an iterator for every `CellAddress` string in this range within the
|
|
1975
|
1976
|
* confines of the given grid's dimensions. Iterates each row in the first
|
|
1976
|
1977
|
* column, then each row in the second, etc. Iteration range is inclusive
|
|
1977
|
1978
|
* of the min and max extents.
|
|
1978
|
1979
|
*
|
|
1979
|
1980
|
* Example:
|
|
1980
|
1981
|
* ```
|
|
1981
|
|
- * foreach (range.cellsIn(grid) as $address) {
|
|
|
1982
|
+ * foreach (range.cellsIn(grid) as $addressString => $cell) {
|
|
1982
|
1983
|
* ...
|
|
1983
|
1984
|
* }
|
|
1984
|
1985
|
* ```
|
|
|
@@ -1992,12 +1993,12 @@ class CellAddressRange {
|
|
1992
|
1993
|
$min_row = $this->minRowIndex;
|
|
1993
|
1994
|
$max_row = $this->maxRowIndex;
|
|
1994
|
1995
|
if ($min_row == -1) {
|
|
1995
|
|
- $min_row = $grid->getHeaderRowCount();
|
|
|
1996
|
+ $min_row = 0;
|
|
1996
|
1997
|
}
|
|
1997
|
1998
|
if ($max_row == -1) {
|
|
1998
|
|
- $max_row = $grid->getRowCount() - 1;
|
|
|
1999
|
+ $max_row = $grid->rowCount - 1;
|
|
1999
|
2000
|
}
|
|
2000
|
|
- $max_col = min($max_col, $grid->getColumnCount());
|
|
|
2001
|
+ $max_col = min($max_col, $grid->columnCount);
|
|
2001
|
2002
|
return new class($grid, $min_col, $max_col, $min_row, $max_row) implements Iterator {
|
|
2002
|
2003
|
private SpreadsheetGrid $grid;
|
|
2003
|
2004
|
private int $min_col;
|
|
|
@@ -2023,7 +2024,7 @@ class CellAddressRange {
|
|
2023
|
2024
|
if ($this->col >= $this->min_col && $this->col <= $this->max_col &&
|
|
2024
|
2025
|
$this->row >= $this->min_row && $this->row <= $this->max_row) {
|
|
2025
|
2026
|
$this->address = new CellAddress($this->col, $this->row);
|
|
2026
|
|
- $this->cell = $this->grid->getCell($this->address);
|
|
|
2027
|
+ $this->cell = $this->grid->cellAt($this->address);
|
|
2027
|
2028
|
if (!$this->cell) {
|
|
2028
|
2029
|
error_log("WARNING: Iterator found no cell at {$this->address->name}");
|
|
2029
|
2030
|
}
|
|
|
@@ -2072,36 +2073,36 @@ class CellAddressRange {
|
|
2072
|
2073
|
*/
|
|
2073
|
2074
|
class CellValue {
|
|
2074
|
2075
|
/**
|
|
2075
|
|
- * Blank cell. `value` is `null`.
|
|
|
2076
|
+ * Blank cell. `$value` is `null`.
|
|
2076
|
2077
|
*/
|
|
2077
|
2078
|
public const TYPE_BLANK = 'blank';
|
|
2078
|
2079
|
/**
|
|
2079
|
|
- * Currency value. `value` is `number`.
|
|
|
2080
|
+ * Currency value. `$value` is `float`.
|
|
2080
|
2081
|
*/
|
|
2081
|
2082
|
public const TYPE_CURRENCY = 'currency';
|
|
2082
|
2083
|
/**
|
|
2083
|
|
- * Regular number value. `value` is `number`.
|
|
|
2084
|
+ * Regular number value. `$value` is `float`.
|
|
2084
|
2085
|
*/
|
|
2085
|
2086
|
public const TYPE_NUMBER = 'number';
|
|
2086
|
2087
|
/**
|
|
2087
|
|
- * Percentage. `value` is `number`, represented as a ratio (100% = 1.0).
|
|
|
2088
|
+ * Percentage. `$value` is `float`, represented as a ratio (100% = 1.0).
|
|
2088
|
2089
|
*/
|
|
2089
|
2090
|
public const TYPE_PERCENT = 'percent';
|
|
2090
|
2091
|
/**
|
|
2091
|
|
- * Unaltered text value. `value` is `string`.
|
|
|
2092
|
+ * Unaltered text value. `$value` is `string`.
|
|
2092
|
2093
|
*/
|
|
2093
|
2094
|
public const TYPE_STRING = 'string';
|
|
2094
|
2095
|
/**
|
|
2095
|
|
- * Boolean. `value` is `boolean`.
|
|
|
2096
|
+ * Boolean. `$value` is `bool`.
|
|
2096
|
2097
|
*/
|
|
2097
|
2098
|
public const TYPE_BOOLEAN = 'boolean';
|
|
2098
|
2099
|
/**
|
|
2099
|
2100
|
* A formula that has resulted in an error during parsing or evaluation.
|
|
2100
|
|
- * `value` is `string` error message.
|
|
|
2101
|
+ * `$value` is `string` error message.
|
|
2101
|
2102
|
*/
|
|
2102
|
2103
|
public const TYPE_ERROR = 'error';
|
|
2103
|
2104
|
/**
|
|
2104
|
|
- * A formula expression. `value` is `string` and includes the leading `=`.
|
|
|
2105
|
+ * A formula expression. `$value` is `string` and includes the leading `=`.
|
|
2105
|
2106
|
*/
|
|
2106
|
2107
|
public const TYPE_FORMULA = 'formula';
|
|
2107
|
2108
|
|
|
|
@@ -2136,10 +2137,10 @@ class CellValue {
|
|
2136
|
2137
|
string $type = CellValue::TYPE_STRING,
|
|
2137
|
2138
|
int $decimals = 0
|
|
2138
|
2139
|
) {
|
|
2139
|
|
- $this->formattedValue = formattedValue;
|
|
2140
|
|
- $this->value = value;
|
|
2141
|
|
- $this->type = type;
|
|
2142
|
|
- $this->decimals = decimals;
|
|
|
2140
|
+ $this->formattedValue = $formattedValue;
|
|
|
2141
|
+ $this->value = $value;
|
|
|
2142
|
+ $this->type = $type;
|
|
|
2143
|
+ $this->decimals = $decimals;
|
|
2143
|
2144
|
}
|
|
2144
|
2145
|
|
|
2145
|
2146
|
/**
|
|
|
@@ -2176,16 +2177,16 @@ class CellValue {
|
|
2176
|
2177
|
}
|
|
2177
|
2178
|
if ($value instanceof Error) {
|
|
2178
|
2179
|
if ($value instanceof CellException) {
|
|
2179
|
|
- return new CellValue($value->errorSymbol, $value->message, CellValue::TYPE_ERROR);
|
|
|
2180
|
+ return new CellValue($value->errorSymbol, $value->getMessage(), CellValue::TYPE_ERROR);
|
|
2180
|
2181
|
}
|
|
2181
|
|
- return new CellValue('#ERROR', $value->message, CellValue::TYPE_ERROR);
|
|
|
2182
|
+ return new CellValue('#ERROR', $value->getMessage(), CellValue::TYPE_ERROR);
|
|
2182
|
2183
|
}
|
|
2183
|
2184
|
if (is_bool($value)) {
|
|
2184
|
2185
|
$formatted = CellValue::formatType($value, CellValue::TYPE_BOOLEAN, 0);
|
|
2185
|
2186
|
return new CellValue($formatted, $value, CellValue::TYPE_BOOLEAN);
|
|
2186
|
2187
|
}
|
|
2187
|
2188
|
if (is_numeric($value)) {
|
|
2188
|
|
- $resolvedType = $type || CellValue::TYPE_NUMBER;
|
|
|
2189
|
+ $resolvedType = $type ?? CellValue::TYPE_NUMBER;
|
|
2189
|
2190
|
$resolvedDecimals = ($decimals !== null) ? $decimals :
|
|
2190
|
2191
|
($resolvedType == CellValue::TYPE_CURRENCY ? 2 :
|
|
2191
|
2192
|
CellValue::autodecimals($resolvedType == CellValue::TYPE_PERCENT ?
|
|
|
@@ -2298,7 +2299,7 @@ class CellValue {
|
|
2298
|
2299
|
$wholes = mb_eregi_replace(',', '', $groups[1]);
|
|
2299
|
2300
|
$this->type = CellValue::TYPE_NUMBER;
|
|
2300
|
2301
|
$this->decimals = 0;
|
|
2301
|
|
- $this->value = parseFloat($wholes);
|
|
|
2302
|
+ $this->value = floatval($wholes);
|
|
2302
|
2303
|
$this->formattedValue = CellValue::formatNumber($this->value, $this->decimals);
|
|
2303
|
2304
|
return;
|
|
2304
|
2305
|
}
|
|
|
@@ -2358,7 +2359,7 @@ class CellValue {
|
|
2358
|
2359
|
case CellValue::TYPE_CURRENCY:
|
|
2359
|
2360
|
case CellValue::TYPE_NUMBER:
|
|
2360
|
2361
|
case CellValue::TYPE_PERCENT:
|
|
2361
|
|
- return $formatted ? $this->formattedValue : "${$this->value}";
|
|
|
2362
|
+ return $formatted ? $this->formattedValue : "{$this->value}";
|
|
2362
|
2363
|
case CellValue::TYPE_STRING:
|
|
2363
|
2364
|
return $formatted ? $this->formattedValue : $this->value;
|
|
2364
|
2365
|
case CellValue::TYPE_ERROR:
|
|
|
@@ -2522,23 +2523,23 @@ class CellValue {
|
|
2522
|
2523
|
* @param CellValue $a operand A
|
|
2523
|
2524
|
* @param CellValue $b operand B
|
|
2524
|
2525
|
* @param string $op operator symbol
|
|
2525
|
|
- * @return array 3-element tuple array with A number value, B number value,
|
|
|
2526
|
+ * @return array 3-element tuple array with A float value, B float value,
|
|
2526
|
2527
|
* and result type string
|
|
2527
|
2528
|
* @throws CellEvaluationException if types are incompatible for numeric operations
|
|
2528
|
2529
|
*/
|
|
2529
|
2530
|
private static function resolveNumericOperands(CellValue $a, CellValue $b, string $op): array {
|
|
2530
|
|
- if ($a->type == $this->TYPE_ERROR) throw $a->value;
|
|
2531
|
|
- if ($b->type == $this->TYPE_ERROR) throw $b->value;
|
|
2532
|
|
- if ($a->type == $this->TYPE_STRING || $b->type == $this->TYPE_STRING) {
|
|
|
2531
|
+ if ($a->type == self::TYPE_ERROR) throw $a->value;
|
|
|
2532
|
+ if ($b->type == self::TYPE_ERROR) throw $b->value;
|
|
|
2533
|
+ if ($a->type == self::TYPE_STRING || $b->type == self::TYPE_STRING) {
|
|
2533
|
2534
|
throw new CellEvaluationException("Cannot perform math on text values");
|
|
2534
|
2535
|
}
|
|
2535
|
2536
|
|
|
2536
|
|
- if ($a->type == $this->TYPE_BLANK) {
|
|
2537
|
|
- if ($b->type == $this->TYPE_BLANK) {
|
|
2538
|
|
- return [ 0, 0, $this->TYPE_NUMBER, 0 ];
|
|
|
2537
|
+ if ($a->type == self::TYPE_BLANK) {
|
|
|
2538
|
+ if ($b->type == self::TYPE_BLANK) {
|
|
|
2539
|
+ return [ 0, 0, self::TYPE_NUMBER, 0 ];
|
|
2539
|
2540
|
}
|
|
2540
|
2541
|
return [ 0, $b->value, $b->type ];
|
|
2541
|
|
- } elseif ($b->type == $this->TYPE_BLANK) {
|
|
|
2542
|
+ } elseif ($b->type == self::TYPE_BLANK) {
|
|
2542
|
2543
|
return [ $a->value, 0, $a->type ];
|
|
2543
|
2544
|
}
|
|
2544
|
2545
|
|
|
|
@@ -2549,30 +2550,30 @@ class CellValue {
|
|
2549
|
2550
|
}
|
|
2550
|
2551
|
|
|
2551
|
2552
|
switch ($a->type . $b->type) {
|
|
2552
|
|
- case $this->TYPE_CURRENCY . $this->TYPE_NUMBER:
|
|
2553
|
|
- case $this->TYPE_CURRENCY . $this->TYPE_PERCENT:
|
|
2554
|
|
- return [ $a->value, $b->value, $this->TYPE_CURRENCY ];
|
|
2555
|
|
- case $this->TYPE_PERCENT . $this->TYPE_CURRENCY:
|
|
|
2553
|
+ case self::TYPE_CURRENCY . self::TYPE_NUMBER:
|
|
|
2554
|
+ case self::TYPE_CURRENCY . self::TYPE_PERCENT:
|
|
|
2555
|
+ return [ $a->value, $b->value, self::TYPE_CURRENCY ];
|
|
|
2556
|
+ case self::TYPE_PERCENT . self::TYPE_CURRENCY:
|
|
2556
|
2557
|
return [ $a->value, $b->value,
|
|
2557
|
|
- $isMultOrDiv ? $this->TYPE_CURRENCY : $this->TYPE_PERCENT ];
|
|
2558
|
|
- case $this->TYPE_PERCENT . $this->TYPE_NUMBER:
|
|
|
2558
|
+ $isMultOrDiv ? self::TYPE_CURRENCY : self::TYPE_PERCENT ];
|
|
|
2559
|
+ case self::TYPE_PERCENT . self::TYPE_NUMBER:
|
|
2559
|
2560
|
return [ $a->value, $b->value,
|
|
2560
|
|
- $isMultOrDiv ? $this->TYPE_NUMBER : $this->TYPE_PERCENT ];
|
|
2561
|
|
- case $this->TYPE_NUMBER . $this->TYPE_CURRENCY:
|
|
|
2561
|
+ $isMultOrDiv ? self::TYPE_NUMBER : self::TYPE_PERCENT ];
|
|
|
2562
|
+ case self::TYPE_NUMBER . self::TYPE_CURRENCY:
|
|
2562
|
2563
|
return [ $a->value, $b->value, $b->type ];
|
|
2563
|
|
- case $this->TYPE_NUMBER . $this->TYPE_PERCENT:
|
|
|
2564
|
+ case self::TYPE_NUMBER . self::TYPE_PERCENT:
|
|
2564
|
2565
|
return [ $a->value, $b->value,
|
|
2565
|
|
- $isMultOrDiv ? $this->TYPE_NUMBER : $b->type ];
|
|
2566
|
|
- case $this->TYPE_BOOLEAN . $this->TYPE_CURRENCY:
|
|
2567
|
|
- case $this->TYPE_BOOLEAN . $this->TYPE_NUMBER:
|
|
2568
|
|
- case $this->TYPE_BOOLEAN . $this->TYPE_PERCENT:
|
|
|
2566
|
+ $isMultOrDiv ? self::TYPE_NUMBER : $b->type ];
|
|
|
2567
|
+ case self::TYPE_BOOLEAN . self::TYPE_CURRENCY:
|
|
|
2568
|
+ case self::TYPE_BOOLEAN . self::TYPE_NUMBER:
|
|
|
2569
|
+ case self::TYPE_BOOLEAN . self::TYPE_PERCENT:
|
|
2569
|
2570
|
return [ $a->value ? 1 : 0, $b->value, $b->type ];
|
|
2570
|
|
- case $this->TYPE_CURRENCY . $this->TYPE_BOOLEAN:
|
|
2571
|
|
- case $this->TYPE_NUMBER . $this->TYPE_BOOLEAN:
|
|
2572
|
|
- case $this->TYPE_PERCENT . $this->TYPE_BOOLEAN:
|
|
|
2571
|
+ case self::TYPE_CURRENCY . self::TYPE_BOOLEAN:
|
|
|
2572
|
+ case self::TYPE_NUMBER . self::TYPE_BOOLEAN:
|
|
|
2573
|
+ case self::TYPE_PERCENT . self::TYPE_BOOLEAN:
|
|
2573
|
2574
|
return [ $a->value, $b->value ? 1 : 0, $a->type ];
|
|
2574
|
2575
|
}
|
|
2575
|
|
- throw new CellEvaluationException(`Unhandled operand types "${a.type}" and "${b.type}"`);
|
|
|
2576
|
+ throw new CellEvaluationException("Unhandled operand types \"{$a->type}\" and \"{$b->type}\"");
|
|
2576
|
2577
|
}
|
|
2577
|
2578
|
|
|
2578
|
2579
|
/**
|
|
|
@@ -2648,6 +2649,7 @@ class CellValue {
|
|
2648
|
2649
|
case CellValue::TYPE_BOOLEAN: return $value ? 'TRUE' : 'FALSE';
|
|
2649
|
2650
|
case CellValue::TYPE_STRING: return "{$value}";
|
|
2650
|
2651
|
case CellValue::TYPE_FORMULA: return "{$value}";
|
|
|
2652
|
+ default: throw new CellException("Cannot format value of type {$type}");
|
|
2651
|
2653
|
}
|
|
2652
|
2654
|
}
|
|
2653
|
2655
|
|
|
|
@@ -2660,7 +2662,7 @@ class CellValue {
|
|
2660
|
2662
|
if (str_starts_with($s, '-')) {
|
|
2661
|
2663
|
return '-$' . mb_substr($s, 1);
|
|
2662
|
2664
|
}
|
|
2663
|
|
- return '$' . s;
|
|
|
2665
|
+ return '$' . $s;
|
|
2664
|
2666
|
}
|
|
2665
|
2667
|
|
|
2666
|
2668
|
private static function formatPercent(float|int $value, int $decimals): string {
|
|
|
@@ -2679,6 +2681,7 @@ class CellValue {
|
|
2679
|
2681
|
$s = number_format($value, $maxDigits);
|
|
2680
|
2682
|
if (strpos($s, '.') === false) return 0;
|
|
2681
|
2683
|
$fraction = explode('.', $s)[1];
|
|
|
2684
|
+ $fraction = rtrim($fraction, '0');
|
|
2682
|
2685
|
return min($maxDigits, mb_strlen($fraction));
|
|
2683
|
2686
|
}
|
|
2684
|
2687
|
return 0;
|
|
|
@@ -2830,7 +2833,7 @@ class MDSpreadsheetReader extends MDReader {
|
|
2830
|
2833
|
// Copy results back to table
|
|
2831
|
2834
|
for ($c = 0; $c < $columnCount; $c++) {
|
|
2832
|
2835
|
for ($r = 0; $r < $rowCount; $r++) {
|
|
2833
|
|
- $cellNode = $tableNode->bodyRows[$r]->children[$c];
|
|
|
2836
|
+ $cellNode = $tableNode->bodyRows()[$r]->children[$c];
|
|
2834
|
2837
|
if ($cellNode === null) continue;
|
|
2835
|
2838
|
$gridCell = $grid->cells[$c][$r];
|
|
2836
|
2839
|
$gridValue = $gridCell->outputValue;
|