Sfoglia il codice sorgente

Spreadsheet PHP mostly working

main
Rocketsoup 1 anno fa
parent
commit
0e986f8081
4 ha cambiato i file con 202 aggiunte e 187 eliminazioni
  1. 1
    1
      js/spreadsheet.js
  2. 1
    1
      js/spreadsheet.min.js
  3. 178
    175
      php/spreadsheet.php
  4. 22
    10
      playgroundapi.php

+ 1
- 1
js/spreadsheet.js Vedi File

1933
 		if (!this.isResolved && resolveToRow) {
1933
 		if (!this.isResolved && resolveToRow) {
1934
 			newRowIndex = relativeFrom.rowIndex;
1934
 			newRowIndex = relativeFrom.rowIndex;
1935
 		}
1935
 		}
1936
-		if (newRowIndex != -1 && !this.isRowAbsolute) {
1936
+		if (newRowIndex != -1 && !this.isRowFixed) {
1937
 			const rowDelta = relativeTo.rowIndex - relativeFrom.rowIndex;
1937
 			const rowDelta = relativeTo.rowIndex - relativeFrom.rowIndex;
1938
 			newRowIndex += rowDelta;
1938
 			newRowIndex += rowDelta;
1939
 		}
1939
 		}

+ 1
- 1
js/spreadsheet.min.js
File diff soppresso perché troppo grande
Vedi File


+ 178
- 175
php/spreadsheet.php Vedi File

85
  * `Function`.
85
  * `Function`.
86
  */
86
  */
87
 enum CellExpressionOperation {
87
 enum CellExpressionOperation {
88
-	/** Arg is `number` */
88
+	/** Arg is `float` */
89
 	case Number;
89
 	case Number;
90
 	/** Arg is `string` without quotes */
90
 	/** Arg is `string` without quotes */
91
 	case String;
91
 	case String;
92
-	/** Arg is `boolean` */
92
+	/** Arg is `bool` */
93
 	case Boolean;
93
 	case Boolean;
94
 	/** Arg is `CellAddress` */
94
 	/** Arg is `CellAddress` */
95
 	case Reference;
95
 	case Reference;
154
 		/** @$CellAddress[] */
154
 		/** @$CellAddress[] */
155
 		$expressionAddressQueue = [];
155
 		$expressionAddressQueue = [];
156
 		$range = new CellAddressRange(new CellAddress(0, 0), new CellAddress($colCount - 1, $rowCount - 1));
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
 			$value = $cell->originalValue;
162
 			$value = $cell->originalValue;
160
 			if ($value->type != CellValue::TYPE_FORMULA) {
163
 			if ($value->type != CellValue::TYPE_FORMULA) {
161
 				$cell->outputValue = $value;
164
 				$cell->outputValue = $value;
235
 	 */
238
 	 */
236
 	private function enqueueFilledBlanks(CellExpression $expression, array &$addresses) {
239
 	private function enqueueFilledBlanks(CellExpression $expression, array &$addresses) {
237
 		foreach ($expression->fillRanges ?? [] as $range) {
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
 				if ($filledCell->originalValue->type === CellValue::TYPE_BLANK &&
243
 				if ($filledCell->originalValue->type === CellValue::TYPE_BLANK &&
241
 						(!$filledCell->outputValue ||
244
 						(!$filledCell->outputValue ||
242
 						$filledCell->outputValue->type === CellValue::TYPE_BLANK)) {
245
 						$filledCell->outputValue->type === CellValue::TYPE_BLANK)) {
309
 			case CellExpressionOperation::Range: {
312
 			case CellExpressionOperation::Range: {
310
 				$range = $expr->arguments[0];
313
 				$range = $expr->arguments[0];
311
 				$values = [];
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
 					if ($rAddress->equals($address)) continue;
317
 					if ($rAddress->equals($address)) continue;
315
 					$val = $this->grid->outputValueAt($rAddress);
318
 					$val = $this->grid->outputValueAt($rAddress);
316
 					if ($val === null) {
319
 					if ($val === null) {
653
 		}
656
 		}
654
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
657
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
655
 		for ($i = 0; $i < sizeof($evaled) - 1; $i += 2) {
658
 		for ($i = 0; $i < sizeof($evaled) - 1; $i += 2) {
656
-			$test = $evaled[i].booleanValue();
659
+			$test = $evaled[$i].booleanValue();
657
 			if ($test === null) {
660
 			if ($test === null) {
658
 				throw new CellEvaluationException("IFS expects a boolean for argument " . ($i + 1));
661
 				throw new CellEvaluationException("IFS expects a boolean for argument " . ($i + 1));
659
 			}
662
 			}
964
 	public ?string $qualifier;
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
 	public ?string $outputType = null;
972
 	public ?string $outputType = null;
970
 
973
 
1005
 	 * @return ?CellExpression  parsed expression, or `null` if it failed
1008
 	 * @return ?CellExpression  parsed expression, or `null` if it failed
1006
 	 */
1009
 	 */
1007
 	public static function parse(string $expression, CellAddress $address): ?CellExpression {
1010
 	public static function parse(string $expression, CellAddress $address): ?CellExpression {
1008
-		$tokens = $this->expressionToTokens($expression);
1011
+		$tokens = self::expressionToTokens($expression);
1009
 		if (sizeof($tokens) == 0) return null;
1012
 		if (sizeof($tokens) == 0) return null;
1010
-		$expr = $this->expressionFromTokens($tokens, $address);
1013
+		$expr = self::expressionFromTokens($tokens, $address);
1011
 		$expr->location = $address;
1014
 		$expr->location = $address;
1012
 		return $expr;
1015
 		return $expr;
1013
 	}
1016
 	}
1047
 	}
1050
 	}
1048
 
1051
 
1049
 	private function clone(): CellExpression {
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
 		$cp->outputType = $this->outputType;
1054
 		$cp->outputType = $this->outputType;
1055
 		$cp->outputDecimals = $this->outputDecimals;
1055
 		$cp->outputDecimals = $this->outputDecimals;
1056
 		$cp->fillRanges = $this->fillRanges !== null ? array($this->fillRanges) : null;
1056
 		$cp->fillRanges = $this->fillRanges !== null ? array($this->fillRanges) : null;
1089
 	 */
1089
 	 */
1090
 	public static function expressionToTokens(string $text): array {
1090
 	public static function expressionToTokens(string $text): array {
1091
 		$tokens = [];
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
 			// Ignore equals
1095
 			// Ignore equals
1096
-			$pos[0]++;
1096
+			$pos++;
1097
 		}
1097
 		}
1098
-		$this->skipWhitespace($text, $pos);
1098
+		self::skipWhitespace($text, $pos);
1099
 		$l = mb_strlen($text);
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
 		return $tokens;
1104
 		return $tokens;
1105
 	}
1105
 	}
1111
 	 */
1111
 	 */
1112
 	private static function readNextToken(string $text, int &$pos): CellExpressionToken {
1112
 	private static function readNextToken(string $text, int &$pos): CellExpressionToken {
1113
 		// Single char tokens
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
 		// Other tokens
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
 		$ch = mb_substr($text, $pos, 1);
1136
 		$ch = mb_substr($text, $pos, 1);
1137
 		throw new CellSyntaxException("Unexpected character \"{$ch}\" at {$pos}");
1137
 		throw new CellSyntaxException("Unexpected character \"{$ch}\" at {$pos}");
1138
 	}
1138
 	}
1166
 		$address = '';
1166
 		$address = '';
1167
 		$isName = true;
1167
 		$isName = true;
1168
 		if ($ch == '$') {
1168
 		if ($ch == '$') {
1169
-			$address += $ch;
1169
+			$address .= $ch;
1170
 			$isName = false;
1170
 			$isName = false;
1171
 			$p++;
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
 		if ($col === null) return null;
1174
 		if ($col === null) return null;
1175
-		$address += $col;
1175
+		$address .= $col;
1176
 		$ch = mb_substr($text, $p, 1);
1176
 		$ch = mb_substr($text, $p, 1);
1177
 		if ($ch == '$') {
1177
 		if ($ch == '$') {
1178
-			$address += $ch;
1178
+			$address .= $ch;
1179
 			$isName = false;
1179
 			$isName = false;
1180
 			$p++;
1180
 			$p++;
1181
-			$row = $this->readChars($text, $p, $this->isDigit, 1);
1181
+			$row = self::readChars($text, $p, fn($ch) => self::isDigit($ch), 1);
1182
 			if ($row === null) return null;
1182
 			if ($row === null) return null;
1183
-			$address += $row;
1183
+			$address .= $row;
1184
 		} else {
1184
 		} else {
1185
-			$row = $this->readChars($text, $p, $this->isDigit, 0);
1185
+			$row = self::readChars($text, $p, fn($ch) => self::isDigit($ch), 0);
1186
 			if ($row === null) return null;
1186
 			if ($row === null) return null;
1187
-			$address += $row;
1187
+			$address .= $row;
1188
 		}
1188
 		}
1189
 		$pos = $p;
1189
 		$pos = $p;
1190
 		return new CellExpressionToken(
1190
 		return new CellExpressionToken(
1194
 
1194
 
1195
 	private static function readNextNameToken(string $text, int &$pos): ?CellExpressionToken {
1195
 	private static function readNextNameToken(string $text, int &$pos): ?CellExpressionToken {
1196
 		$p = $pos;
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
 		if ($name === null) return null;
1198
 		if ($name === null) return null;
1199
 		$pos = $p;
1199
 		$pos = $p;
1200
 		if (CellAddress::isAddress($name)) {
1200
 		if (CellAddress::isAddress($name)) {
1205
 
1205
 
1206
 	private static function readNextNumberToken(string $text, int &$pos): ?CellExpressionToken {
1206
 	private static function readNextNumberToken(string $text, int &$pos): ?CellExpressionToken {
1207
 		$ch = mb_substr($text, $pos, 1);
1207
 		$ch = mb_substr($text, $pos, 1);
1208
-		if (!$this->isDigit($ch)) return null;
1208
+		if (!self::isDigit($ch)) return null;
1209
 		$l = mb_strlen($text);
1209
 		$l = mb_strlen($text);
1210
 		$numStr = $ch;
1210
 		$numStr = $ch;
1211
 		$pos++;
1211
 		$pos++;
1212
 		while ($pos < $l) {
1212
 		while ($pos < $l) {
1213
 			$ch = mb_substr($text, $pos, 1);
1213
 			$ch = mb_substr($text, $pos, 1);
1214
-			if ($this->isDigit($ch)) {
1214
+			if (self::isDigit($ch)) {
1215
 				$pos++;
1215
 				$pos++;
1216
 				$numStr .= $ch;
1216
 				$numStr .= $ch;
1217
 			} else {
1217
 			} else {
1222
 			$ch = mb_substr($text, $pos, 1);
1222
 			$ch = mb_substr($text, $pos, 1);
1223
 			if ($ch == '.') {
1223
 			if ($ch == '.') {
1224
 				$numStr .= $ch;
1224
 				$numStr .= $ch;
1225
-				$pos[0]++;
1225
+				$pos++;
1226
 				while ($pos < $l) {
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
 						$pos++;
1229
 						$pos++;
1230
 						$numStr .= $ch;
1230
 						$numStr .= $ch;
1231
 					} else {
1231
 					} else {
1245
 		$l = mb_strlen($text);
1245
 		$l = mb_strlen($text);
1246
 		$inEscape = false;
1246
 		$inEscape = false;
1247
 		while ($pos < $l) {
1247
 		while ($pos < $l) {
1248
-			$ch = mb_substr($text, $pos[0], $pos[0] + 1);
1248
+			$ch = mb_substr($text, $pos, 1);
1249
 			$pos++;
1249
 			$pos++;
1250
 			if ($inEscape) {
1250
 			if ($inEscape) {
1251
 				$inEscape = false;
1251
 				$inEscape = false;
1259
 			} elseif ($ch == '"') {
1259
 			} elseif ($ch == '"') {
1260
 				return new CellExpressionToken(CellExpressionTokenType::String, $str);
1260
 				return new CellExpressionToken(CellExpressionTokenType::String, $str);
1261
 			} else {
1261
 			} else {
1262
-				$str += $ch;
1262
+				$str .= $ch;
1263
 			}
1263
 			}
1264
 		}
1264
 		}
1265
 		throw new CellSyntaxException('Unterminated string');
1265
 		throw new CellSyntaxException('Unterminated string');
1284
 		while ($p < $l && ($maximumLength === null || $sl < $maximumLength)) {
1284
 		while ($p < $l && ($maximumLength === null || $sl < $maximumLength)) {
1285
 			$ch = mb_substr($text, $p, 1);
1285
 			$ch = mb_substr($text, $p, 1);
1286
 			if (!$charTest($ch)) break;
1286
 			if (!$charTest($ch)) break;
1287
-			$s += $ch;
1287
+			$s .= $ch;
1288
 			$sl++;
1288
 			$sl++;
1289
 			$p++;
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
 			return null;
1292
 			return null;
1293
 		}
1293
 		}
1294
 		if ($minimumLength !== null && $sl < $minimumLength) {
1294
 		if ($minimumLength !== null && $sl < $minimumLength) {
1324
 	 * @return ?CellExpression
1324
 	 * @return ?CellExpression
1325
 	 */
1325
 	 */
1326
 	public static function expressionFromTokens(array $tokens, CellAddress $address): ?CellExpression {
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
 		return null;
1330
 		return null;
1331
 	}
1331
 	}
1332
 
1332
 
1340
 	private static function tryExpressionAndFormat(array $tokens, int $start,
1340
 	private static function tryExpressionAndFormat(array $tokens, int $start,
1341
 			int $end, CellAddress $address): ?CellExpression {
1341
 			int $end, CellAddress $address): ?CellExpression {
1342
 		for ($t = $start + 1; $t < $end; $t++) {
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
 				if ($expr === null) return null;
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
 				if ($format === null) return null;
1348
 				if ($format === null) return null;
1349
 				[ $expr->outputType, $expr->outputDecimals ] = $format;
1349
 				[ $expr->outputType, $expr->outputDecimals ] = $format;
1350
 				return $expr;
1350
 				return $expr;
1367
 		if (!$tokens[$end]->type->isPotentialName()) return null;
1367
 		if (!$tokens[$end]->type->isPotentialName()) return null;
1368
 		$name = mb_strtoupper($tokens[$end]->content);
1368
 		$name = mb_strtoupper($tokens[$end]->content);
1369
 		if ($name != 'FILL') return null;
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
 		$columnIndex = $address->columnIndex;
1371
 		$columnIndex = $address->columnIndex;
1372
 		$exp->fillRanges = [
1372
 		$exp->fillRanges = [
1373
 			new CellAddressRange(new CellAddress($columnIndex, -1), new CellAddress($columnIndex, -1)),
1373
 			new CellAddressRange(new CellAddress($columnIndex, -1), new CellAddress($columnIndex, -1)),
1416
 	 */
1416
 	 */
1417
 	private static function tryExpression(array $tokens, int $start, int $end,
1417
 	private static function tryExpression(array $tokens, int $start, int $end,
1418
 			CellAddress $address): CellExpression {
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
 		throw new CellSyntaxException("Invalid expression");
1428
 		throw new CellSyntaxException("Invalid expression");
1429
 	}
1429
 	}
1430
 
1430
 
1431
 	/**
1431
 	/**
1432
 	 * @param CellExpressionToken[] $tokens
1432
 	 * @param CellExpressionToken[] $tokens
1433
-	 * @param number $start
1434
-	 * @param number $end
1433
+	 * @param int $start
1434
+	 * @param int $end
1435
 	 * @param CellAddress $address
1435
 	 * @param CellAddress $address
1436
 	 * @return CellExpression|null
1436
 	 * @return CellExpression|null
1437
 	 */
1437
 	 */
1449
 			if ($parenLevel < 0) return null;
1449
 			if ($parenLevel < 0) return null;
1450
 		}
1450
 		}
1451
 		if ($parenLevel != 0) return null;
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
 	 * @param CellExpressionToken[] $tokens
1456
 	 * @param CellExpressionToken[] $tokens
1457
-	 * @param number $start
1458
-	 * @param number $end
1457
+	 * @param int $start
1458
+	 * @param int $end
1459
 	 * @param CellAddress $address
1459
 	 * @param CellAddress $address
1460
 	 * @return CellExpression|null
1460
 	 * @return CellExpression|null
1461
 	 */
1461
 	 */
1473
 
1473
 
1474
 	/**
1474
 	/**
1475
 	 * @param CellExpressionToken[] $tokens
1475
 	 * @param CellExpressionToken[] $tokens
1476
-	 * @param number $start
1477
-	 * @param number $end
1476
+	 * @param int $start
1477
+	 * @param int $end
1478
 	 * @param CellAddress $address
1478
 	 * @param CellAddress $address
1479
 	 * @return CellExpression|null
1479
 	 * @return CellExpression|null
1480
 	 */
1480
 	 */
1489
 
1489
 
1490
 	/**
1490
 	/**
1491
 	 * @param CellExpressionToken[] $tokens
1491
 	 * @param CellExpressionToken[] $tokens
1492
-	 * @param number $start
1493
-	 * @param number $end
1492
+	 * @param int $start
1493
+	 * @param int $end
1494
 	 * @param CellAddress $address
1494
 	 * @param CellAddress $address
1495
 	 * @return CellExpression|null
1495
 	 * @return CellExpression|null
1496
 	 */
1496
 	 */
1506
 
1506
 
1507
 	/**
1507
 	/**
1508
 	 * @param CellExpressionToken[] $tokens
1508
 	 * @param CellExpressionToken[] $tokens
1509
-	 * @param number $start
1510
-	 * @param number $end
1509
+	 * @param int $start
1510
+	 * @param int $end
1511
 	 * @param CellAddress $address
1511
 	 * @param CellAddress $address
1512
 	 * @return CellExpression|null
1512
 	 * @return CellExpression|null
1513
 	 */
1513
 	 */
1519
 		$qualifier = $tokens[$start]->content;
1519
 		$qualifier = $tokens[$start]->content;
1520
 		if ($tokens[$start + 1]->type != CellExpressionTokenType::OpenParen) return null;
1520
 		if ($tokens[$start + 1]->type != CellExpressionTokenType::OpenParen) return null;
1521
 		if ($tokens[$end]->type != CellExpressionTokenType::CloseParen) return null;
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
 		if ($argList === null) return null;
1523
 		if ($argList === null) return null;
1524
 		return new CellExpression(CellExpressionOperation::Function, $argList, $qualifier);
1524
 		return new CellExpression(CellExpressionOperation::Function, $argList, $qualifier);
1525
 	}
1525
 	}
1526
 
1526
 
1527
 	/**
1527
 	/**
1528
 	 * @param CellExpressionToken[] $tokens
1528
 	 * @param CellExpressionToken[] $tokens
1529
-	 * @param number $start
1530
-	 * @param number $end
1529
+	 * @param int $start
1530
+	 * @param int $end
1531
 	 * @param CellAddress $address
1531
 	 * @param CellAddress $address
1532
 	 * @return CellExpression[]|null
1532
 	 * @return CellExpression[]|null
1533
 	 */
1533
 	 */
1534
 	private static function tryArgumentList(array $tokens, int $start, int $end,
1534
 	private static function tryArgumentList(array $tokens, int $start, int $end,
1535
-			CellAddress $address): ?CellExpression {
1535
+			CellAddress $address): ?array {
1536
 		$count = $end - $start + 1;
1536
 		$count = $end - $start + 1;
1537
 		if ($count == 0) return [];
1537
 		if ($count == 0) return [];
1538
 		$parenDepth = 0;
1538
 		$parenDepth = 0;
1558
 		// Convert token ranges to expressions
1558
 		// Convert token ranges to expressions
1559
 		$args = [];
1559
 		$args = [];
1560
 		foreach ($argTokens as $argToken) {
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
 			if ($arg === null) return null;
1562
 			if ($arg === null) return null;
1563
-			$args.push($arg);
1563
+			array_push($args, $arg);
1564
 		}
1564
 		}
1565
 		return $args;
1565
 		return $args;
1566
 	}
1566
 	}
1567
 
1567
 
1568
 	/**
1568
 	/**
1569
 	 * @param CellExpressionToken[] $tokens
1569
 	 * @param CellExpressionToken[] $tokens
1570
-	 * @param number $start
1571
-	 * @param number $end
1570
+	 * @param int $start
1571
+	 * @param int $end
1572
 	 * @param CellAddress $address
1572
 	 * @param CellAddress $address
1573
 	 * @return CellExpression|null
1573
 	 * @return CellExpression|null
1574
 	 */
1574
 	 */
1589
 
1589
 
1590
 	/**
1590
 	/**
1591
 	 * @param CellExpressionToken[] $tokens
1591
 	 * @param CellExpressionToken[] $tokens
1592
-	 * @param number $start
1593
-	 * @param number $end
1592
+	 * @param int $start
1593
+	 * @param int $end
1594
 	 * @param CellAddress $address
1594
 	 * @param CellAddress $address
1595
 	 * @return CellExpression|null
1595
 	 * @return CellExpression|null
1596
 	 */
1596
 	 */
1619
 
1619
 
1620
 	/**
1620
 	/**
1621
 	 * @param CellExpressionToken[] $tokens
1621
 	 * @param CellExpressionToken[] $tokens
1622
-	 * @param number $start
1623
-	 * @param number $end
1622
+	 * @param int $start
1623
+	 * @param int $end
1624
 	 * @param CellAddress $address
1624
 	 * @param CellAddress $address
1625
 	 * @return CellExpression|null
1625
 	 * @return CellExpression|null
1626
 	 */
1626
 	 */
1637
 				$parenLevel--;
1637
 				$parenLevel--;
1638
 			} elseif ($parenLevel == 0 && $i > $start && $i < $end) {
1638
 			} elseif ($parenLevel == 0 && $i > $start && $i < $end) {
1639
 				$op = $tokens[$i]->type->name;
1639
 				$op = $tokens[$i]->type->name;
1640
-				$priority = $this->infixPriority[$op] ?? false;
1640
+				$priority = self::infixPriority[$op] ?? false;
1641
 				if ($priority === false) continue;
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
 		usort($candidates, fn($a, $b) => $a['priority'] - $b['priority']);
1645
 		usort($candidates, fn($a, $b) => $a['priority'] - $b['priority']);
1647
 		foreach ($candidates as $candidate) {
1647
 		foreach ($candidates as $candidate) {
1648
 			try {
1648
 			try {
1649
 				$i = $candidate['i'];
1649
 				$i = $candidate['i'];
1650
-				$operand1 = $this->tryExpression($tokens, $start, $i - 1, $address);
1650
+				$operand1 = self::tryExpression($tokens, $start, $i - 1, $address);
1651
 				if ($operand1 === null) continue;
1651
 				if ($operand1 === null) continue;
1652
-				$operand2 = $this->tryExpression($tokens, $i + 1, $end, $address);
1652
+				$operand2 = self::tryExpression($tokens, $i + 1, $end, $address);
1653
 				if ($operand2 === null) continue;
1653
 				if ($operand2 === null) continue;
1654
 				$bestCandidate = $candidate;
1654
 				$bestCandidate = $candidate;
1655
 				break;
1655
 				break;
1662
 		if ($bestCandidate === null) {
1662
 		if ($bestCandidate === null) {
1663
 			return null;
1663
 			return null;
1664
 		}
1664
 		}
1665
-		$i = $bestCandidate->i;
1665
+		$i = $bestCandidate['i'];
1666
 		switch ($tokens[$bestCandidate['i']]->type) {
1666
 		switch ($tokens[$bestCandidate['i']]->type) {
1667
 			case CellExpressionTokenType::Plus:
1667
 			case CellExpressionTokenType::Plus:
1668
 				return new CellExpression(CellExpressionOperation::Add, [ $operand1, $operand2 ]);
1668
 				return new CellExpression(CellExpressionOperation::Add, [ $operand1, $operand2 ]);
1692
 
1692
 
1693
 	/**
1693
 	/**
1694
 	 * @param CellExpressionToken[] $tokens
1694
 	 * @param CellExpressionToken[] $tokens
1695
-	 * @param number $start
1696
-	 * @param number $end
1695
+	 * @param int $start
1696
+	 * @param int $end
1697
 	 * @param CellAddress $address
1697
 	 * @param CellAddress $address
1698
 	 * @return CellExpression|null
1698
 	 * @return CellExpression|null
1699
 	 */
1699
 	 */
1707
 		];
1707
 		];
1708
 		foreach ($ops as $op) {
1708
 		foreach ($ops as $op) {
1709
 			if ($tokens[$start]->type != $op[0]) continue;
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
 			if ($operand === null) return null;
1711
 			if ($operand === null) return null;
1712
 			return new CellExpression($op[1], [ $operand ]);
1712
 			return new CellExpression($op[1], [ $operand ]);
1713
 		}
1713
 		}
1744
 	 * Whether the column should remain unchanged when transposed. This is
1744
 	 * Whether the column should remain unchanged when transposed. This is
1745
 	 * symbolized by prefixing the column name with a `$` (e.g. `$C3`).
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
 	 * Zero-based column index.
1750
 	 * Zero-based column index.
1751
 	 */
1751
 	 */
1752
-	public int $columnIndex = -1;
1752
+	public int $columnIndex;
1753
 
1753
 
1754
 	/**
1754
 	/**
1755
 	 * Letter code for the column.
1755
 	 * Letter code for the column.
1760
 	 * Whether the row should remain unchanged when transposed. This is
1760
 	 * Whether the row should remain unchanged when transposed. This is
1761
 	 * symbolized by prefixing the row number with a `$` (e.g. `C$3`).
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
 	 * Zero-based row index.
1766
 	 * Zero-based row index.
1767
 	 */
1767
 	 */
1768
-	public int $rowIndex = -1;
1768
+	public int $rowIndex;
1769
 
1769
 
1770
 	/**
1770
 	/**
1771
 	 * One-based row number. This is the human-facing row number.
1771
 	 * One-based row number. This is the human-facing row number.
1775
 	/**
1775
 	/**
1776
 	 * Whether this address has both a definite column and row.
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
 	 * @param int $columnIndex  0-based column index
1781
 	 * @param int $columnIndex  0-based column index
1797
 		$this->rowIndex = $rowIndex;
1797
 		$this->rowIndex = $rowIndex;
1798
 		$this->isColumnFixed = $isColumnFixed;
1798
 		$this->isColumnFixed = $isColumnFixed;
1799
 		$this->isRowFixed = $isRowFixed;
1799
 		$this->isRowFixed = $isRowFixed;
1800
+		$this->isResolved = ($columnIndex >= 0 && $rowIndex >= 0);
1800
 		$this->name = self::formatAddress($columnIndex, $rowIndex, $isColumnFixed, $isRowFixed);
1801
 		$this->name = self::formatAddress($columnIndex, $rowIndex, $isColumnFixed, $isRowFixed);
1801
 	}
1802
 	}
1802
 
1803
 
1828
 	 * @param CellAddress $relativeFrom  original address of the formula
1829
 	 * @param CellAddress $relativeFrom  original address of the formula
1829
 	 * @param CellAddress $relativeTo  address where the formula is being
1830
 	 * @param CellAddress $relativeTo  address where the formula is being
1830
 	 *   repeated
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
 	 *   address doesn't have one
1833
 	 *   address doesn't have one
1833
 	 * @return CellAddress|null   resolved address, or `null` if out of bounds
1834
 	 * @return CellAddress|null   resolved address, or `null` if out of bounds
1834
 	 */
1835
 	 */
1835
 	public function transpose(CellAddress $relativeFrom, CellAddress $relativeTo,
1836
 	public function transpose(CellAddress $relativeFrom, CellAddress $relativeTo,
1836
 			bool $resolveToRow = true): ?CellAddress {
1837
 			bool $resolveToRow = true): ?CellAddress {
1837
-		if (!$relativeFrom->isResolved() || !$relativeTo->isResolved()) {
1838
+		if (!$relativeFrom->isResolved || !$relativeTo->isResolved) {
1838
 			throw new CellEvaluationException("Can only transpose to and from resolved addresses");
1839
 			throw new CellEvaluationException("Can only transpose to and from resolved addresses");
1839
 		}
1840
 		}
1840
 		$newColumnIndex = $this->columnIndex;
1841
 		$newColumnIndex = $this->columnIndex;
1846
 		if (!$this->isResolved && $resolveToRow) {
1847
 		if (!$this->isResolved && $resolveToRow) {
1847
 			$newRowIndex = $relativeFrom->rowIndex;
1848
 			$newRowIndex = $relativeFrom->rowIndex;
1848
 		}
1849
 		}
1849
-		if ($newRowIndex != -1 && !$this->isRowAbsolute) {
1850
+		if ($newRowIndex != -1 && !$this->isRowFixed) {
1850
 			$rowDelta = $relativeTo->rowIndex - $relativeFrom->rowIndex;
1851
 			$rowDelta = $relativeTo->rowIndex - $relativeFrom->rowIndex;
1851
 			$newRowIndex += $rowDelta;
1852
 			$newRowIndex += $rowDelta;
1852
 		}
1853
 		}
1891
 			$ACodepoint = ord('A');
1892
 			$ACodepoint = ord('A');
1892
 			$remaining = $columnIndex;
1893
 			$remaining = $columnIndex;
1893
 			do {
1894
 			do {
1894
-				$letters = chr(ACodepoint + (remaining % 26)) . $letters;
1895
+				$letters = chr($ACodepoint + ($remaining % 26)) . $letters;
1895
 				$remaining = floor($remaining / 26);
1896
 				$remaining = floor($remaining / 26);
1896
 			} while ($remaining > 0);
1897
 			} while ($remaining > 0);
1897
 		}
1898
 		}
1904
 		if ($isColumnFixed && $columnIndex >= 0) $addr .= '$';
1905
 		if ($isColumnFixed && $columnIndex >= 0) $addr .= '$';
1905
 		if ($columnIndex >= 0) $addr .= self::columnIndexToLetters($columnIndex);
1906
 		if ($columnIndex >= 0) $addr .= self::columnIndexToLetters($columnIndex);
1906
 		if ($isRowFixed && $rowIndex >= 0) $addr .= '$';
1907
 		if ($isRowFixed && $rowIndex >= 0) $addr .= '$';
1907
-		if ($rowIndex >= 0) $addr .= "${rowIndex + 1}";
1908
+		if ($rowIndex >= 0) $addr .= ($rowIndex + 1);
1908
 		return $addr;
1909
 		return $addr;
1909
 	}
1910
 	}
1910
 
1911
 
1914
 	 * @param string $address  cell address string
1915
 	 * @param string $address  cell address string
1915
 	 * @param ?CellAddress $relativeTo  address to resolve relative
1916
 	 * @param ?CellAddress $relativeTo  address to resolve relative
1916
 	 *   addresses against
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
 	 *   is invalid
1919
 	 *   is invalid
1919
 	 * @return ?CellAddress  address, if parsable
1920
 	 * @return ?CellAddress  address, if parsable
1920
 	 * @throws CellEvaluationException  if the address is invalid and
1921
 	 * @throws CellEvaluationException  if the address is invalid and
1922
 	 */
1923
 	 */
1923
 	public static function fromString(string $address, ?CellAddress $relativeTo=null,
1924
 	public static function fromString(string $address, ?CellAddress $relativeTo=null,
1924
 			bool $throwIfInvalid=false): ?CellAddress {
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
 			if ($throwIfInvalid) throw new CellEvaluationException("Bad address \"{$address}\"", '#REF');
1927
 			if ($throwIfInvalid) throw new CellEvaluationException("Bad address \"{$address}\"", '#REF');
1927
 			return null;
1928
 			return null;
1928
 		}
1929
 		}
1930
 		$letters = mb_strtoupper($groups[2]);
1931
 		$letters = mb_strtoupper($groups[2]);
1931
 		$isRowFixed = ($groups[3] == '$');
1932
 		$isRowFixed = ($groups[3] == '$');
1932
 		$numbers = $groups[4];
1933
 		$numbers = $groups[4];
1933
-		$columnIndex = $this->lettersToColumnIndex($letters);
1934
+		$columnIndex = self::lettersToColumnIndex($letters);
1934
 		$rowIndex = (mb_strlen($numbers) == 0) ? -1 : intval($numbers) - 1;
1935
 		$rowIndex = (mb_strlen($numbers) == 0) ? -1 : intval($numbers) - 1;
1935
 		if ($columnIndex < 0 && $relativeTo !== null) $columnIndex = $relativeTo->columnIndex;
1936
 		if ($columnIndex < 0 && $relativeTo !== null) $columnIndex = $relativeTo->columnIndex;
1936
 		if ($rowIndex < 0 && $relativeTo !== null) $rowIndex = $relativeTo->rowIndex;
1937
 		if ($rowIndex < 0 && $relativeTo !== null) $rowIndex = $relativeTo->rowIndex;
1957
 	 * @param CellAddress $toCell
1958
 	 * @param CellAddress $toCell
1958
 	 */
1959
 	 */
1959
 	public function __construct(CellAddress $fromCell, CellAddress $toCell) {
1960
 	public function __construct(CellAddress $fromCell, CellAddress $toCell) {
1960
-		if ($fromCell->isResolved() != $toCell->isResolved()) {
1961
+		if ($fromCell->isResolved != $toCell->isResolved) {
1961
 			throw new CellEvaluationException("Cannot mix resolved and unresolved cell addresses in range: {$fromCell->name} and {$toCell->name}");
1962
 			throw new CellEvaluationException("Cannot mix resolved and unresolved cell addresses in range: {$fromCell->name} and {$toCell->name}");
1962
 		}
1963
 		}
1963
 		$this->minColumnIndex = min($fromCell->columnIndex, $toCell->columnIndex);
1964
 		$this->minColumnIndex = min($fromCell->columnIndex, $toCell->columnIndex);
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
 	 * confines of the given grid's dimensions. Iterates each row in the first
1976
 	 * confines of the given grid's dimensions. Iterates each row in the first
1976
 	 * column, then each row in the second, etc. Iteration range is inclusive
1977
 	 * column, then each row in the second, etc. Iteration range is inclusive
1977
 	 * of the min and max extents.
1978
 	 * of the min and max extents.
1978
 	 * 
1979
 	 * 
1979
 	 * Example:
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
 		$min_row = $this->minRowIndex;
1993
 		$min_row = $this->minRowIndex;
1993
 		$max_row = $this->maxRowIndex;
1994
 		$max_row = $this->maxRowIndex;
1994
 		if ($min_row == -1) {
1995
 		if ($min_row == -1) {
1995
-			$min_row = $grid->getHeaderRowCount();
1996
+			$min_row = 0;
1996
 		}
1997
 		}
1997
 		if ($max_row == -1) {
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
 		return new class($grid, $min_col, $max_col, $min_row, $max_row) implements Iterator {
2002
 		return new class($grid, $min_col, $max_col, $min_row, $max_row) implements Iterator {
2002
 			private SpreadsheetGrid $grid;
2003
 			private SpreadsheetGrid $grid;
2003
 			private int $min_col;
2004
 			private int $min_col;
2023
 				if ($this->col >= $this->min_col && $this->col <= $this->max_col &&
2024
 				if ($this->col >= $this->min_col && $this->col <= $this->max_col &&
2024
 						$this->row >= $this->min_row && $this->row <= $this->max_row) {
2025
 						$this->row >= $this->min_row && $this->row <= $this->max_row) {
2025
 					$this->address = new CellAddress($this->col, $this->row);
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
 					if (!$this->cell) {
2028
 					if (!$this->cell) {
2028
 						error_log("WARNING: Iterator found no cell at {$this->address->name}");
2029
 						error_log("WARNING: Iterator found no cell at {$this->address->name}");
2029
 					}
2030
 					}
2072
  */
2073
  */
2073
 class CellValue {
2074
 class CellValue {
2074
 	/**
2075
 	/**
2075
-	 * Blank cell. `value` is `null`.
2076
+	 * Blank cell. `$value` is `null`.
2076
 	 */
2077
 	 */
2077
 	public const TYPE_BLANK = 'blank';
2078
 	public const TYPE_BLANK = 'blank';
2078
 	/**
2079
 	/**
2079
-	 * Currency value. `value` is `number`.
2080
+	 * Currency value. `$value` is `float`.
2080
 	 */
2081
 	 */
2081
 	public const TYPE_CURRENCY = 'currency';
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
 	public const TYPE_NUMBER = 'number';
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
 	public const TYPE_PERCENT = 'percent';
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
 	public const TYPE_STRING = 'string';
2094
 	public const TYPE_STRING = 'string';
2094
 	/**
2095
 	/**
2095
-	 * Boolean. `value` is `boolean`.
2096
+	 * Boolean. `$value` is `bool`.
2096
 	 */
2097
 	 */
2097
 	public const TYPE_BOOLEAN = 'boolean';
2098
 	public const TYPE_BOOLEAN = 'boolean';
2098
 	/**
2099
 	/**
2099
 	 * A formula that has resulted in an error during parsing or evaluation.
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
 	public const TYPE_ERROR = 'error';
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
 	public const TYPE_FORMULA = 'formula';
2107
 	public const TYPE_FORMULA = 'formula';
2107
 
2108
 
2136
 		string $type = CellValue::TYPE_STRING,
2137
 		string $type = CellValue::TYPE_STRING,
2137
 		int $decimals = 0
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
 		}
2177
 		}
2177
 		if ($value instanceof Error) {
2178
 		if ($value instanceof Error) {
2178
 			if ($value instanceof CellException) {
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
 		if (is_bool($value)) {
2184
 		if (is_bool($value)) {
2184
 			$formatted = CellValue::formatType($value, CellValue::TYPE_BOOLEAN, 0);
2185
 			$formatted = CellValue::formatType($value, CellValue::TYPE_BOOLEAN, 0);
2185
 			return new CellValue($formatted, $value, CellValue::TYPE_BOOLEAN);
2186
 			return new CellValue($formatted, $value, CellValue::TYPE_BOOLEAN);
2186
 		}
2187
 		}
2187
 		if (is_numeric($value)) {
2188
 		if (is_numeric($value)) {
2188
-			$resolvedType = $type || CellValue::TYPE_NUMBER;
2189
+			$resolvedType = $type ?? CellValue::TYPE_NUMBER;
2189
 			$resolvedDecimals = ($decimals !== null) ? $decimals :
2190
 			$resolvedDecimals = ($decimals !== null) ? $decimals :
2190
 				($resolvedType == CellValue::TYPE_CURRENCY ? 2 :
2191
 				($resolvedType == CellValue::TYPE_CURRENCY ? 2 :
2191
 					CellValue::autodecimals($resolvedType == CellValue::TYPE_PERCENT ?
2192
 					CellValue::autodecimals($resolvedType == CellValue::TYPE_PERCENT ?
2298
 			$wholes = mb_eregi_replace(',', '', $groups[1]);
2299
 			$wholes = mb_eregi_replace(',', '', $groups[1]);
2299
 			$this->type = CellValue::TYPE_NUMBER;
2300
 			$this->type = CellValue::TYPE_NUMBER;
2300
 			$this->decimals = 0;
2301
 			$this->decimals = 0;
2301
-			$this->value = parseFloat($wholes);
2302
+			$this->value = floatval($wholes);
2302
 			$this->formattedValue = CellValue::formatNumber($this->value, $this->decimals);
2303
 			$this->formattedValue = CellValue::formatNumber($this->value, $this->decimals);
2303
 			return;
2304
 			return;
2304
 		}
2305
 		}
2358
 			case CellValue::TYPE_CURRENCY:
2359
 			case CellValue::TYPE_CURRENCY:
2359
 			case CellValue::TYPE_NUMBER:
2360
 			case CellValue::TYPE_NUMBER:
2360
 			case CellValue::TYPE_PERCENT:
2361
 			case CellValue::TYPE_PERCENT:
2361
-				return $formatted ? $this->formattedValue : "${$this->value}";
2362
+				return $formatted ? $this->formattedValue : "{$this->value}";
2362
 			case CellValue::TYPE_STRING:
2363
 			case CellValue::TYPE_STRING:
2363
 				return $formatted ? $this->formattedValue : $this->value;
2364
 				return $formatted ? $this->formattedValue : $this->value;
2364
 			case CellValue::TYPE_ERROR:
2365
 			case CellValue::TYPE_ERROR:
2522
 	 * @param CellValue $a  operand A
2523
 	 * @param CellValue $a  operand A
2523
 	 * @param CellValue $b  operand B
2524
 	 * @param CellValue $b  operand B
2524
 	 * @param string $op  operator symbol
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
 	 *   and result type string
2527
 	 *   and result type string
2527
 	 * @throws CellEvaluationException  if types are incompatible for numeric operations
2528
 	 * @throws CellEvaluationException  if types are incompatible for numeric operations
2528
 	 */
2529
 	 */
2529
 	private static function resolveNumericOperands(CellValue $a, CellValue $b, string $op): array {
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
 			throw new CellEvaluationException("Cannot perform math on text values");
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
 			return [ 0, $b->value, $b->type ];
2541
 			return [ 0, $b->value, $b->type ];
2541
-		} elseif ($b->type == $this->TYPE_BLANK) {
2542
+		} elseif ($b->type == self::TYPE_BLANK) {
2542
 			return [ $a->value, 0, $a->type ];
2543
 			return [ $a->value, 0, $a->type ];
2543
 		}
2544
 		}
2544
 
2545
 
2549
 		}
2550
 		}
2550
 
2551
 
2551
 		switch ($a->type . $b->type) {
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
 				return [ $a->value, $b->value,
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
 				return [ $a->value, $b->value,
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
 				return [ $a->value, $b->value, $b->type ];
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
 				return [ $a->value, $b->value,
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
 				return [ $a->value ? 1 : 0, $b->value, $b->type ];
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
 				return [ $a->value, $b->value ? 1 : 0, $a->type ];
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
 			case CellValue::TYPE_BOOLEAN: return $value ? 'TRUE' : 'FALSE';
2649
 			case CellValue::TYPE_BOOLEAN: return $value ? 'TRUE' : 'FALSE';
2649
 			case CellValue::TYPE_STRING: return "{$value}";
2650
 			case CellValue::TYPE_STRING: return "{$value}";
2650
 			case CellValue::TYPE_FORMULA: return "{$value}";
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
 		if (str_starts_with($s, '-')) {
2662
 		if (str_starts_with($s, '-')) {
2661
 			return '-$' . mb_substr($s, 1);
2663
 			return '-$' . mb_substr($s, 1);
2662
 		}
2664
 		}
2663
-		return '$' . s;
2665
+		return '$' . $s;
2664
 	}
2666
 	}
2665
 
2667
 
2666
 	private static function formatPercent(float|int $value, int $decimals): string {
2668
 	private static function formatPercent(float|int $value, int $decimals): string {
2679
 			$s = number_format($value, $maxDigits);
2681
 			$s = number_format($value, $maxDigits);
2680
 			if (strpos($s, '.') === false) return 0;
2682
 			if (strpos($s, '.') === false) return 0;
2681
 			$fraction = explode('.', $s)[1];
2683
 			$fraction = explode('.', $s)[1];
2684
+			$fraction = rtrim($fraction, '0');
2682
 			return min($maxDigits, mb_strlen($fraction));
2685
 			return min($maxDigits, mb_strlen($fraction));
2683
 		}
2686
 		}
2684
 		return 0;
2687
 		return 0;
2830
 		// Copy results back to table
2833
 		// Copy results back to table
2831
 		for ($c = 0; $c < $columnCount; $c++) {
2834
 		for ($c = 0; $c < $columnCount; $c++) {
2832
 			for ($r = 0; $r < $rowCount; $r++) {
2835
 			for ($r = 0; $r < $rowCount; $r++) {
2833
-				$cellNode = $tableNode->bodyRows[$r]->children[$c];
2836
+				$cellNode = $tableNode->bodyRows()[$r]->children[$c];
2834
 				if ($cellNode === null) continue;
2837
 				if ($cellNode === null) continue;
2835
 				$gridCell = $grid->cells[$c][$r];
2838
 				$gridCell = $grid->cells[$c][$r];
2836
 				$gridValue = $gridCell->outputValue;
2839
 				$gridValue = $gridCell->outputValue;

+ 22
- 10
playgroundapi.php Vedi File

22
 	'MDFootnoteReader' => true,
22
 	'MDFootnoteReader' => true,
23
 	'MDAbbreviationReader' => true,
23
 	'MDAbbreviationReader' => true,
24
 	'MDParagraphReader' => true,
24
 	'MDParagraphReader' => true,
25
+	'MDSpreadsheetReader' => true,
25
 
26
 
26
 	'MDEmphasisReader' => true,
27
 	'MDEmphasisReader' => true,
27
 	'MDStrongReader' => true,
28
 	'MDStrongReader' => true,
41
 ];
42
 ];
42
 
43
 
43
 include 'php/markdown.php';
44
 include 'php/markdown.php';
44
-$readers = [];
45
-foreach ($readerNames as $readerName) {
46
-	if ($permittedReaders[$readerName] ?? false) {
47
-		$ref = new ReflectionClass($readerName);
48
-		$reader = $ref->newInstanceArgs([]);
49
-		array_push($readers, $reader);
45
+include 'php/spreadsheet.php';
46
+try {
47
+	$readers = [];
48
+	foreach ($readerNames as $readerName) {
49
+		if ($permittedReaders[$readerName] ?? false) {
50
+			$ref = new ReflectionClass($readerName);
51
+			$reader = $ref->newInstanceArgs([]);
52
+			array_push($readers, $reader);
53
+		}
50
 	}
54
 	}
55
+	$parser = new Markdown($readers);
56
+	$html = $parser->toHTML($markdown);
57
+	header('Content-Type: text/html');
58
+	print($html);
59
+} catch (Error $e) {
60
+	header('Content-Type: text/html');
61
+	print('<pre><code>');
62
+	print($e->getMessage() . "\n");
63
+	print($e->getTraceAsString() . "\n");
64
+	print('</code></pre>');
65
+	flush();
66
+	throw $e;
51
 }
67
 }
52
-$parser = new Markdown($readers);
53
-$html = $parser->toHTML($markdown);
54
-header('Content-Type: text/html');
55
-print($html);
56
 ?>
68
 ?>

Loading…
Annulla
Salva