Procházet zdrojové kódy

Cleanup, minor spreadsheet bugs

main
Rocketsoup před 1 rokem
rodič
revize
75b163050f
4 změnil soubory, kde provedl 179 přidání a 170 odebrání
  1. 3
    3
      js/spreadsheet.js
  2. 1
    1
      js/spreadsheet.min.js
  3. 169
    161
      php/spreadsheet.php
  4. 6
    5
      playground.html

+ 3
- 3
js/spreadsheet.js Zobrazit soubor

578
 		if (args.length == 0) {
578
 		if (args.length == 0) {
579
 			throw new CellEvaluationException("AND requires one or more arguments");
579
 			throw new CellEvaluationException("AND requires one or more arguments");
580
 		}
580
 		}
581
-		const values = args.map((arg) => this.#evaluate(arg, address));
581
+		const values = this.#flattenedNumericArguments('AND', args, address, false);
582
 		for (const value of values) {
582
 		for (const value of values) {
583
 			const result = value.booleanValue();
583
 			const result = value.booleanValue();
584
 			if (result === null) {
584
 			if (result === null) {
842
 		if (args.length == 0) {
842
 		if (args.length == 0) {
843
 			throw new CellEvaluationException("OR requires one or more arguments");
843
 			throw new CellEvaluationException("OR requires one or more arguments");
844
 		}
844
 		}
845
-		const values = args.map((arg) => this.#evaluate(arg, address));
845
+		const values = this.#flattenedNumericArguments('OR', args, address, false);
846
 		for (const value of values) {
846
 		for (const value of values) {
847
 			const result = value.booleanValue();
847
 			const result = value.booleanValue();
848
 			if (result === null) {
848
 			if (result === null) {
974
 		if (args.length == 0) {
974
 		if (args.length == 0) {
975
 			throw new CellEvaluationException("XOR requires one or more arguments");
975
 			throw new CellEvaluationException("XOR requires one or more arguments");
976
 		}
976
 		}
977
-		const values = args.map((arg) => this.#evaluate(arg, address));
977
+		const values = this.#flattenedNumericArguments('XOR', args, address, false);
978
 		var result = null;
978
 		var result = null;
979
 		for (const value of values) {
979
 		for (const value of values) {
980
 			const b = value.booleanValue();
980
 			const b = value.booleanValue();

+ 1
- 1
js/spreadsheet.min.js
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 169
- 161
php/spreadsheet.php Zobrazit soubor

6
 	/**
6
 	/**
7
 	 * Brief symbol to show in the cell to signify an error occurred during
7
 	 * Brief symbol to show in the cell to signify an error occurred during
8
 	 * evaluation.
8
 	 * evaluation.
9
-	 *
10
-	 * @$string
11
 	 */
9
 	 */
12
 	public string $errorSymbol;
10
 	public string $errorSymbol;
13
 
11
 
151
 		$colCount = $this->grid->columnCount;
149
 		$colCount = $this->grid->columnCount;
152
 
150
 
153
 		// Make queue of cell addresses with expressions in them
151
 		// Make queue of cell addresses with expressions in them
154
-		/** @$CellAddress[] */
152
+		/** @var CellAddress[] */
155
 		$expressionAddressQueue = [];
153
 		$expressionAddressQueue = [];
156
-		$range = new CellAddressRange(new CellAddress(0, 0), new CellAddress($colCount - 1, $rowCount - 1));
154
+		$range = new CellAddressRange(new CellAddress(0, 0),
155
+			new CellAddress($colCount - 1, $rowCount - 1));
157
 		foreach ($range->cellsIn($this->grid) as $addressStr => $cell) {
156
 		foreach ($range->cellsIn($this->grid) as $addressStr => $cell) {
158
 			$address = CellAddress::fromString($addressStr);
157
 			$address = CellAddress::fromString($addressStr);
159
 			if ($address === null) {
158
 			if ($address === null) {
160
 				error_log("Couldn't parse address string {$addressStr}!");
159
 				error_log("Couldn't parse address string {$addressStr}!");
161
 			}
160
 			}
162
 			$value = $cell->originalValue;
161
 			$value = $cell->originalValue;
163
-			if ($value->type != CellValue::TYPE_FORMULA) {
162
+			if ($value->type !== CellValue::TYPE_FORMULA) {
164
 				$cell->outputValue = $value;
163
 				$cell->outputValue = $value;
165
 				$cell->isCalculated = false;
164
 				$cell->isCalculated = false;
166
 				continue;
165
 				continue;
187
 	}
186
 	}
188
 
187
 
189
 	/**
188
 	/**
190
-	 * Attempts to evaluate expressions at the given `addresses`. If an
189
+	 * Attempts to evaluate expressions at the given `$addresses`. If an
191
 	 * expression has unevaluated references, the expression is moved to the
190
 	 * expression has unevaluated references, the expression is moved to the
192
 	 * end of the queue and tried again later. When this method returns, any
191
 	 * end of the queue and tried again later. When this method returns, any
193
-	 * elements left in `addresses` can be considered circular references.
192
+	 * elements left in `$addresses` can be considered circular references.
194
 	 * 
193
 	 * 
195
 	 * @param CellAddress[] $addresses  mutable queue of formula addresses
194
 	 * @param CellAddress[] $addresses  mutable queue of formula addresses
196
 	 */
195
 	 */
197
 	private function processExpressionQueue(array &$addresses) {
196
 	private function processExpressionQueue(array &$addresses) {
198
 		$requeueCount = 0;
197
 		$requeueCount = 0;
199
-		while (sizeof($addresses) > 0 && $requeueCount < sizeof($addresses)) {
198
+		while (count($addresses) > 0 && $requeueCount < count($addresses)) {
200
 			$address = $addresses[0];
199
 			$address = $addresses[0];
201
 			array_splice($addresses, 0, 1);
200
 			array_splice($addresses, 0, 1);
202
 			$cell = $this->grid->cellAt($address);
201
 			$cell = $this->grid->cellAt($address);
207
 					$cell->outputValue = $result;
206
 					$cell->outputValue = $result;
208
 					$requeueCount = 0;
207
 					$requeueCount = 0;
209
 				} elseif (is_array($result)) {
208
 				} elseif (is_array($result)) {
210
-					if (sizeof($result) == 1) {
209
+					if (count($result) === 1) {
211
 						$cell->outputValue = $result[0];
210
 						$cell->outputValue = $result[0];
212
 						$requeueCount = 0;
211
 						$requeueCount = 0;
213
 					} else {
212
 					} else {
214
-						throw new CellEvaluationException("Expression resolved to " . sizeof($result) . " values, single value expected");
213
+						throw new CellEvaluationException("Expression resolved to " .
214
+							count($result) . " values, single value expected");
215
 					}
215
 					}
216
 				} else {
216
 				} else {
217
-					$typename = gettype($result) == 'object' ? get_class($result) : gettype($result);
218
-					throw new CellEvaluationException("Expression resolved to {$typename}, expected CellValue");
217
+					$typename = gettype($result) === 'object' ? get_class($result)
218
+						: gettype($result);
219
+					throw new CellEvaluationException(
220
+						"Expression resolved to {$typename}, expected CellValue");
219
 				}
221
 				}
220
 			} catch (CellDependencyException $e) {
222
 			} catch (CellDependencyException $e) {
221
 				// Depends on a value that hasn't been calculated yet
223
 				// Depends on a value that hasn't been calculated yet
230
 
232
 
231
 	/**
233
 	/**
232
 	 * Autofills a formula, transposing the formula to each affected cell and
234
 	 * Autofills a formula, transposing the formula to each affected cell and
233
-	 * stored in `parsedExpression`, and each address is queued in `addresses`
235
+	 * stored in `$parsedExpression`, and each address is queued in `$addresses`
234
 	 * for evaluation.
236
 	 * for evaluation.
235
 	 *
237
 	 *
236
 	 * @param CellExpression $expression  autofilled formula
238
 	 * @param CellExpression $expression  autofilled formula
270
 	 * @param CellAddress $address  location of expression
272
 	 * @param CellAddress $address  location of expression
271
 	 * @return CellValue|CellValue[]  results
273
 	 * @return CellValue|CellValue[]  results
272
 	 */
274
 	 */
273
-	private function evaluate(CellExpression $expr, CellAddress $address) {
275
+	private function evaluate(CellExpression $expr, CellAddress $address): CellValue|array {
274
 		$result = $this->preevaluate($expr, $address);
276
 		$result = $this->preevaluate($expr, $address);
275
 		if ($result instanceof CellValue) {
277
 		if ($result instanceof CellValue) {
276
 			// Expression included formatting override. Apply it to value.
278
 			// Expression included formatting override. Apply it to value.
289
 	 * @param CellExpression $expr  expression to evaluate
291
 	 * @param CellExpression $expr  expression to evaluate
290
 	 * @param CellAddress $address  location of expression
292
 	 * @param CellAddress $address  location of expression
291
 	 * @return CellValue|CellValue[]  results
293
 	 * @return CellValue|CellValue[]  results
292
-	 * @throws CellException if evaluation fails for any reason
294
+	 * @throws CellException  if evaluation fails for any reason
293
 	 */
295
 	 */
294
-	private function preevaluate(CellExpression $expr, CellAddress $address) {
296
+	private function preevaluate(CellExpression $expr, CellAddress $address): CellValue|array {
295
 		switch ($expr->op) {
297
 		switch ($expr->op) {
296
 			case CellExpressionOperation::Number:
298
 			case CellExpressionOperation::Number:
297
 			case CellExpressionOperation::String:
299
 			case CellExpressionOperation::String:
404
 	 * @param array $args  raw arguments
406
 	 * @param array $args  raw arguments
405
 	 * @param CellAddress $address  location of the expression
407
 	 * @param CellAddress $address  location of the expression
406
 	 * @return CellValue  result
408
 	 * @return CellValue  result
407
-	 * @throws CellException if evaluation fails for any reason
409
+	 * @throws CellException  if evaluation fails for any reason
408
 	 */
410
 	 */
409
 	private function callFunction(string $functionName, array $args, CellAddress $address): CellValue {
411
 	private function callFunction(string $functionName, array $args, CellAddress $address): CellValue {
410
 		switch (mb_strtoupper($functionName)) {
412
 		switch (mb_strtoupper($functionName)) {
446
 	 * @param array $args  raw arguments
448
 	 * @param array $args  raw arguments
447
 	 * @param CellAddress $address  address of the formula
449
 	 * @param CellAddress $address  address of the formula
448
 	 * @return CellValue[]  numeric arguments
450
 	 * @return CellValue[]  numeric arguments
449
-	 * @throws CellSyntaxException if wrong number of arguments is passed
450
-	 * @throws CellEvaluationException if an argument does not resolve to a numeric value
451
+	 * @throws CellSyntaxException  if wrong number of arguments is passed
452
+	 * @throws CellEvaluationException  if an argument does not resolve to a numeric value
451
 	 */
453
 	 */
452
 	private function assertNumericArguments(string $functionName, int $minArgs,
454
 	private function assertNumericArguments(string $functionName, int $minArgs,
453
 			int $maxArgs, array $args, CellAddress $address): array {
455
 			int $maxArgs, array $args, CellAddress $address): array {
454
-		$argCount = sizeof($args);
456
+		$argCount = count($args);
455
 		if ($argCount < $minArgs || $argCount > $maxArgs) {
457
 		if ($argCount < $minArgs || $argCount > $maxArgs) {
456
-			if ($minArgs == $maxArgs) {
457
-				throw new CellSyntaxException("{$functionName}() expects {$minArgs} arguments, got {$argCount}");
458
+			if ($minArgs === $maxArgs) {
459
+				throw new CellSyntaxException("{$functionName}() expects {$minArgs} " +
460
+					"arguments, got {$argCount}");
458
 			}
461
 			}
459
-			throw new CellSyntaxException("{$functionName}() expects between {$minArgs} and {$maxArgs} arguments, got {$argCount}");
462
+			throw new CellSyntaxException("{$functionName}() expects between " +
463
+				"{$minArgs} and {$maxArgs} arguments, got {$argCount}");
460
 		}
464
 		}
461
 		$out = [];
465
 		$out = [];
462
 		foreach ($args as $argument) {
466
 		foreach ($args as $argument) {
534
 	 * @return CellValue  result
538
 	 * @return CellValue  result
535
 	 */
539
 	 */
536
 	private function funcAnd(array $args, CellAddress $address): CellValue {
540
 	private function funcAnd(array $args, CellAddress $address): CellValue {
537
-		if (sizeof($args) == 0) {
541
+		if (count($args) === 0) {
538
 			throw new CellEvaluationException("AND requires one or more arguments");
542
 			throw new CellEvaluationException("AND requires one or more arguments");
539
 		}
543
 		}
540
-		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
544
+		$values = $this->flattenedNumericArguments('AND', $args, $address, false);
541
 		foreach ($values as $value) {
545
 		foreach ($values as $value) {
542
 			$result = $value->booleanValue();
546
 			$result = $value->booleanValue();
543
 			if ($result === null) {
547
 			if ($result === null) {
622
 	 * @return CellValue  result
626
 	 * @return CellValue  result
623
 	 */
627
 	 */
624
 	private function funcIf(array $args, CellAddress $address): CellValue {
628
 	private function funcIf(array $args, CellAddress $address): CellValue {
625
-		if (sizeof($args) != 3) {
629
+		if (count($args) !== 3) {
626
 			throw new CellEvaluationException("IF expects three arguments");
630
 			throw new CellEvaluationException("IF expects three arguments");
627
 		}
631
 		}
628
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
632
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
648
 	 * @return CellValue  result
652
 	 * @return CellValue  result
649
 	 */
653
 	 */
650
 	private function funcIfs(array $args, CellAddress $address): CellValue {
654
 	private function funcIfs(array $args, CellAddress $address): CellValue {
651
-		if (sizeof($args) < 3) {
655
+		if (count($args) < 3) {
652
 			throw new CellEvaluationException("IFS expects at least 3 arguments");
656
 			throw new CellEvaluationException("IFS expects at least 3 arguments");
653
 		}
657
 		}
654
-		if ((sizeof($args) % 2) != 1) {
658
+		if ((count($args) % 2) !== 1) {
655
 			throw new CellEvaluationException("IFS expects an odd number of arguments");
659
 			throw new CellEvaluationException("IFS expects an odd number of arguments");
656
 		}
660
 		}
657
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
661
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
658
-		for ($i = 0; $i < sizeof($evaled) - 1; $i += 2) {
662
+		for ($i = 0; $i < count($evaled) - 1; $i += 2) {
659
 			$test = $evaled[$i].booleanValue();
663
 			$test = $evaled[$i].booleanValue();
660
 			if ($test === null) {
664
 			if ($test === null) {
661
 				throw new CellEvaluationException("IFS expects a boolean for argument " . ($i + 1));
665
 				throw new CellEvaluationException("IFS expects a boolean for argument " . ($i + 1));
664
 				return $evaled[$i + 1];
668
 				return $evaled[$i + 1];
665
 			}
669
 			}
666
 		}
670
 		}
667
-		return $evaled[sizeof($evaled) - 1];
671
+		return $evaled[count($evaled) - 1];
668
 	}
672
 	}
669
 
673
 
670
 	/**
674
 	/**
690
 	private function funcLog(array $args, CellAddress $address): CellValue {
694
 	private function funcLog(array $args, CellAddress $address): CellValue {
691
 		$evaled = $this->assertNumericArguments('LOG', 1, 2, $args, $address);
695
 		$evaled = $this->assertNumericArguments('LOG', 1, 2, $args, $address);
692
 		$exponent = $evaled[0];
696
 		$exponent = $evaled[0];
693
-		$base = (sizeof($evaled) > 1) ? $evaled[1]->value : 10.0;
697
+		$base = (count($evaled) > 1) ? $evaled[1]->value : 10.0;
694
 		$newValue = log($exponent->value) / log($base);
698
 		$newValue = log($exponent->value) / log($base);
695
 		return CellValue::fromValue($newValue, $exponent->type);
699
 		return CellValue::fromValue($newValue, $exponent->type);
696
 	}
700
 	}
703
 	 * @return CellValue  result
707
 	 * @return CellValue  result
704
 	 */
708
 	 */
705
 	private function funcLower(array $args, CellAddress $address): CellValue {
709
 	private function funcLower(array $args, CellAddress $address): CellValue {
706
-		if (sizeof($args) != 1) {
710
+		if (count($args) !== 1) {
707
 			throw new CellEvaluationException("LOWER requires one argument");
711
 			throw new CellEvaluationException("LOWER requires one argument");
708
 		}
712
 		}
709
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
713
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
724
 	private function funcMax(array $args, CellAddress $address): CellValue {
728
 	private function funcMax(array $args, CellAddress $address): CellValue {
725
 		$maxValue = null;
729
 		$maxValue = null;
726
 		$flattened = $this->flattenedNumericArguments('MAX', $args, $address);
730
 		$flattened = $this->flattenedNumericArguments('MAX', $args, $address);
727
-		if (sizeof($flattened) == 0) {
731
+		if (count($flattened) === 0) {
728
 			throw new CellEvaluationException("MAX requires at least one numeric argument");
732
 			throw new CellEvaluationException("MAX requires at least one numeric argument");
729
 		}
733
 		}
730
 		foreach ($flattened as $argument) {
734
 		foreach ($flattened as $argument) {
745
 	private function funcMin(array $args, CellAddress $address): CellValue {
749
 	private function funcMin(array $args, CellAddress $address): CellValue {
746
 		$minValue = null;
750
 		$minValue = null;
747
 		$flattened = $this->flattenedNumericArguments('MIN', $args, $address);
751
 		$flattened = $this->flattenedNumericArguments('MIN', $args, $address);
748
-		if (sizeof($flattened) == 0) {
752
+		if (count($flattened) === 0) {
749
 			throw new CellEvaluationException("MIN requires at least one numeric argument");
753
 			throw new CellEvaluationException("MIN requires at least one numeric argument");
750
 		}
754
 		}
751
 		foreach ($flattened as $argument) {
755
 		foreach ($flattened as $argument) {
764
 	 * @return CellValue  result
768
 	 * @return CellValue  result
765
 	 */
769
 	 */
766
 	private function funcMod(array $args, CellAddress $address): CellValue {
770
 	private function funcMod(array $args, CellAddress $address): CellValue {
767
-		if (sizeof($args) != 2) {
771
+		if (count($args) !== 2) {
768
 			throw new CellEvaluationException("MOD requires two numeric arguments");
772
 			throw new CellEvaluationException("MOD requires two numeric arguments");
769
 		}
773
 		}
770
 		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
774
 		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
779
 	 * @return CellValue  result
783
 	 * @return CellValue  result
780
 	 */
784
 	 */
781
 	private function funcNot(array $args, CellAddress $address): CellValue {
785
 	private function funcNot(array $args, CellAddress $address): CellValue {
782
-		if (sizeof($args) != 1) {
786
+		if (count($args) !== 1) {
783
 			throw new CellEvaluationException("NOT expects one argument");
787
 			throw new CellEvaluationException("NOT expects one argument");
784
 		}
788
 		}
785
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
789
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
798
 	 * @return CellValue  result
802
 	 * @return CellValue  result
799
 	 */
803
 	 */
800
 	private function funcOr(array $args, CellAddress $address): CellValue {
804
 	private function funcOr(array $args, CellAddress $address): CellValue {
801
-		if (sizeof($args) == 0) {
805
+		if (count($args) === 0) {
802
 			throw new CellEvaluationException("OR requires one or more arguments");
806
 			throw new CellEvaluationException("OR requires one or more arguments");
803
 		}
807
 		}
804
-		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
808
+		$values = $this->flattenedNumericArguments('OR', $args, $address, false);
805
 		foreach ($values as $value) {
809
 		foreach ($values as $value) {
806
 			$result = $value->booleanValue();
810
 			$result = $value->booleanValue();
807
 			if ($result === null) {
811
 			if ($result === null) {
839
 	private function funcRound(array $args, CellAddress $address): CellValue {
843
 	private function funcRound(array $args, CellAddress $address): CellValue {
840
 		$evaled = $this->assertNumericArguments('ROUND', 1, 2, $args, $address);
844
 		$evaled = $this->assertNumericArguments('ROUND', 1, 2, $args, $address);
841
 		$val = $evaled[0];
845
 		$val = $evaled[0];
842
-		$places = sizeof($evaled) > 1 ? $evaled[1]->value : 0;
846
+		$places = count($evaled) > 1 ? $evaled[1]->value : 0;
843
 		$divider = pow(10.0, $places);
847
 		$divider = pow(10.0, $places);
844
 		$newValue = round($val->value * $divider) / $divider;
848
 		$newValue = round($val->value * $divider) / $divider;
845
 		return CellValue::fromValue($newValue, $val->type);
849
 		return CellValue::fromValue($newValue, $val->type);
867
 	 * @return CellValue  result
871
 	 * @return CellValue  result
868
 	 */
872
 	 */
869
 	private function funcSubstitute(array $args, CellAddress $address): CellValue {
873
 	private function funcSubstitute(array $args, CellAddress $address): CellValue {
870
-		if (sizeof($args) != 3) {
874
+		if (count($args) !== 3) {
871
 			throw new CellEvaluationException("SUBSTITUTE expects 3 string arguments");
875
 			throw new CellEvaluationException("SUBSTITUTE expects 3 string arguments");
872
 		}
876
 		}
873
 		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
877
 		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
911
 	 * @return CellValue  result
915
 	 * @return CellValue  result
912
 	 */
916
 	 */
913
 	private function funcUpper(array $args, CellAddress $address): CellValue {
917
 	private function funcUpper(array $args, CellAddress $address): CellValue {
914
-		if (sizeof($args) != 1) {
918
+		if (count($args) !== 1) {
915
 			throw new CellEvaluationException("UPPER requires one argument");
919
 			throw new CellEvaluationException("UPPER requires one argument");
916
 		}
920
 		}
917
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
921
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
930
 	 * @return CellValue  result
934
 	 * @return CellValue  result
931
 	 */
935
 	 */
932
 	private function funcXor(array $args, CellAddress $address): CellValue {
936
 	private function funcXor(array $args, CellAddress $address): CellValue {
933
-		if (sizeof($args) == 0) {
937
+		if (count($args) === 0) {
934
 			throw new CellEvaluationException("XOR requires one or more arguments");
938
 			throw new CellEvaluationException("XOR requires one or more arguments");
935
 		}
939
 		}
936
-		$values = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
940
+		$values = $this->flattenedNumericArguments('XOR', $args, $address, false);
937
 		$result = null;
941
 		$result = null;
938
 		foreach ($values as $value) {
942
 		foreach ($values as $value) {
939
 			$b = $value->booleanValue();
943
 			$b = $value->booleanValue();
942
 			}
946
 			}
943
 			$result = ($result === null) ? $b : ($result ^ $b);
947
 			$result = ($result === null) ? $b : ($result ^ $b);
944
 		}
948
 		}
945
-		return CellValue::fromValue($result != 0);
949
+		return CellValue::fromValue($result !== 0);
946
 	}
950
 	}
947
 }
951
 }
948
 
952
 
1009
 	 */
1013
 	 */
1010
 	public static function parse(string $expression, CellAddress $address): ?CellExpression {
1014
 	public static function parse(string $expression, CellAddress $address): ?CellExpression {
1011
 		$tokens = self::expressionToTokens($expression);
1015
 		$tokens = self::expressionToTokens($expression);
1012
-		if (sizeof($tokens) == 0) return null;
1016
+		if (count($tokens) === 0) return null;
1013
 		$expr = self::expressionFromTokens($tokens, $address);
1017
 		$expr = self::expressionFromTokens($tokens, $address);
1014
 		$expr->location = $address;
1018
 		$expr->location = $address;
1015
 		return $expr;
1019
 		return $expr;
1022
 	 * @param string $indent
1026
 	 * @param string $indent
1023
 	 */
1027
 	 */
1024
 	public static function dumpExpression(CellExpression $expression, string $indent = '') {
1028
 	public static function dumpExpression(CellExpression $expression, string $indent = '') {
1025
-		if (sizeof($expression->arguments) == 0) {
1029
+		if (count($expression->arguments) === 0) {
1026
 			error_log($indent . "expr " . $expression->op->name . "()");
1030
 			error_log($indent . "expr " . $expression->op->name . "()");
1027
 		} else {
1031
 		} else {
1028
 			error_log($indent . $expression->op->name . '(');
1032
 			error_log($indent . $expression->op->name . '(');
1141
 		$l = mb_strlen($text);
1145
 		$l = mb_strlen($text);
1142
 		while ($pos < $l) {
1146
 		while ($pos < $l) {
1143
 			$ch = mb_substr($text, $pos, 1);
1147
 			$ch = mb_substr($text, $pos, 1);
1144
-			if ($ch == ' ' || $ch == "\t" || $ch == "\n" || $ch == "\r") {
1148
+			if ($ch === ' ' || $ch === "\t" || $ch === "\n" || $ch === "\r") {
1145
 				$pos++;
1149
 				$pos++;
1146
 			} else {
1150
 			} else {
1147
 				return;
1151
 				return;
1155
 		$l = mb_strlen($target);
1159
 		$l = mb_strlen($target);
1156
 		if ($l > $remaining) return null;
1160
 		if ($l > $remaining) return null;
1157
 		$test = mb_substr($text, $pos, $l);
1161
 		$test = mb_substr($text, $pos, $l);
1158
-		if (mb_strtoupper($test) != mb_strtoupper($target)) return null;
1162
+		if (mb_strtoupper($test) !== mb_strtoupper($target)) return null;
1159
 		$pos += $l;
1163
 		$pos += $l;
1160
 		return new CellExpressionToken($type, $test);
1164
 		return new CellExpressionToken($type, $test);
1161
 	}
1165
 	}
1165
 		$ch = mb_substr($text, $p, 1);
1169
 		$ch = mb_substr($text, $p, 1);
1166
 		$address = '';
1170
 		$address = '';
1167
 		$isName = true;
1171
 		$isName = true;
1168
-		if ($ch == '$') {
1172
+		if ($ch === '$') {
1169
 			$address .= $ch;
1173
 			$address .= $ch;
1170
 			$isName = false;
1174
 			$isName = false;
1171
 			$p++;
1175
 			$p++;
1174
 		if ($col === null) return null;
1178
 		if ($col === null) return null;
1175
 		$address .= $col;
1179
 		$address .= $col;
1176
 		$ch = mb_substr($text, $p, 1);
1180
 		$ch = mb_substr($text, $p, 1);
1177
-		if ($ch == '$') {
1181
+		if ($ch === '$') {
1178
 			$address .= $ch;
1182
 			$address .= $ch;
1179
 			$isName = false;
1183
 			$isName = false;
1180
 			$p++;
1184
 			$p++;
1181
-			$row = self::readChars($text, $p, fn($ch) => self::isDigit($ch), 1);
1185
+			$row = self::readChars($text, $p, fn($s) => self::isDigit($s), 1);
1182
 			if ($row === null) return null;
1186
 			if ($row === null) return null;
1183
 			$address .= $row;
1187
 			$address .= $row;
1184
 		} else {
1188
 		} else {
1185
-			$row = self::readChars($text, $p, fn($ch) => self::isDigit($ch), 0);
1189
+			$row = self::readChars($text, $p, fn($s) => self::isDigit($s), 0);
1186
 			if ($row === null) return null;
1190
 			if ($row === null) return null;
1187
 			$address .= $row;
1191
 			$address .= $row;
1188
 		}
1192
 		}
1220
 		}
1224
 		}
1221
 		if ($pos < $l) {
1225
 		if ($pos < $l) {
1222
 			$ch = mb_substr($text, $pos, 1);
1226
 			$ch = mb_substr($text, $pos, 1);
1223
-			if ($ch == '.') {
1227
+			if ($ch === '.') {
1224
 				$numStr .= $ch;
1228
 				$numStr .= $ch;
1225
 				$pos++;
1229
 				$pos++;
1226
 				while ($pos < $l) {
1230
 				while ($pos < $l) {
1249
 			$pos++;
1253
 			$pos++;
1250
 			if ($inEscape) {
1254
 			if ($inEscape) {
1251
 				$inEscape = false;
1255
 				$inEscape = false;
1252
-				if ($ch == '\\' || $ch == '"') {
1256
+				if ($ch === '\\' || $ch === '"') {
1253
 					$str .= $ch;
1257
 					$str .= $ch;
1254
 				} else {
1258
 				} else {
1255
 					throw new CellSyntaxException("Bad string escape sequence \"\\{$ch}\"");
1259
 					throw new CellSyntaxException("Bad string escape sequence \"\\{$ch}\"");
1256
 				}
1260
 				}
1257
-			} elseif ($ch == '\\') {
1261
+			} elseif ($ch === '\\') {
1258
 				$inEscape = true;
1262
 				$inEscape = true;
1259
-			} elseif ($ch == '"') {
1263
+			} elseif ($ch === '"') {
1260
 				return new CellExpressionToken(CellExpressionTokenType::String, $str);
1264
 				return new CellExpressionToken(CellExpressionTokenType::String, $str);
1261
 			} else {
1265
 			} else {
1262
 				$str .= $ch;
1266
 				$str .= $ch;
1324
 	 * @return ?CellExpression
1328
 	 * @return ?CellExpression
1325
 	 */
1329
 	 */
1326
 	public static function expressionFromTokens(array $tokens, CellAddress $address): ?CellExpression {
1330
 	public static function expressionFromTokens(array $tokens, CellAddress $address): ?CellExpression {
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;
1331
+		if ($expr = self::tryExpressionAndFormat($tokens, 0, count($tokens) - 1, $address)) return $expr;
1332
+		if ($expr = self::tryExpressionAndFill($tokens, 0, count($tokens) - 1, $address)) return $expr;
1333
+		if ($expr = self::tryExpression($tokens, 0, count($tokens) - 1, $address)) return $expr;
1330
 		return null;
1334
 		return null;
1331
 	}
1335
 	}
1332
 
1336
 
1340
 	private static function tryExpressionAndFormat(array $tokens, int $start,
1344
 	private static function tryExpressionAndFormat(array $tokens, int $start,
1341
 			int $end, CellAddress $address): ?CellExpression {
1345
 			int $end, CellAddress $address): ?CellExpression {
1342
 		for ($t = $start + 1; $t < $end; $t++) {
1346
 		for ($t = $start + 1; $t < $end; $t++) {
1343
-			if ($tokens[$t]->type == CellExpressionTokenType::Semicolon) {
1347
+			if ($tokens[$t]->type === CellExpressionTokenType::Semicolon) {
1344
 				$expr = self::tryExpressionAndFill($tokens, $start, $t - 1, $address) ??
1348
 				$expr = self::tryExpressionAndFill($tokens, $start, $t - 1, $address) ??
1345
 					self::tryExpression($tokens, $start, $t - 1, $address);
1349
 					self::tryExpression($tokens, $start, $t - 1, $address);
1346
 				if ($expr === null) return null;
1350
 				if ($expr === null) return null;
1366
 		if ($count < 2) return null;
1370
 		if ($count < 2) return null;
1367
 		if (!$tokens[$end]->type->isPotentialName()) return null;
1371
 		if (!$tokens[$end]->type->isPotentialName()) return null;
1368
 		$name = mb_strtoupper($tokens[$end]->content);
1372
 		$name = mb_strtoupper($tokens[$end]->content);
1369
-		if ($name != 'FILL') return null;
1373
+		if ($name !== 'FILL') return null;
1370
 		$exp = self::tryExpression($tokens, $start, $end - 1, $address);
1374
 		$exp = self::tryExpression($tokens, $start, $end - 1, $address);
1371
 		$columnIndex = $address->columnIndex;
1375
 		$columnIndex = $address->columnIndex;
1372
 		$exp->fillRanges = [
1376
 		$exp->fillRanges = [
1396
 		$count = $end - $start + 1;
1400
 		$count = $end - $start + 1;
1397
 		if ($count < 0 || $count > 2) return null;
1401
 		if ($count < 0 || $count > 2) return null;
1398
 		if (!$tokens[$start]->type->isPotentialName()) return null;
1402
 		if (!$tokens[$start]->type->isPotentialName()) return null;
1399
-		$type = mb_strtolower($tokens[$start]);
1403
+		$type = mb_strtolower($tokens[$start]->content);
1400
 		if (!CellValue::isTypeNumeric($type)) return null;
1404
 		if (!CellValue::isTypeNumeric($type)) return null;
1401
 		if ($count > 1) {
1405
 		if ($count > 1) {
1402
-			if ($tokens[$start + 1]->type != CellExpressionTokenType::Number) return null;
1406
+			if ($tokens[$start + 1]->type !== CellExpressionTokenType::Number) return null;
1403
 			$decimals = intval($tokens[$start + 1]->content);
1407
 			$decimals = intval($tokens[$start + 1]->content);
1404
 		} else {
1408
 		} else {
1405
 			$decimals = null;
1409
 			$decimals = null;
1437
 	 */
1441
 	 */
1438
 	private static function tryParenExpression(array $tokens, int $start,
1442
 	private static function tryParenExpression(array $tokens, int $start,
1439
 			int $end, CellAddress $address): ?CellExpression {
1443
 			int $end, CellAddress $address): ?CellExpression {
1440
-		if ($tokens[$start]->type != CellExpressionTokenType::OpenParen) return null;
1441
-		if ($tokens[$end]->type != CellExpressionTokenType::CloseParen) return null;
1444
+		if ($tokens[$start]->type !== CellExpressionTokenType::OpenParen) return null;
1445
+		if ($tokens[$end]->type !== CellExpressionTokenType::CloseParen) return null;
1442
 		$parenLevel = 0;
1446
 		$parenLevel = 0;
1443
 		for ($t = $start + 1; $t < $end; $t++) {
1447
 		for ($t = $start + 1; $t < $end; $t++) {
1444
-			if ($tokens[$t]->type == CellExpressionTokenType::OpenParen) {
1448
+			if ($tokens[$t]->type === CellExpressionTokenType::OpenParen) {
1445
 				$parenLevel++;
1449
 				$parenLevel++;
1446
-			} elseif ($tokens[$t]->type == CellExpressionTokenType::CloseParen) {
1450
+			} elseif ($tokens[$t]->type === CellExpressionTokenType::CloseParen) {
1447
 				$parenLevel--;
1451
 				$parenLevel--;
1448
 			}
1452
 			}
1449
 			if ($parenLevel < 0) return null;
1453
 			if ($parenLevel < 0) return null;
1450
 		}
1454
 		}
1451
-		if ($parenLevel != 0) return null;
1455
+		if ($parenLevel !== 0) return null;
1452
 		return self::tryExpression($tokens, $start + 1, $end - 1, $address);
1456
 		return self::tryExpression($tokens, $start + 1, $end - 1, $address);
1453
 	}
1457
 	}
1454
 
1458
 
1461
 	 */
1465
 	 */
1462
 	private static function tryNumber(array $tokens, int $start, int $end,
1466
 	private static function tryNumber(array $tokens, int $start, int $end,
1463
 			CellAddress $address): ?CellExpression {
1467
 			CellAddress $address): ?CellExpression {
1464
-		if ($tokens[$end]->type != CellExpressionTokenType::Number) return null;
1468
+		if ($tokens[$end]->type !== CellExpressionTokenType::Number) return null;
1465
 		if ($end > $start + 1) return null;
1469
 		if ($end > $start + 1) return null;
1466
 		$val = CellValue::fromCellString($tokens[$end]->content);
1470
 		$val = CellValue::fromCellString($tokens[$end]->content);
1467
 		if ($end > $start) {
1471
 		if ($end > $start) {
1468
-			if ($tokens[$start]->type != CellExpressionTokenType::Minus) return null;
1472
+			if ($tokens[$start]->type !== CellExpressionTokenType::Minus) return null;
1469
 			$val->value = -$val->value;
1473
 			$val->value = -$val->value;
1470
 		}
1474
 		}
1471
 		return new CellExpression(CellExpressionOperation::Number, [ $val ]);
1475
 		return new CellExpression(CellExpressionOperation::Number, [ $val ]);
1480
 	 */
1484
 	 */
1481
 	private static function tryString(array $tokens, int $start, int $end,
1485
 	private static function tryString(array $tokens, int $start, int $end,
1482
 			CellAddress $address): ?CellExpression {
1486
 			CellAddress $address): ?CellExpression {
1483
-		if ($start != $end) return null;
1484
-		if ($tokens[$start]->type != CellExpressionTokenType::String) return null;
1487
+		if ($start !== $end) return null;
1488
+		if ($tokens[$start]->type !== CellExpressionTokenType::String) return null;
1485
 		$str = $tokens[$start]->content;
1489
 		$str = $tokens[$start]->content;
1486
 		return new CellExpression(CellExpressionOperation::String,
1490
 		return new CellExpression(CellExpressionOperation::String,
1487
 			[ new CellValue($str, $str, CellValue::TYPE_STRING, 0) ]);
1491
 			[ new CellValue($str, $str, CellValue::TYPE_STRING, 0) ]);
1496
 	 */
1500
 	 */
1497
 	private static function tryBoolean(array $tokens, int $start, int $end,
1501
 	private static function tryBoolean(array $tokens, int $start, int $end,
1498
 			CellAddress $address): ?CellExpression {
1502
 			CellAddress $address): ?CellExpression {
1499
-		if ($start != $end) return null;
1503
+		if ($start !== $end) return null;
1500
 		if (!$tokens[$start]->type->isPotentialName()) return null;
1504
 		if (!$tokens[$start]->type->isPotentialName()) return null;
1501
 		$str = mb_strtoupper($tokens[$start]->content);
1505
 		$str = mb_strtoupper($tokens[$start]->content);
1502
-		if ($str != 'TRUE' && $str != 'FALSE') return null;
1506
+		if ($str !== 'TRUE' && $str !== 'FALSE') return null;
1503
 		return new CellExpression(CellExpressionOperation::Boolean,
1507
 		return new CellExpression(CellExpressionOperation::Boolean,
1504
-			[ new CellValue($str, $str == 'TRUE', CellValue::TYPE_BOOLEAN) ]);
1508
+			[ new CellValue($str, $str === 'TRUE', CellValue::TYPE_BOOLEAN) ]);
1505
 	}
1509
 	}
1506
 
1510
 
1507
 	/**
1511
 	/**
1517
 		if ($count < 3) return null;
1521
 		if ($count < 3) return null;
1518
 		if (!$tokens[$start]->type->isPotentialName()) return null;
1522
 		if (!$tokens[$start]->type->isPotentialName()) return null;
1519
 		$qualifier = $tokens[$start]->content;
1523
 		$qualifier = $tokens[$start]->content;
1520
-		if ($tokens[$start + 1]->type != CellExpressionTokenType::OpenParen) return null;
1521
-		if ($tokens[$end]->type != CellExpressionTokenType::CloseParen) return null;
1524
+		if ($tokens[$start + 1]->type !== CellExpressionTokenType::OpenParen) return null;
1525
+		if ($tokens[$end]->type !== CellExpressionTokenType::CloseParen) return null;
1522
 		$argList = self::tryArgumentList($tokens, $start + 2, $end - 1, $address);
1526
 		$argList = self::tryArgumentList($tokens, $start + 2, $end - 1, $address);
1523
 		if ($argList === null) return null;
1527
 		if ($argList === null) return null;
1524
 		return new CellExpression(CellExpressionOperation::Function, $argList, $qualifier);
1528
 		return new CellExpression(CellExpressionOperation::Function, $argList, $qualifier);
1534
 	private static function tryArgumentList(array $tokens, int $start, int $end,
1538
 	private static function tryArgumentList(array $tokens, int $start, int $end,
1535
 			CellAddress $address): ?array {
1539
 			CellAddress $address): ?array {
1536
 		$count = $end - $start + 1;
1540
 		$count = $end - $start + 1;
1537
-		if ($count == 0) return [];
1541
+		if ($count === 0) return [];
1538
 		$parenDepth = 0;
1542
 		$parenDepth = 0;
1539
 		$argCount = 1;
1543
 		$argCount = 1;
1540
 
1544
 
1543
 		$argTokens = []; // argindex -> [ start, end ]
1547
 		$argTokens = []; // argindex -> [ start, end ]
1544
 		$exprStartToken = $start;
1548
 		$exprStartToken = $start;
1545
 		for ($i = $start; $i <= $end; $i++) {
1549
 		for ($i = $start; $i <= $end; $i++) {
1546
-			if ($tokens[$i]->type == CellExpressionTokenType::OpenParen) {
1550
+			if ($tokens[$i]->type === CellExpressionTokenType::OpenParen) {
1547
 				$parenDepth++;
1551
 				$parenDepth++;
1548
-			} elseif ($tokens[$i]->type == CellExpressionTokenType::CloseParen) {
1552
+			} elseif ($tokens[$i]->type === CellExpressionTokenType::CloseParen) {
1549
 				$parenDepth--;
1553
 				$parenDepth--;
1550
-			} elseif ($tokens[$i]->type == CellExpressionTokenType::Comma && $parenDepth == 0) {
1554
+			} elseif ($tokens[$i]->type === CellExpressionTokenType::Comma && $parenDepth === 0) {
1551
 				$exprEndToken = $i - 1;
1555
 				$exprEndToken = $i - 1;
1552
 				array_push($argTokens, [ $exprStartToken, $exprEndToken ]);
1556
 				array_push($argTokens, [ $exprStartToken, $exprEndToken ]);
1553
 				$exprStartToken = $i + 1;
1557
 				$exprStartToken = $i + 1;
1575
 	private static function tryRange(array $tokens, int $start, int $end,
1579
 	private static function tryRange(array $tokens, int $start, int $end,
1576
 			CellAddress $address): ?CellExpression {
1580
 			CellAddress $address): ?CellExpression {
1577
 		$count = $end - $start + 1;
1581
 		$count = $end - $start + 1;
1578
-		if ($count != 3) return null;
1582
+		if ($count !== 3) return null;
1579
 		if (!$tokens[$start]->type->isPotentialAddress()) return null;
1583
 		if (!$tokens[$start]->type->isPotentialAddress()) return null;
1580
 		$first = mb_strtoupper($tokens[$start]->content);
1584
 		$first = mb_strtoupper($tokens[$start]->content);
1581
-		if ($tokens[$start + 1]->type != CellExpressionTokenType::Colon) return null;
1585
+		if ($tokens[$start + 1]->type !== CellExpressionTokenType::Colon) return null;
1582
 		if (!$tokens[$end]->type->isPotentialAddress()) return null;
1586
 		if (!$tokens[$end]->type->isPotentialAddress()) return null;
1583
 		$last = mb_strtoupper($tokens[$end]->content);
1587
 		$last = mb_strtoupper($tokens[$end]->content);
1584
 		$firstAddress = CellAddress::fromString($first);
1588
 		$firstAddress = CellAddress::fromString($first);
1596
 	 */
1600
 	 */
1597
 	private static function tryReference(array $tokens, int $start, int $end,
1601
 	private static function tryReference(array $tokens, int $start, int $end,
1598
 			CellAddress $address): ?CellExpression {
1602
 			CellAddress $address): ?CellExpression {
1599
-		if ($start != $end) return null;
1603
+		if ($start !== $end) return null;
1600
 		if (!$tokens[$start]->type->isPotentialAddress()) return null;
1604
 		if (!$tokens[$start]->type->isPotentialAddress()) return null;
1601
 		$ref = mb_strtoupper($tokens[$start]->content);
1605
 		$ref = mb_strtoupper($tokens[$start]->content);
1602
 		$refAddress = CellAddress::fromString($ref, $address, true);
1606
 		$refAddress = CellAddress::fromString($ref, $address, true);
1631
 		$candidates = [];
1635
 		$candidates = [];
1632
 		$parenLevel = 0;
1636
 		$parenLevel = 0;
1633
 		for ($i = $start; $i <= $end; $i++) {
1637
 		for ($i = $start; $i <= $end; $i++) {
1634
-			if ($tokens[$i]->type == CellExpressionTokenType::OpenParen) {
1638
+			if ($tokens[$i]->type === CellExpressionTokenType::OpenParen) {
1635
 				$parenLevel++;
1639
 				$parenLevel++;
1636
-			} elseif ($tokens[$i]->type == CellExpressionTokenType::CloseParen) {
1640
+			} elseif ($tokens[$i]->type === CellExpressionTokenType::CloseParen) {
1637
 				$parenLevel--;
1641
 				$parenLevel--;
1638
-			} elseif ($parenLevel == 0 && $i > $start && $i < $end) {
1642
+			} elseif ($parenLevel === 0 && $i > $start && $i < $end) {
1639
 				$op = $tokens[$i]->type->name;
1643
 				$op = $tokens[$i]->type->name;
1640
 				$priority = self::infixPriority[$op] ?? false;
1644
 				$priority = self::infixPriority[$op] ?? false;
1641
 				if ($priority === false) continue;
1645
 				if ($priority === false) continue;
1706
 			[ CellExpressionTokenType::Not, CellExpressionOperation::UnaryNot ],
1710
 			[ CellExpressionTokenType::Not, CellExpressionOperation::UnaryNot ],
1707
 		];
1711
 		];
1708
 		foreach ($ops as $op) {
1712
 		foreach ($ops as $op) {
1709
-			if ($tokens[$start]->type != $op[0]) continue;
1713
+			if ($tokens[$start]->type !== $op[0]) continue;
1710
 			$operand = self::tryExpression($tokens, $start + 1, $end, $address);
1714
 			$operand = self::tryExpression($tokens, $start + 1, $end, $address);
1711
 			if ($operand === null) return null;
1715
 			if ($operand === null) return null;
1712
 			return new CellExpression($op[1], [ $operand ]);
1716
 			return new CellExpression($op[1], [ $operand ]);
1805
 	 * Tests if a string is formatted like an address.
1809
 	 * Tests if a string is formatted like an address.
1806
 	 */
1810
 	 */
1807
 	public static function isAddress(string $text): bool {
1811
 	public static function isAddress(string $text): bool {
1808
-		return self::fromString($text) != null;
1812
+		return self::fromString($text) !== null;
1809
 	}
1813
 	}
1810
 
1814
 
1811
 	/**
1815
 	/**
1847
 		if (!$this->isResolved && $resolveToRow) {
1851
 		if (!$this->isResolved && $resolveToRow) {
1848
 			$newRowIndex = $relativeFrom->rowIndex;
1852
 			$newRowIndex = $relativeFrom->rowIndex;
1849
 		}
1853
 		}
1850
-		if ($newRowIndex != -1 && !$this->isRowFixed) {
1854
+		if ($newRowIndex !== -1 && !$this->isRowFixed) {
1851
 			$rowDelta = $relativeTo->rowIndex - $relativeFrom->rowIndex;
1855
 			$rowDelta = $relativeTo->rowIndex - $relativeFrom->rowIndex;
1852
 			$newRowIndex += $rowDelta;
1856
 			$newRowIndex += $rowDelta;
1853
 		}
1857
 		}
1857
 
1861
 
1858
 	public function equals($other): bool {
1862
 	public function equals($other): bool {
1859
 		if (!($other instanceof CellAddress)) return false;
1863
 		if (!($other instanceof CellAddress)) return false;
1860
-		return $other->columnIndex == $this->columnIndex && $other->rowIndex == $this->rowIndex;
1864
+		return $other->columnIndex === $this->columnIndex && $other->rowIndex === $this->rowIndex;
1861
 	}
1865
 	}
1862
 
1866
 
1863
 	public function exactlyEquals($other): bool {
1867
 	public function exactlyEquals($other): bool {
1864
 		if (!($other instanceof CellAddress)) return false;
1868
 		if (!($other instanceof CellAddress)) return false;
1865
-		return $other->name == $this->name;
1869
+		return $other->name === $this->name;
1866
 	}
1870
 	}
1867
 
1871
 
1868
 	public function __toString(): string {
1872
 	public function __toString(): string {
1927
 			if ($throwIfInvalid) throw new CellEvaluationException("Bad address \"{$address}\"", '#REF');
1931
 			if ($throwIfInvalid) throw new CellEvaluationException("Bad address \"{$address}\"", '#REF');
1928
 			return null;
1932
 			return null;
1929
 		}
1933
 		}
1930
-		$isColumnFixed = ($groups[1] == '$');
1934
+		$isColumnFixed = ($groups[1] === '$');
1931
 		$letters = mb_strtoupper($groups[2]);
1935
 		$letters = mb_strtoupper($groups[2]);
1932
-		$isRowFixed = ($groups[3] == '$');
1936
+		$isRowFixed = ($groups[3] === '$');
1933
 		$numbers = $groups[4];
1937
 		$numbers = $groups[4];
1934
 		$columnIndex = self::lettersToColumnIndex($letters);
1938
 		$columnIndex = self::lettersToColumnIndex($letters);
1935
-		$rowIndex = (mb_strlen($numbers) == 0) ? -1 : intval($numbers) - 1;
1939
+		$rowIndex = (mb_strlen($numbers) === 0) ? -1 : intval($numbers) - 1;
1936
 		if ($columnIndex < 0 && $relativeTo !== null) $columnIndex = $relativeTo->columnIndex;
1940
 		if ($columnIndex < 0 && $relativeTo !== null) $columnIndex = $relativeTo->columnIndex;
1937
 		if ($rowIndex < 0 && $relativeTo !== null) $rowIndex = $relativeTo->rowIndex;
1941
 		if ($rowIndex < 0 && $relativeTo !== null) $rowIndex = $relativeTo->rowIndex;
1938
 		return new CellAddress($columnIndex, $rowIndex, $isColumnFixed, $isRowFixed);
1942
 		return new CellAddress($columnIndex, $rowIndex, $isColumnFixed, $isRowFixed);
1958
 	 * @param CellAddress $toCell
1962
 	 * @param CellAddress $toCell
1959
 	 */
1963
 	 */
1960
 	public function __construct(CellAddress $fromCell, CellAddress $toCell) {
1964
 	public function __construct(CellAddress $fromCell, CellAddress $toCell) {
1961
-		if ($fromCell->isResolved != $toCell->isResolved) {
1965
+		if ($fromCell->isResolved !== $toCell->isResolved) {
1962
 			throw new CellEvaluationException("Cannot mix resolved and unresolved cell addresses in range: {$fromCell->name} and {$toCell->name}");
1966
 			throw new CellEvaluationException("Cannot mix resolved and unresolved cell addresses in range: {$fromCell->name} and {$toCell->name}");
1963
 		}
1967
 		}
1964
 		$this->minColumnIndex = min($fromCell->columnIndex, $toCell->columnIndex);
1968
 		$this->minColumnIndex = min($fromCell->columnIndex, $toCell->columnIndex);
1988
 	 * @return object iterable object
1992
 	 * @return object iterable object
1989
 	 */
1993
 	 */
1990
 	public function cellsIn(SpreadsheetGrid $grid): Iterator {
1994
 	public function cellsIn(SpreadsheetGrid $grid): Iterator {
1991
-		$min_col = $this->minColumnIndex;
1992
-		$max_col = $this->maxColumnIndex;
1993
-		$min_row = $this->minRowIndex;
1994
-		$max_row = $this->maxRowIndex;
1995
-		if ($min_row == -1) {
1996
-			$min_row = 0;
1997
-		}
1998
-		if ($max_row == -1) {
1999
-			$max_row = $grid->rowCount - 1;
2000
-		}
2001
-		$max_col = min($max_col, $grid->columnCount);
2002
-		return new class($grid, $min_col, $max_col, $min_row, $max_row) implements Iterator {
1995
+		$minCol = $this->minColumnIndex;
1996
+		$maxCol = $this->maxColumnIndex;
1997
+		$minRow = $this->minRowIndex;
1998
+		$maxRow = $this->maxRowIndex;
1999
+		if ($minRow === -1) {
2000
+			$minRow = 0;
2001
+		}
2002
+		if ($maxRow === -1) {
2003
+			$maxRow = $grid->rowCount - 1;
2004
+		}
2005
+		$maxCol = min($maxCol, $grid->columnCount);
2006
+		return new class($grid, $minCol, $maxCol, $minRow, $maxRow) implements Iterator {
2003
 			private SpreadsheetGrid $grid;
2007
 			private SpreadsheetGrid $grid;
2004
-			private int $min_col;
2005
-			private int $max_col;
2006
-			private int $min_row;
2007
-			private int $max_row;
2008
+			private int $minCol;
2009
+			private int $maxCol;
2010
+			private int $minRow;
2011
+			private int $maxRow;
2008
 			private int $col;
2012
 			private int $col;
2009
 			private int $row;
2013
 			private int $row;
2010
 			private ?CellAddress $address = null;
2014
 			private ?CellAddress $address = null;
2011
 			private ?SpreadsheetCell $cell = null;
2015
 			private ?SpreadsheetCell $cell = null;
2012
-			function __construct($grid, $min_col, $max_col, $min_row, $max_row) {
2016
+			function __construct($grid, $minCol, $maxCol, $minRow, $maxRow) {
2013
 				$this->grid = $grid;
2017
 				$this->grid = $grid;
2014
-				$this->min_col = $min_col;
2015
-				$this->max_col = $max_col;
2016
-				$this->min_row = $min_row;
2017
-				$this->max_row = $max_row;
2018
-				$this->col = $min_col;
2019
-				$this->row = $min_row;
2018
+				$this->minCol = $minCol;
2019
+				$this->maxCol = $maxCol;
2020
+				$this->minRow = $minRow;
2021
+				$this->maxRow = $maxRow;
2022
+				$this->col = $minCol;
2023
+				$this->row = $minRow;
2020
 				$this->setValues();
2024
 				$this->setValues();
2021
 			}
2025
 			}
2022
 
2026
 
2023
 			private function setValues() {
2027
 			private function setValues() {
2024
-				if ($this->col >= $this->min_col && $this->col <= $this->max_col &&
2025
-						$this->row >= $this->min_row && $this->row <= $this->max_row) {
2028
+				if ($this->col >= $this->minCol && $this->col <= $this->maxCol &&
2029
+						$this->row >= $this->minRow && $this->row <= $this->maxRow) {
2026
 					$this->address = new CellAddress($this->col, $this->row);
2030
 					$this->address = new CellAddress($this->col, $this->row);
2027
 					$this->cell = $this->grid->cellAt($this->address);
2031
 					$this->cell = $this->grid->cellAt($this->address);
2028
 					if (!$this->cell) {
2032
 					if (!$this->cell) {
2036
 
2040
 
2037
 			private function increment(): void {
2041
 			private function increment(): void {
2038
 				$this->row++;
2042
 				$this->row++;
2039
-				if ($this->row > $this->max_row) {
2040
-					$this->row = $this->min_row;
2043
+				if ($this->row > $this->maxRow) {
2044
+					$this->row = $this->minRow;
2041
 					$this->col++;
2045
 					$this->col++;
2042
 				}
2046
 				}
2043
 				$this->setValues();
2047
 				$this->setValues();
2056
 			}
2060
 			}
2057
 
2061
 
2058
 			function rewind(): void {
2062
 			function rewind(): void {
2059
-				$this->row = $this->min_row;
2060
-				$this->col = $this->min_col;
2063
+				$this->row = $this->minRow;
2064
+				$this->col = $this->minCol;
2061
 				$this->setValues();
2065
 				$this->setValues();
2062
 			}
2066
 			}
2063
 
2067
 
2064
 			function valid(): bool {
2068
 			function valid(): bool {
2065
-				return $this->address != null;
2069
+				return $this->address !== null;
2066
 			}
2070
 			}
2067
 		};
2071
 		};
2068
 	}
2072
 	}
2188
 		if (is_numeric($value)) {
2192
 		if (is_numeric($value)) {
2189
 			$resolvedType = $type ?? CellValue::TYPE_NUMBER;
2193
 			$resolvedType = $type ?? CellValue::TYPE_NUMBER;
2190
 			$resolvedDecimals = ($decimals !== null) ? $decimals :
2194
 			$resolvedDecimals = ($decimals !== null) ? $decimals :
2191
-				($resolvedType == CellValue::TYPE_CURRENCY ? 2 :
2192
-					CellValue::autodecimals($resolvedType == CellValue::TYPE_PERCENT ?
2195
+				($resolvedType === CellValue::TYPE_CURRENCY ? 2 :
2196
+					CellValue::autodecimals($resolvedType === CellValue::TYPE_PERCENT ?
2193
 						$value * 100.0 : $value));
2197
 						$value * 100.0 : $value));
2194
 			$formatted = CellValue::formatType($value, $resolvedType, $resolvedDecimals);
2198
 			$formatted = CellValue::formatType($value, $resolvedType, $resolvedDecimals);
2195
 			return new CellValue($formatted, $value, $resolvedType, $resolvedDecimals);
2199
 			return new CellValue($formatted, $value, $resolvedType, $resolvedDecimals);
2208
 		$cellString = ($cellString !== null) ? trim($cellString) : null;
2212
 		$cellString = ($cellString !== null) ? trim($cellString) : null;
2209
 		$this->formattedValue = $cellString;
2213
 		$this->formattedValue = $cellString;
2210
 		// blank
2214
 		// blank
2211
-		if ($cellString === null || $cellString == '') {
2215
+		if ($cellString === null || $cellString === '') {
2212
 			$this->type = CellValue::TYPE_BLANK;
2216
 			$this->type = CellValue::TYPE_BLANK;
2213
 			$this->value = null;
2217
 			$this->value = null;
2214
 			return;
2218
 			return;
2223
 		}
2227
 		}
2224
 		// =TRUE
2228
 		// =TRUE
2225
 		$caps = mb_strtoupper($cellString);
2229
 		$caps = mb_strtoupper($cellString);
2226
-		if ($caps == 'TRUE') {
2230
+		if ($caps === 'TRUE') {
2227
 			$this->type = CellValue::TYPE_BOOLEAN;
2231
 			$this->type = CellValue::TYPE_BOOLEAN;
2228
 			$this->formattedValue = $caps;
2232
 			$this->formattedValue = $caps;
2229
 			$this->value = true;
2233
 			$this->value = true;
2231
 			return;
2235
 			return;
2232
 		}
2236
 		}
2233
 		// =FALSE
2237
 		// =FALSE
2234
-		if ($caps == 'FALSE') {
2238
+		if ($caps === 'FALSE') {
2235
 			$this->type = CellValue::TYPE_BOOLEAN;
2239
 			$this->type = CellValue::TYPE_BOOLEAN;
2236
 			$this->formattedValue = $caps;
2240
 			$this->formattedValue = $caps;
2237
 			$this->value = false;
2241
 			$this->value = false;
2319
 			case CellValue::TYPE_CURRENCY:
2323
 			case CellValue::TYPE_CURRENCY:
2320
 			case CellValue::TYPE_NUMBER:
2324
 			case CellValue::TYPE_NUMBER:
2321
 			case CellValue::TYPE_PERCENT:
2325
 			case CellValue::TYPE_PERCENT:
2322
-				return $this->value != 0;
2326
+				return $this->value !== 0;
2323
 			case CellValue::TYPE_ERROR:
2327
 			case CellValue::TYPE_ERROR:
2324
 			case CellValue::TYPE_FORMULA:
2328
 			case CellValue::TYPE_FORMULA:
2325
 			case CellValue::TYPE_STRING:
2329
 			case CellValue::TYPE_STRING:
2401
 	 */
2405
 	 */
2402
 	public function divide(CellValue $b): CellValue {
2406
 	public function divide(CellValue $b): CellValue {
2403
 		return self::binaryNumericOperation($this, $b, '/', function($aVal, $bVal) {
2407
 		return self::binaryNumericOperation($this, $b, '/', function($aVal, $bVal) {
2404
-			if ($bVal == 0) throw new CellEvaluationException("Division by zero", '#NAN');
2408
+			if ($bVal === 0) throw new CellEvaluationException("Division by zero", '#NAN');
2405
 			return $aVal / $bVal;
2409
 			return $aVal / $bVal;
2406
 		});
2410
 		});
2407
 	}
2411
 	}
2413
 	 */
2417
 	 */
2414
 	public function modulo($b) {
2418
 	public function modulo($b) {
2415
 		return self::binaryNumericOperation($this, $b, '%', function($aVal, $bVal) {
2419
 		return self::binaryNumericOperation($this, $b, '%', function($aVal, $bVal) {
2416
-			if ($bVal == 0) throw new CellEvaluationException("Division by zero", '#NAN');
2420
+			if ($bVal === 0) throw new CellEvaluationException("Division by zero", '#NAN');
2417
 			return $aVal % $bVal;
2421
 			return $aVal % $bVal;
2418
 		});
2422
 		});
2419
 	}
2423
 	}
2450
 	 * Returns the result of whether this value is equal to `$b`.
2454
 	 * Returns the result of whether this value is equal to `$b`.
2451
 	 */
2455
 	 */
2452
 	public function eq(CellValue $b): CellValue {
2456
 	public function eq(CellValue $b): CellValue {
2453
-		return self::fromValue(CellValue::compare($this, $b) == 0);
2457
+		return self::fromValue(CellValue::compare($this, $b) === 0);
2454
 	}
2458
 	}
2455
 
2459
 
2456
 	/**
2460
 	/**
2457
 	 * Returns the result of whether this value is unequal to `$b`.
2461
 	 * Returns the result of whether this value is unequal to `$b`.
2458
 	 */
2462
 	 */
2459
 	public function neq(CellValue $b): CellValue {
2463
 	public function neq(CellValue $b): CellValue {
2460
-		return self::fromValue(CellValue::compare($this, $b) != 0);
2464
+		return self::fromValue(CellValue::compare($this, $b) !== 0);
2461
 	}
2465
 	}
2462
 
2466
 
2463
 	/**
2467
 	/**
2470
 			case CellValue::TYPE_CURRENCY:
2474
 			case CellValue::TYPE_CURRENCY:
2471
 			case CellValue::TYPE_NUMBER:
2475
 			case CellValue::TYPE_NUMBER:
2472
 			case CellValue::TYPE_PERCENT:
2476
 			case CellValue::TYPE_PERCENT:
2473
-				return self::fromValue($this->value == 0);
2477
+				return self::fromValue($this->value === 0);
2474
 			case CellValue::TYPE_STRING:
2478
 			case CellValue::TYPE_STRING:
2475
 				throw new CellEvaluationException("Cannot perform NOT on string");
2479
 				throw new CellEvaluationException("Cannot perform NOT on string");
2476
 			case CellValue::TYPE_BOOLEAN:
2480
 			case CellValue::TYPE_BOOLEAN:
2528
 	 * @throws CellEvaluationException  if types are incompatible for numeric operations
2532
 	 * @throws CellEvaluationException  if types are incompatible for numeric operations
2529
 	 */
2533
 	 */
2530
 	private static function resolveNumericOperands(CellValue $a, CellValue $b, string $op): array {
2534
 	private static function resolveNumericOperands(CellValue $a, CellValue $b, string $op): array {
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) {
2535
+		if ($a->type === self::TYPE_ERROR) throw $a->value;
2536
+		if ($b->type === self::TYPE_ERROR) throw $b->value;
2537
+		if ($a->type === self::TYPE_STRING || $b->type === self::TYPE_STRING) {
2534
 			throw new CellEvaluationException("Cannot perform math on text values");
2538
 			throw new CellEvaluationException("Cannot perform math on text values");
2535
 		}
2539
 		}
2536
 
2540
 
2537
-		if ($a->type == self::TYPE_BLANK) {
2538
-			if ($b->type == self::TYPE_BLANK) {
2541
+		if ($a->type === self::TYPE_BLANK) {
2542
+			if ($b->type === self::TYPE_BLANK) {
2539
 				return [ 0, 0, self::TYPE_NUMBER, 0 ];
2543
 				return [ 0, 0, self::TYPE_NUMBER, 0 ];
2540
 			}
2544
 			}
2541
 			return [ 0, $b->value, $b->type ];
2545
 			return [ 0, $b->value, $b->type ];
2542
-		} elseif ($b->type == self::TYPE_BLANK) {
2546
+		} elseif ($b->type === self::TYPE_BLANK) {
2543
 			return [ $a->value, 0, $a->type ];
2547
 			return [ $a->value, 0, $a->type ];
2544
 		}
2548
 		}
2545
 
2549
 
2546
-		$isMultOrDiv = ($op == '*' || $op == '/' || $op == '%');
2550
+		$isMultOrDiv = ($op === '*' || $op === '/' || $op === '%');
2547
 
2551
 
2548
-		if ($a->type == $b->type) {
2552
+		if ($a->type === $b->type) {
2549
 			return [ $a->value, $b->value, $a->type ];
2553
 			return [ $a->value, $b->value, $a->type ];
2550
 		}
2554
 		}
2551
 
2555
 
2604
 	 * @throws CellException  if `$a` or `$b` is of type `TYPE_ERROR`
2608
 	 * @throws CellException  if `$a` or `$b` is of type `TYPE_ERROR`
2605
 	 */
2609
 	 */
2606
 	private static function resolveComparableArguments(CellValue $a, CellValue $b): array {
2610
 	private static function resolveComparableArguments(CellValue $a, CellValue $b): array {
2607
-		if ($a->type == CellValue::TYPE_ERROR) throw $a->value;
2608
-		if ($b->type == CellValue::TYPE_ERROR) throw $b->value;
2609
-		if ($a->type == CellValue::TYPE_FORMULA) throw new CellEvaluationException("Can't compare formula values");
2610
-		if ($b->type == CellValue::TYPE_FORMULA) throw new CellEvaluationException("Can't compare formula values");
2611
+		if ($a->type === CellValue::TYPE_ERROR) throw $a->value;
2612
+		if ($b->type === CellValue::TYPE_ERROR) throw $b->value;
2613
+		if ($a->type === CellValue::TYPE_FORMULA) throw new CellEvaluationException("Can't compare formula values");
2614
+		if ($b->type === CellValue::TYPE_FORMULA) throw new CellEvaluationException("Can't compare formula values");
2611
 		$aNumValue = $a->value;
2615
 		$aNumValue = $a->value;
2612
 		$bNumValue = $b->value;
2616
 		$bNumValue = $b->value;
2613
 		$aStrValue = "{$a->value}";
2617
 		$aStrValue = "{$a->value}";
2630
 				$bNumValue = ($bNumValue) ? 1 : 0;
2634
 				$bNumValue = ($bNumValue) ? 1 : 0;
2631
 				break;
2635
 				break;
2632
 		}
2636
 		}
2633
-		if ($a->type == CellValue::TYPE_STRING || $b->type == CellValue::TYPE_STRING) {
2637
+		if ($a->type === CellValue::TYPE_STRING || $b->type === CellValue::TYPE_STRING) {
2634
 			return [ $aStrValue, $bStrValue ];
2638
 			return [ $aStrValue, $bStrValue ];
2635
 		}
2639
 		}
2636
 		return [ $aNumValue, $bNumValue ];
2640
 		return [ $aNumValue, $bNumValue ];
2691
 	 * Tests if a type is numeric.
2695
 	 * Tests if a type is numeric.
2692
 	 */
2696
 	 */
2693
 	public static function isTypeNumeric(string $type): bool {
2697
 	public static function isTypeNumeric(string $type): bool {
2694
-		return $type == CellValue::TYPE_NUMBER ||
2695
-			$type == CellValue::TYPE_PERCENT ||
2696
-			$type == CellValue::TYPE_CURRENCY ||
2697
-			$type == CellValue::TYPE_BOOLEAN;
2698
+		return $type === CellValue::TYPE_NUMBER ||
2699
+			$type === CellValue::TYPE_PERCENT ||
2700
+			$type === CellValue::TYPE_CURRENCY ||
2701
+			$type === CellValue::TYPE_BOOLEAN;
2698
 	}
2702
 	}
2699
 
2703
 
2700
 	public function __toString(): string {
2704
 	public function __toString(): string {
2736
 	 */
2740
 	 */
2737
 	public function cellAt(CellAddress $address): SpreadsheetCell {
2741
 	public function cellAt(CellAddress $address): SpreadsheetCell {
2738
 		$c = $address->columnIndex; $r = $address->rowIndex;
2742
 		$c = $address->columnIndex; $r = $address->rowIndex;
2739
-		if ($c < 0 || $c >= sizeof($this->cells)) throw new CellEvaluationException("Unresolved cell address {$address->name}", '#REF');
2743
+		if ($c < 0 || $c >= count($this->cells)) throw new CellEvaluationException("Unresolved cell address {$address->name}", '#REF');
2740
 		$col = $this->cells[$c];
2744
 		$col = $this->cells[$c];
2741
-		if ($r < 0 || $r >= sizeof($col)) throw new CellEvaluationException("Unresolved cell address {$address->name}", '#REF');
2745
+		if ($r < 0 || $r >= count($col)) throw new CellEvaluationException("Unresolved cell address {$address->name}", '#REF');
2742
 		return $col[$r];
2746
 		return $col[$r];
2743
 	}
2747
 	}
2744
 
2748
 
2796
 
2800
 
2797
 	private function processTable(MDTableNode $tableNode, MDState $state) {
2801
 	private function processTable(MDTableNode $tableNode, MDState $state) {
2798
 		// Measure table
2802
 		// Measure table
2799
-		$rowCount = sizeof($tableNode->bodyRows());
2803
+		$rowCount = count($tableNode->bodyRows());
2800
 		$columnCount = 0;
2804
 		$columnCount = 0;
2801
 		foreach ($tableNode->bodyRows() as $row) {
2805
 		foreach ($tableNode->bodyRows() as $row) {
2802
-			$columnCount = max($columnCount, sizeof($row->children));
2806
+			$columnCount = max($columnCount, count($row->children));
2803
 		}
2807
 		}
2804
 
2808
 
2805
 		// Create and populate grid
2809
 		// Create and populate grid
2806
 		$grid = new SpreadsheetGrid($columnCount, $rowCount);
2810
 		$grid = new SpreadsheetGrid($columnCount, $rowCount);
2807
 		for ($c = 0; $c < $columnCount; $c++) {
2811
 		for ($c = 0; $c < $columnCount; $c++) {
2808
 			for ($r = 0; $r < $rowCount; $r++) {
2812
 			for ($r = 0; $r < $rowCount; $r++) {
2809
-				$cellNode = $tableNode->bodyRows()[$r]->children[$c];
2813
+				$row = $tableNode->bodyRows()[$r] ?? null;
2814
+				if ($row === null) continue;
2815
+				$cellNode = $row->children[$c] ?? null;
2810
 				if ($cellNode === null) continue;
2816
 				if ($cellNode === null) continue;
2811
 				$cellText = $cellNode->toPlaintext($state);
2817
 				$cellText = $cellNode->toPlaintext($state);
2812
 				$gridCell = $grid->cells[$c][$r];
2818
 				$gridCell = $grid->cells[$c][$r];
2833
 		// Copy results back to table
2839
 		// Copy results back to table
2834
 		for ($c = 0; $c < $columnCount; $c++) {
2840
 		for ($c = 0; $c < $columnCount; $c++) {
2835
 			for ($r = 0; $r < $rowCount; $r++) {
2841
 			for ($r = 0; $r < $rowCount; $r++) {
2836
-				$cellNode = $tableNode->bodyRows()[$r]->children[$c];
2842
+				$row = $tableNode->bodyRows()[$r] ?? null;
2843
+				if ($row === null) continue;
2844
+				$cellNode = $row->children[$c] ?? null;
2837
 				if ($cellNode === null) continue;
2845
 				if ($cellNode === null) continue;
2838
 				$gridCell = $grid->cells[$c][$r];
2846
 				$gridCell = $grid->cells[$c][$r];
2839
 				$gridValue = $gridCell->outputValue;
2847
 				$gridValue = $gridCell->outputValue;
2843
 					$cellNode->addClass('calculated');
2851
 					$cellNode->addClass('calculated');
2844
 				}
2852
 				}
2845
 				$cellNode->addClass("spreadsheet-type-{$gridValue->type}");
2853
 				$cellNode->addClass("spreadsheet-type-{$gridValue->type}");
2846
-				if ($gridValue->type == CellValue::TYPE_ERROR) {
2854
+				if ($gridValue->type === CellValue::TYPE_ERROR) {
2847
 					$cellNode->attributes['title'] = $gridValue->value;
2855
 					$cellNode->attributes['title'] = $gridValue->value;
2848
 				}
2856
 				}
2849
 				$gridNumber = $gridValue->numericValue();
2857
 				$gridNumber = $gridValue->numericValue();

+ 6
- 5
playground.html Zobrazit soubor

403
 
403
 
404
 ### Modified Heading {style=color:red;}
404
 ### Modified Heading {style=color:red;}
405
 
405
 
406
-| Unit Price | Qty | Subtotal |
407
-| ---: | ---: | ---: |
408
-| $1.23 | 2 | =A*B FILL |
409
-| $4.99 | 6 | |
410
-| Total | | =SUM(C:C) |
406
+| Unit Price | Qty | Discount | Subtotal |
407
+| ---: | ---: | ---: | ---: |
408
+| $1.23 | 2 | 10% | =A\\*B*(1-C) FILL |
409
+| $4.99 | 6 | | |
410
+| $0.99 | 1 | 25% | |
411
+| Total | | | =SUM(D:D) |
411
 
412
 
412
 ---
413
 ---
413
 
414
 

Načítá se…
Zrušit
Uložit