Selaa lähdekoodia

Unit tests and fixes for all spreadsheet functions

main
Rocketsoup 1 vuosi sitten
vanhempi
commit
519c76e9f0
2 muutettua tiedostoa jossa 396 lisäystä ja 86 poistoa
  1. 126
    85
      js/spreadsheet.js
  2. 270
    1
      testjs.html

+ 126
- 85
js/spreadsheet.js Näytä tiedosto

78
 
78
 
79
 class CellExpressionOperation {
79
 class CellExpressionOperation {
80
 	/** Arg is int or float */
80
 	/** Arg is int or float */
81
-	static Number = new this('Number');
81
+	static Number = new CellExpressionOperation('Number');
82
 	/** Arg is string without quotes */
82
 	/** Arg is string without quotes */
83
-	static String = new this('String');
83
+	static String = new CellExpressionOperation('String');
84
 	/** Arg is bool */
84
 	/** Arg is bool */
85
-	static Boolean = new this('Boolean');
85
+	static Boolean = new CellExpressionOperation('Boolean');
86
 	/** Arg is reference address (e.g. "A5") */
86
 	/** Arg is reference address (e.g. "A5") */
87
-	static Reference = new this('Reference');
87
+	static Reference = new CellExpressionOperation('Reference');
88
 	/** Args are start and end addresses (e.g. "A5", "C7") */
88
 	/** Args are start and end addresses (e.g. "A5", "C7") */
89
-	static Range = new this('Range');
89
+	static Range = new CellExpressionOperation('Range');
90
 
90
 
91
 	/** Args are two operand CellExpressions. */
91
 	/** Args are two operand CellExpressions. */
92
-	static Add = new this('Add');
92
+	static Add = new CellExpressionOperation('Add');
93
 	/** Args are two operand CellExpressions */
93
 	/** Args are two operand CellExpressions */
94
-	static Subtract = new this('Subtract');
94
+	static Subtract = new CellExpressionOperation('Subtract');
95
 	/** Args are two operand CellExpressions */
95
 	/** Args are two operand CellExpressions */
96
-	static Multiply = new this('Multiply');
96
+	static Multiply = new CellExpressionOperation('Multiply');
97
 	/** Args are two operand CellExpressions */
97
 	/** Args are two operand CellExpressions */
98
-	static Divide = new this('Divide');
98
+	static Divide = new CellExpressionOperation('Divide');
99
 
99
 
100
 	/** Args are two operand CellExpressions. */
100
 	/** Args are two operand CellExpressions. */
101
-	static Concatenate = new this('Concatenate');
101
+	static Concatenate = new CellExpressionOperation('Concatenate');
102
 
102
 
103
 	/** Arg is operand expression */
103
 	/** Arg is operand expression */
104
-	static UnaryMinus = new this('UnaryMinus');
104
+	static UnaryMinus = new CellExpressionOperation('UnaryMinus');
105
 
105
 
106
 	/** Args are two operand CellExpressions. */
106
 	/** Args are two operand CellExpressions. */
107
-	static GreaterThan = new this('GreaterThan');
107
+	static GreaterThan = new CellExpressionOperation('GreaterThan');
108
 	/** Args are two operand CellExpressions. */
108
 	/** Args are two operand CellExpressions. */
109
-	static GreaterThanEqual = new this('GreaterThanEqual');
109
+	static GreaterThanEqual = new CellExpressionOperation('GreaterThanEqual');
110
 	/** Args are two operand CellExpressions. */
110
 	/** Args are two operand CellExpressions. */
111
-	static LessThan = new this('LessThan');
111
+	static LessThan = new CellExpressionOperation('LessThan');
112
 	/** Args are two operand CellExpressions. */
112
 	/** Args are two operand CellExpressions. */
113
-	static LessThanEqual = new this('LessThanEqual');
113
+	static LessThanEqual = new CellExpressionOperation('LessThanEqual');
114
 	/** Args are two operand CellExpressions. */
114
 	/** Args are two operand CellExpressions. */
115
-	static Equal = new this('Equal');
115
+	static Equal = new CellExpressionOperation('Equal');
116
 	/** Args are two operand CellExpressions. */
116
 	/** Args are two operand CellExpressions. */
117
-	static Unequal = new this('Unequal');
117
+	static Unequal = new CellExpressionOperation('Unequal');
118
 
118
 
119
 	/** Arg is operand expression. */
119
 	/** Arg is operand expression. */
120
-	static UnaryNot = new this('UnaryNot');
120
+	static UnaryNot = new CellExpressionOperation('UnaryNot');
121
 
121
 
122
 	/** Args are 0+ CellExpressions */
122
 	/** Args are 0+ CellExpressions */
123
-	static Function = new this('Function');
123
+	static Function = new CellExpressionOperation('Function');
124
 
124
 
125
+	/** @type {string} */
125
 	name;
126
 	name;
126
 
127
 
127
 	constructor(name) {
128
 	constructor(name) {
129
 	}
130
 	}
130
 
131
 
131
 	toString() {
132
 	toString() {
132
-		return this.name;
133
+		return `${this.constructor.name}.${this.name}`;
133
 	}
134
 	}
134
 
135
 
135
 	equals(other) {
136
 	equals(other) {
211
 				cell.isCalculated = true;
212
 				cell.isCalculated = true;
212
 				if (result instanceof CellValue) {
213
 				if (result instanceof CellValue) {
213
 					cell.outputValue = result;
214
 					cell.outputValue = result;
214
-				} else if (Array.isArray(result) && result.length == 1) {
215
-					cell.outputValue = result[0];
215
+					requeueCount = 0;
216
+				} else if (Array.isArray(result)) {
217
+					if (result.length == 1) {
218
+						cell.outputValue = result[0];
219
+						requeueCount = 0;
220
+					} else {
221
+						throw new CellEvaluationException(`Expression resolved to ${result.length} values, single value expected`);
222
+					}
216
 				} else {
223
 				} else {
217
-					throw new CellEvaluationException("Expression did not resolve to a single value");
224
+					throw new CellEvaluationException(`Expression resolved to ${result && result.constructor ? result.constructor.name : typeof result}, expected CellValue`);
218
 				}
225
 				}
219
 			} catch (e) {
226
 			} catch (e) {
220
 				if (e instanceof CellDependencyException) {
227
 				if (e instanceof CellDependencyException) {
298
 					throw new CellEvaluationException(`No cell at ${refAddress.name}`, '#REF');
305
 					throw new CellEvaluationException(`No cell at ${refAddress.name}`, '#REF');
299
 				}
306
 				}
300
 				if (cell.outputValue === null) {
307
 				if (cell.outputValue === null) {
301
-					throw new CellDependencyException(`Need calculated value for ${refAddress.name} to evaluate`);
308
+					throw new CellDependencyException(`Need calculated value for ${refAddress} to evaluate`);
302
 				}
309
 				}
303
 				return cell.outputValue;
310
 				return cell.outputValue;
304
 			}
311
 			}
387
 			case CellExpressionOperation.Function:
394
 			case CellExpressionOperation.Function:
388
 				return this.#callFunction(expr.qualifier, expr.arguments, address);
395
 				return this.#callFunction(expr.qualifier, expr.arguments, address);
389
 		}
396
 		}
390
-		console.warn(`Unhandled operation ${expr.op.name}`);
397
+		throw new CellSyntaxException(`Unhandled operation ${expr.op.name}`);
391
 	}
398
 	}
392
 
399
 
393
 	/**
400
 	/**
618
 			throw new CellEvaluationException("IFS expects an odd number of arguments");
625
 			throw new CellEvaluationException("IFS expects an odd number of arguments");
619
 		}
626
 		}
620
 		const evaled = args.map((arg) => this.#evaluate(arg, address));
627
 		const evaled = args.map((arg) => this.#evaluate(arg, address));
621
-		for (var i = 0; i < evaled.length; i += 2) {
628
+		for (var i = 0; i < evaled.length - 1; i += 2) {
622
 			const test = evaled[i].booleanValue();
629
 			const test = evaled[i].booleanValue();
623
 			if (test === null) {
630
 			if (test === null) {
624
 				throw new CellEvaluationException(`IFS expects a boolean for argument ${i + 1}`);
631
 				throw new CellEvaluationException(`IFS expects a boolean for argument ${i + 1}`);
678
 	 * @returns {CellValue}
685
 	 * @returns {CellValue}
679
 	 */
686
 	 */
680
 	#funcMax(args, address) {
687
 	#funcMax(args, address) {
681
-		const maxValue = null;
688
+		var maxValue = null;
682
 		const flattened = this.#flattenedNumericArguments('MAX', args, address);
689
 		const flattened = this.#flattenedNumericArguments('MAX', args, address);
683
 		if (flattened.length == 0) {
690
 		if (flattened.length == 0) {
684
 			throw new CellEvaluationException("MAX requires at least one numeric argument");
691
 			throw new CellEvaluationException("MAX requires at least one numeric argument");
781
 	#funcRound(args, address) {
788
 	#funcRound(args, address) {
782
 		const evaled = this.#assertNumericArguments('ROUND', 1, 2, args, address);
789
 		const evaled = this.#assertNumericArguments('ROUND', 1, 2, args, address);
783
 		const val = evaled[0];
790
 		const val = evaled[0];
784
-		const places = sizeof(evaled) > 1 ? evaled[1].value : 0;
791
+		const places = evaled.length > 1 ? evaled[1].value : 0;
785
 		const divider = Math.pow(10.0, places);
792
 		const divider = Math.pow(10.0, places);
786
 		const newValue = Math.round(val.value * divider) / divider;
793
 		const newValue = Math.round(val.value * divider) / divider;
787
 		return CellValue.fromValue(newValue, val.type);
794
 		return CellValue.fromValue(newValue, val.type);
820
 		if (text === null || search === null || replace === null) {
827
 		if (text === null || search === null || replace === null) {
821
 			throw new CellEvaluationException("SUBSTITUTE expects 3 string arguments");
828
 			throw new CellEvaluationException("SUBSTITUTE expects 3 string arguments");
822
 		}
829
 		}
823
-		const result = text.replace(new RegExp(RegExp.escape(search), 'gi'), replace);
830
+		const result = text.replaceAll(search, replace);
824
 		return CellValue.fromValue(result);
831
 		return CellValue.fromValue(result);
825
 	}
832
 	}
826
 
833
 
879
 			}
886
 			}
880
 			result = (result === null) ? b : (result ^ b);
887
 			result = (result === null) ? b : (result ^ b);
881
 		}
888
 		}
882
-		return CellValue.fromValue(result);
889
+		return CellValue.fromValue(result != 0);
883
 	}
890
 	}
884
 }
891
 }
885
 
892
 
961
 		}
968
 		}
962
 	}
969
 	}
963
 
970
 
971
+	#clone() {
972
+		const cp = new CellExpression();
973
+		cp.op = this.op;
974
+		cp.arguments = this.arguments.slice();
975
+		cp.qualifier = this.qualifier;
976
+		cp.outputType = this.outputType;
977
+		cp.outputDecimals = this.outputDecimals;
978
+		cp.fillRanges = this.fillRanges !== null ? this.fillRanges.slice() : null;
979
+		cp.location = this.location;
980
+		return cp;
981
+	}
982
+
964
 	/**
983
 	/**
965
 	 * @param {CellAddress} start
984
 	 * @param {CellAddress} start
966
 	 * @param {CellAddress} end
985
 	 * @param {CellAddress} end
967
 	 * @returns {CellExpression|null}
986
 	 * @returns {CellExpression|null}
968
 	 */
987
 	 */
969
 	transpose(start, end) {
988
 	transpose(start, end) {
970
-		var transposed = structuredClone(this);
989
+		var transposed = this.#clone(); // structuredClone makes a mess of typing
971
 		transposed.arguments = [];
990
 		transposed.arguments = [];
972
 		for (const argument of this.arguments) {
991
 		for (const argument of this.arguments) {
973
 			if (argument instanceof CellExpression) {
992
 			if (argument instanceof CellExpression) {
992
 	 */
1011
 	 */
993
 	static expressionToTokens(text) {
1012
 	static expressionToTokens(text) {
994
 		var tokens = [];
1013
 		var tokens = [];
995
-		var pos = 0;
1014
+		var pos = [0];
996
 		this.#skipWhitespace(text, pos);
1015
 		this.#skipWhitespace(text, pos);
997
-		if (text.substring(pos, pos + 1) == '=') {
1016
+		if (text.substring(pos[0], pos[0] + 1) == '=') {
998
 			// Ignore equals
1017
 			// Ignore equals
999
-			pos++;
1018
+			pos[0]++;
1000
 		}
1019
 		}
1001
 		this.#skipWhitespace(text, pos);
1020
 		this.#skipWhitespace(text, pos);
1002
 		var l = text.length;
1021
 		var l = text.length;
1003
-		while (pos < l) {
1022
+		while (pos[0] < l) {
1004
 			tokens.push(this.#readNextToken(text, pos));
1023
 			tokens.push(this.#readNextToken(text, pos));
1005
 			this.#skipWhitespace(text, pos);
1024
 			this.#skipWhitespace(text, pos);
1006
 		}
1025
 		}
1208
 	 * @returns {string|null}
1227
 	 * @returns {string|null}
1209
 	 */
1228
 	 */
1210
 	static #readChars(text, pos, charTest, minimumLength = null, maximumLength = null) {
1229
 	static #readChars(text, pos, charTest, minimumLength = null, maximumLength = null) {
1211
-		var p = [ pos[0] ];
1230
+		var p = pos[0];
1212
 		const l = text.length;
1231
 		const l = text.length;
1213
 		var s = '';
1232
 		var s = '';
1214
 		var sl = 0;
1233
 		var sl = 0;
1215
-		while (p[0] < l && (maximumLength === null || sl < maximumLength)) {
1216
-			ch = text.substring(p[0], p[0] + 1);
1234
+		while (p < l && (maximumLength === null || sl < maximumLength)) {
1235
+			const ch = text.substring(p, p + 1);
1217
 			if (!charTest(ch)) break;
1236
 			if (!charTest(ch)) break;
1218
 			s += ch;
1237
 			s += ch;
1219
 			sl++;
1238
 			sl++;
1220
-			p[0]++;
1239
+			p++;
1240
+		}
1241
+		if (p < l && charTest(text.substring(p, p + 1))) {
1242
+			return null;
1221
 		}
1243
 		}
1222
 		if (minimumLength !== null && sl < minimumLength) {
1244
 		if (minimumLength !== null && sl < minimumLength) {
1223
 			return null;
1245
 			return null;
1224
 		}
1246
 		}
1225
-		pos[0] = p[0];
1247
+		pos[0] = p;
1226
 		return s;
1248
 		return s;
1227
 	}
1249
 	}
1228
 
1250
 
1509
 		if (start != end) return null;
1531
 		if (start != end) return null;
1510
 		if (!tokens[start].type.isPotentialAddress()) return null;
1532
 		if (!tokens[start].type.isPotentialAddress()) return null;
1511
 		const ref = tokens[start].content.toUpperCase();
1533
 		const ref = tokens[start].content.toUpperCase();
1512
-		const refAddress = new CellAddress(ref, address);
1534
+		const refAddress = CellAddress.fromString(ref, address, true);
1513
 		return new CellExpression(CellExpressionOperation.Reference, [ refAddress ]);
1535
 		return new CellExpression(CellExpressionOperation.Reference, [ refAddress ]);
1514
 	}
1536
 	}
1515
 
1537
 
1547
 				const op = tokens[i].type.name;
1569
 				const op = tokens[i].type.name;
1548
 				const priority = opPriorities[op] ?? false;
1570
 				const priority = opPriorities[op] ?? false;
1549
 				if (priority === false) continue;
1571
 				if (priority === false) continue;
1550
-				console.error(`Found infix candidate at ${i} for ${op} priority ${priority}`);
1572
+				//console.info(`Found infix candidate at ${i} for ${op} priority ${priority}`);
1551
 				candidates.push({ priority: priority, i: i });
1573
 				candidates.push({ priority: priority, i: i });
1552
 			}
1574
 			}
1553
 		}
1575
 		}
1554
-		candidates.sort((a, b) => {
1555
-			if (a.priority < b.priority) return 1;
1556
-			if (a.priority > b.priority) return -1;
1557
-			return 0;
1558
-		});
1576
+		candidates.sort((a, b) => a.priority - b.priority);
1559
 		var bestCandidate = null;
1577
 		var bestCandidate = null;
1578
+		var operand1, operand2;
1560
 		for (const candidate of candidates) {
1579
 		for (const candidate of candidates) {
1561
 			try {
1580
 			try {
1562
 				i = candidate.i;
1581
 				i = candidate.i;
1563
-				const operand1 = this.#tryExpression(tokens, start, i - 1, address);
1582
+				operand1 = this.#tryExpression(tokens, start, i - 1, address);
1564
 				if (operand1 === null) continue;
1583
 				if (operand1 === null) continue;
1565
-				const operand2 = this.#tryExpression(tokens, i + 1, end, address);
1584
+				operand2 = this.#tryExpression(tokens, i + 1, end, address);
1566
 				if (operand2 === null) continue;
1585
 				if (operand2 === null) continue;
1567
 				bestCandidate = candidate;
1586
 				bestCandidate = candidate;
1568
 				break;
1587
 				break;
1573
 			}
1592
 			}
1574
 		}
1593
 		}
1575
 		if (bestCandidate === null) {
1594
 		if (bestCandidate === null) {
1576
-			console.error("No best candidate found");
1595
+			//console.info("No best candidate found");
1577
 			return null;
1596
 			return null;
1578
 		}
1597
 		}
1579
 		i = bestCandidate.i;
1598
 		i = bestCandidate.i;
1580
-		console.error(`Best candidate at token ${i}, priority ${bestCandidate.priority}`);
1599
+		//console.info(`Best candidate at token ${i}, priority ${bestCandidate.priority}`);
1581
 		switch (tokens[bestCandidate.i].type) {
1600
 		switch (tokens[bestCandidate.i].type) {
1582
 			case CellExpressionTokenType.Plus:
1601
 			case CellExpressionTokenType.Plus:
1583
 				return new CellExpression(CellExpressionOperation.Add, [ operand1, operand2 ]);
1602
 				return new CellExpression(CellExpressionOperation.Add, [ operand1, operand2 ]);
1662
 	/**
1681
 	/**
1663
 	 * @type {string}
1682
 	 * @type {string}
1664
 	 */
1683
 	 */
1665
-	get name() { this.#name; }
1684
+	get name() { return this.#name; }
1666
 
1685
 
1667
 	#isColumnFixed = false;
1686
 	#isColumnFixed = false;
1668
 	/** 
1687
 	/** 
1679
 	 */
1698
 	 */
1680
 	get columnIndex() { return this.#columnIndex; };
1699
 	get columnIndex() { return this.#columnIndex; };
1681
 
1700
 
1701
+	/**
1702
+	 * Letter code for the column.
1703
+	 * @type {string}
1704
+	 */
1705
+	get columnLetter() { return CellAddress.#columnIndexToLetters(this.#columnIndex); }
1706
+
1682
 	#isRowFixed = false;
1707
 	#isRowFixed = false;
1683
 	/**
1708
 	/**
1684
 	 * Whether the row should remain unchanged when transposed. This is
1709
 	 * Whether the row should remain unchanged when transposed. This is
1695
 	get rowIndex() { return this.#rowIndex; }
1720
 	get rowIndex() { return this.#rowIndex; }
1696
 
1721
 
1697
 	/**
1722
 	/**
1723
+	 * One-based row number. This is the human-facing row number.
1724
+	 */
1725
+	get rowNumber() { return this.#rowIndex + 1; }
1726
+
1727
+	/**
1698
 	 * Whether this address has both a definite column and row.
1728
 	 * Whether this address has both a definite column and row.
1699
 	 * @type {boolean}
1729
 	 * @type {boolean}
1700
 	 */
1730
 	 */
1709
 	 *   during transpositions. Denoted with a `$` in front of the row digits.
1739
 	 *   during transpositions. Denoted with a `$` in front of the row digits.
1710
 	 */
1740
 	 */
1711
 	constructor(columnIndex, rowIndex, isColumnFixed=false, isRowFixed=false) {
1741
 	constructor(columnIndex, rowIndex, isColumnFixed=false, isRowFixed=false) {
1742
+		if (typeof columnIndex != 'number') {
1743
+			throw new Error(`columnIndex must be number, got ${typeof columnIndex}`);
1744
+		}
1745
+		if (typeof rowIndex != 'number') {
1746
+			throw new Error(`rowIndex must be number, got ${typeof rowIndex}`);
1747
+		}
1712
 		this.#columnIndex = columnIndex;
1748
 		this.#columnIndex = columnIndex;
1713
 		this.#rowIndex = rowIndex;
1749
 		this.#rowIndex = rowIndex;
1714
 		this.#isColumnFixed = isColumnFixed;
1750
 		this.#isColumnFixed = isColumnFixed;
1715
 		this.#isRowFixed = isRowFixed;
1751
 		this.#isRowFixed = isRowFixed;
1716
-		this.#name = (isColumnFixed && columnIndex >= 0 ? '$' : '') +
1717
-			CellAddress.#columnIndexToLetters(columnIndex) +
1718
-			(isRowFixed && rowIndex >= 0 ? '$' : '') +
1719
-			(rowIndex >= 0) ? `${rowIndex + 1}` : '';
1752
+		this.#name = CellAddress.#formatAddress(columnIndex, rowIndex, isColumnFixed, isRowFixed);
1720
 	}
1753
 	}
1721
 
1754
 
1722
 	/**
1755
 	/**
1725
 	 * @returns {boolean}
1758
 	 * @returns {boolean}
1726
 	 */
1759
 	 */
1727
 	static isAddress(text) {
1760
 	static isAddress(text) {
1728
-		return this.fromAddress(text) != null;
1761
+		return this.fromString(text) != null;
1729
 	}
1762
 	}
1730
 
1763
 
1731
 	/**
1764
 	/**
1753
 		if (!relativeFrom.isResolved || !relativeTo.isResolved) {
1786
 		if (!relativeFrom.isResolved || !relativeTo.isResolved) {
1754
 			throw new CellEvaluationException("Can only transpose to and from resolved addresses");
1787
 			throw new CellEvaluationException("Can only transpose to and from resolved addresses");
1755
 		}
1788
 		}
1756
-		newColumnIndex = this.columnIndex;
1789
+		var newColumnIndex = this.columnIndex;
1757
 		if (!this.isColumnFixed) {
1790
 		if (!this.isColumnFixed) {
1758
-			columnDelta = relativeTo.columnIndex - relativeFrom.columnIndex;
1791
+			const columnDelta = relativeTo.columnIndex - relativeFrom.columnIndex;
1759
 			newColumnIndex += columnDelta;
1792
 			newColumnIndex += columnDelta;
1760
 		}
1793
 		}
1761
-		newRowIndex = this.rowIndex;
1794
+		var newRowIndex = this.rowIndex;
1762
 		if (!this.isResolved && resolveToRow) {
1795
 		if (!this.isResolved && resolveToRow) {
1763
 			newRowIndex = relativeFrom.rowIndex;
1796
 			newRowIndex = relativeFrom.rowIndex;
1764
 		}
1797
 		}
1765
 		if (newRowIndex != -1 && !this.isRowAbsolute) {
1798
 		if (newRowIndex != -1 && !this.isRowAbsolute) {
1766
-			rowDelta = relativeTo.rowIndex - relativeFrom.rowIndex;
1799
+			const rowDelta = relativeTo.rowIndex - relativeFrom.rowIndex;
1767
 			newRowIndex += rowDelta;
1800
 			newRowIndex += rowDelta;
1768
 		}
1801
 		}
1769
 		if (newColumnIndex < 0 || newRowIndex < 0) return null;
1802
 		if (newColumnIndex < 0 || newRowIndex < 0) return null;
1788
 	 * Converts column letters (e.g. `A`, `C`, `AA`) to a 0-based column index.
1821
 	 * Converts column letters (e.g. `A`, `C`, `AA`) to a 0-based column index.
1789
 	 * Assumes a validated well-formed column letter or else behavior is undefined.
1822
 	 * Assumes a validated well-formed column letter or else behavior is undefined.
1790
 	 *
1823
 	 *
1791
-	 * @param {string} letter
1824
+	 * @param {string} letters
1792
 	 * @returns {number} column index
1825
 	 * @returns {number} column index
1793
 	 */
1826
 	 */
1794
-	static #lettersToColumnIndex(letter) {
1827
+	static #lettersToColumnIndex(letters) {
1795
 		const ACodepoint = 'A'.codePointAt(0);
1828
 		const ACodepoint = 'A'.codePointAt(0);
1796
 		var columnIndex = 0;
1829
 		var columnIndex = 0;
1797
 		for (var i = letters.length - 1; i >= 0; i--) {
1830
 		for (var i = letters.length - 1; i >= 0; i--) {
1820
 		return letters;
1853
 		return letters;
1821
 	}
1854
 	}
1822
 
1855
 
1856
+	static #formatAddress(columnIndex, rowIndex, isColumnFixed, isRowFixed) {
1857
+		var addr = '';
1858
+		if (isColumnFixed && columnIndex >= 0) addr += '$';
1859
+		if (columnIndex >= 0) addr += this.#columnIndexToLetters(columnIndex);
1860
+		if (isRowFixed && rowIndex >= 0) addr += '$';
1861
+		if (rowIndex >= 0) addr += `${rowIndex + 1}`;
1862
+		return addr;
1863
+	}
1864
+
1823
 	/**
1865
 	/**
1824
 	 * @param {string} address - cell address string
1866
 	 * @param {string} address - cell address string
1825
 	 * @param {CellAddress|null} relativeTo - address to resolve relative addresses against
1867
 	 * @param {CellAddress|null} relativeTo - address to resolve relative addresses against
1885
 	 * @returns {object} iterable object
1927
 	 * @returns {object} iterable object
1886
 	 */
1928
 	 */
1887
 	cellsIn(table) {
1929
 	cellsIn(table) {
1888
-		const minCol = Math.max(1, this.minColumnIndex);
1889
-		const maxCol = Math.min(this.maxColumnIndex, table.columnCount - 1);
1890
-		const minRow = Math.max(1, this.minRowIndex);
1891
-		const maxRow = Math.min(this.maxRowIndex, table.rowCount - 1);
1930
+		const minCol = this.minColumnIndex < 0 ? 0 : this.minColumnIndex;
1931
+		const maxCol = this.maxColumnIndex < 0 ? table.columnCount - 1 : Math.min(this.maxColumnIndex, table.columnCount - 1);
1932
+		const minRow = this.minRowIndex < 0 ? 0 : this.minRowIndex;
1933
+		const maxRow = this.maxRowIndex < 0 ? table.rowCount - 1 : Math.min(this.maxRowIndex, table.rowCount - 1);
1892
 		const iterable = {};
1934
 		const iterable = {};
1893
 		iterable[Symbol.iterator] = function() {
1935
 		iterable[Symbol.iterator] = function() {
1894
 			var currentCol = minCol;
1936
 			var currentCol = minCol;
1950
 		// -- Properties -----
1992
 		// -- Properties -----
1951
 
1993
 
1952
 	/**
1994
 	/**
1995
+	 * A blank value.
1996
+	 * @type {CellValue}
1997
+	 */
1998
+	static BLANK = new CellValue('', null, CellValue.TYPE_BLANK);
1999
+
2000
+	/**
1953
 	 * Type of value. One of the `TYPE_` constants.
2001
 	 * Type of value. One of the `TYPE_` constants.
1954
 	 * @type {string}
2002
 	 * @type {string}
1955
 	 */
2003
 	 */
2029
 		}
2077
 		}
2030
 		if (value instanceof Error) {
2078
 		if (value instanceof Error) {
2031
 			if (value instanceof CellException) {
2079
 			if (value instanceof CellException) {
2032
-				return new CellValue(value.getErrorSymbol(), value.getMessage(), CellValue.TYPE_ERROR);
2080
+				return new CellValue(value.errorSymbol, value.message, CellValue.TYPE_ERROR);
2033
 			}
2081
 			}
2034
-			return new CellValue('#ERROR', value.getMessage(), CellValue.TYPE_ERROR);
2082
+			return new CellValue('#ERROR', value.message, CellValue.TYPE_ERROR);
2035
 		}
2083
 		}
2036
 		if (typeof value == 'boolean') {
2084
 		if (typeof value == 'boolean') {
2037
 			const formatted = CellValue.formatType(value, CellValue.TYPE_BOOLEAN, 0);
2085
 			const formatted = CellValue.formatType(value, CellValue.TYPE_BOOLEAN, 0);
2529
 	 * @returns {string}
2577
 	 * @returns {string}
2530
 	 */
2578
 	 */
2531
 	static #formatNumber(value, decimals) {
2579
 	static #formatNumber(value, decimals) {
2532
-		return (value).toLocaleString(undefined, { minimumFractionDigits: decimals });
2580
+		return (value).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
2533
 	}
2581
 	}
2534
 
2582
 
2535
 	/**
2583
 	/**
2538
 	 * @returns {string}
2586
 	 * @returns {string}
2539
 	 */
2587
 	 */
2540
 	static #formatCurrency(dollars, decimals) {
2588
 	static #formatCurrency(dollars, decimals) {
2541
-		var s = (dollars).toLocaleString(undefined, { minimumFractionDigits: decimals });
2589
+		var s = (dollars).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
2542
 		if (s.startsWith('-')) {
2590
 		if (s.startsWith('-')) {
2543
 			return '-$' + s.substring(1);
2591
 			return '-$' + s.substring(1);
2544
 		}
2592
 		}
2552
 	 */
2600
 	 */
2553
 	static #formatPercent(value, decimals) {
2601
 	static #formatPercent(value, decimals) {
2554
 		const dec = value * 100.0;
2602
 		const dec = value * 100.0;
2555
-		return (dec).toLocaleString(undefined, { minimumFractionDigits: decimals }) + '%';
2603
+		return (dec).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + '%';
2556
 	}
2604
 	}
2557
 
2605
 
2558
 	/**
2606
 	/**
2566
 			return CellValue.#autodecimals(value.value);
2614
 			return CellValue.#autodecimals(value.value);
2567
 		}
2615
 		}
2568
 		if (typeof value == 'number') {
2616
 		if (typeof value == 'number') {
2569
-			var s = `${Math.abs(value)}`;
2617
+			var s = (value).toLocaleString(undefined, { maximumFractionDigits: maxDigits });
2570
 			if (/\./.exec(s) === null) return 0;
2618
 			if (/\./.exec(s) === null) return 0;
2571
-			if (s.endsWith('.0')) return 0;
2572
-			const parts = s.split('.');
2573
-			const whole = parts[0];
2574
-			var fraction = parts[1];
2575
-			if (fraction.endsWith('99')) {
2576
-				// More than one 9 at the end points to floating point rounding. Lop em off.
2577
-				fraction = fraction.replace(/[9]+$/, '');
2578
-			}
2619
+			var fraction = s.split('.')[1];
2579
 			return Math.min(maxDigits, fraction.length);
2620
 			return Math.min(maxDigits, fraction.length);
2580
 		}
2621
 		}
2581
 		return 0;
2622
 		return 0;
2683
 	/**
2724
 	/**
2684
 	 * @type {MDTableCellBlock|null}
2725
 	 * @type {MDTableCellBlock|null}
2685
 	 */
2726
 	 */
2686
-	block;
2727
+	block = null;
2687
 
2728
 
2688
 	/**
2729
 	/**
2689
-	 * @type {CellValue|null}
2730
+	 * @type {CellValue}
2690
 	 */
2731
 	 */
2691
-	originalValue;
2732
+	originalValue = CellValue.BLANK;
2692
 
2733
 
2693
 	/**
2734
 	/**
2694
 	 * @type {CellValue|null}
2735
 	 * @type {CellValue|null}
2699
 	isCalculated = false;
2740
 	isCalculated = false;
2700
 
2741
 
2701
 	/** @type {CellExpression|null} */
2742
 	/** @type {CellExpression|null} */
2702
-	parsedExpression;
2743
+	parsedExpression = null;
2703
 
2744
 
2704
 	/**
2745
 	/**
2705
 	 * @type {CellValue|null}
2746
 	 * @type {CellValue|null}

+ 270
- 1
testjs.html Näytä tiedosto

123
 				assertFalse(test, failMessage=null) {
123
 				assertFalse(test, failMessage=null) {
124
 					if (test) this.fail(failMessage || `expected false, got ${test}`);
124
 					if (test) this.fail(failMessage || `expected false, got ${test}`);
125
 				}
125
 				}
126
-				assertEqual(a, b, floatDifferenceRatio=0.0, failMessage=null) {
126
+				assertEqual(a, b, floatDifferenceRatio=0.000001, failMessage=null) {
127
 					if (MDUtils.equal(a, b, floatDifferenceRatio)) return;
127
 					if (MDUtils.equal(a, b, floatDifferenceRatio)) return;
128
 					const aVal = (typeof a == 'string') ? `"${a}"` : `${a}`;
128
 					const aVal = (typeof a == 'string') ? `"${a}"` : `${a}`;
129
 					const bVal = (typeof b == 'string') ? `"${b}"` : `${b}`;
129
 					const bVal = (typeof b == 'string') ? `"${b}"` : `${b}`;
430
 					// InlineTests,
430
 					// InlineTests,
431
 					// BlockTests,
431
 					// BlockTests,
432
 					CellValueTests,
432
 					CellValueTests,
433
+					CellAddressRangeTests,
434
+					ExpressionSetTests,
433
 				];
435
 				];
434
 				TestClassRunner.runAll(testClasses);
436
 				TestClassRunner.runAll(testClasses);
435
 			}
437
 			}
1366
 					this.assertEqual(result, expected);
1368
 					this.assertEqual(result, expected);
1367
 				}
1369
 				}
1368
 
1370
 
1371
+				test_operation_unaryNot() {
1372
+					this.assertEqual(CellValue.fromValue(true).not(), CellValue.fromValue(false));
1373
+					this.assertEqual(CellValue.fromValue(false).not(), CellValue.fromValue(true));
1374
+				}
1375
+
1369
 				test_operation_comparators() {
1376
 				test_operation_comparators() {
1370
 					const a = CellValue.fromValue(3);
1377
 					const a = CellValue.fromValue(3);
1371
 					const b = CellValue.fromValue(4);
1378
 					const b = CellValue.fromValue(4);
1402
 					this.assertEqual(result, expected);
1409
 					this.assertEqual(result, expected);
1403
 				}
1410
 				}
1404
 			}
1411
 			}
1412
+
1413
+			class CellAddressRangeTests extends BaseTest {
1414
+				test_iterator() {
1415
+					const grid = new SpreadsheetGrid(3, 4);
1416
+					let range = new CellAddressRange(new CellAddress(0, 0), new CellAddress(2, 3));
1417
+					var visited = [];
1418
+					var sanity = 100;
1419
+					for (const address of range.cellsIn(grid)) {
1420
+						visited.push(address.name);
1421
+						if (sanity-- < 0) break;
1422
+					}
1423
+					const result = visited.join(',');
1424
+					const expected = 'A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4';
1425
+					this.assertEqual(result, expected);
1426
+				}
1427
+
1428
+				test_iterator_column() {
1429
+					const grid = new SpreadsheetGrid(3, 4);
1430
+					let range = new CellAddressRange(new CellAddress(1, -1), new CellAddress(2, -1));
1431
+					var visited = [];
1432
+					var sanity = 100;
1433
+					for (const address of range.cellsIn(grid)) {
1434
+						visited.push(address.name);
1435
+						if (sanity-- < 0) break;
1436
+					}
1437
+					const result = visited.join(',');
1438
+					const expected = 'B1,B2,B3,B4,C1,C2,C3,C4';
1439
+					this.assertEqual(result, expected);
1440
+				}
1441
+
1442
+				test_iterator_beyondBounds() {
1443
+					const grid = new SpreadsheetGrid(3, 4);
1444
+					let range = new CellAddressRange(new CellAddress(0, 1), new CellAddress(9, 9));
1445
+					var visited = [];
1446
+					var sanity = 100;
1447
+					for (const address of range.cellsIn(grid)) {
1448
+						visited.push(address.name);
1449
+						if (sanity-- < 0) break;
1450
+					}
1451
+					const result = visited.join(',');
1452
+					const expected = 'A2,A3,A4,B2,B3,B4,C2,C3,C4';
1453
+					this.assertEqual(result, expected);
1454
+				}
1455
+
1456
+				test_iterator_outOfBounds() {
1457
+					const grid = new SpreadsheetGrid(3, 4);
1458
+					let range = new CellAddressRange(new CellAddress(5, 0), new CellAddress(5, 9));
1459
+					var visited = [];
1460
+					var sanity = 100;
1461
+					for (const address of range.cellsIn(grid)) {
1462
+						visited.push(address.name);
1463
+						if (sanity-- < 0) break;
1464
+					}
1465
+					const result = visited.join(',');
1466
+					const expected = '';
1467
+					this.assertEqual(result, expected);
1468
+				}
1469
+			}
1470
+
1471
+			class ExpressionSetTests extends BaseTest {
1472
+				test_simpleMath() {
1473
+					const grid = new SpreadsheetGrid(1, 1);
1474
+					grid.cells[0][0].originalValue = CellValue.fromCellString('=7*3');
1475
+					const expressionSet = new CellExpressionSet(grid);
1476
+					expressionSet.calculateCells();
1477
+					const expected = CellValue.fromValue(21);
1478
+					this.assertEqual(grid.cells[0][0].outputValue, expected);
1479
+				}
1480
+
1481
+				test_reference() {
1482
+					const grid = new SpreadsheetGrid(3, 1);
1483
+					grid.cells[0][0].originalValue = CellValue.fromValue(123);
1484
+					grid.cells[1][0].originalValue = CellValue.fromValue(3);
1485
+					grid.cells[2][0].originalValue = CellValue.fromCellString('=A1*B1');
1486
+					const expressionSet = new CellExpressionSet(grid);
1487
+					expressionSet.calculateCells();
1488
+					const expected = CellValue.fromValue(369);
1489
+					this.assertEqual(grid.cells[2][0].outputValue, expected);
1490
+				}
1491
+
1492
+				test_infixPriority() {
1493
+					const grid = new SpreadsheetGrid(1, 1);
1494
+					grid.cells[0][0].originalValue = CellValue.fromCellString('=4*9/3+7-4');
1495
+					const expressionSet = new CellExpressionSet(grid);
1496
+					expressionSet.calculateCells();
1497
+					const expected = CellValue.fromValue(15);
1498
+					this.assertEqual(grid.cells[0][0].outputValue, expected);
1499
+				}
1500
+
1501
+				test_filledFormula() {
1502
+					const grid = new SpreadsheetGrid(3, 3);
1503
+					grid.cells[0][0].originalValue = CellValue.fromValue(4);
1504
+					grid.cells[1][0].originalValue = CellValue.fromValue(5);
1505
+					grid.cells[2][0].originalValue = CellValue.fromCellString('=A1*B1 FILL');
1506
+					grid.cells[0][1].originalValue = CellValue.fromValue(6);
1507
+					grid.cells[1][1].originalValue = CellValue.fromValue(7);
1508
+					grid.cells[0][2].originalValue = CellValue.fromValue(8);
1509
+					grid.cells[1][2].originalValue = CellValue.fromValue(9);
1510
+					const expressionSet = new CellExpressionSet(grid);
1511
+					expressionSet.calculateCells();
1512
+					this.assertEqual(grid.cells[2][0].outputValue, CellValue.fromValue(20));
1513
+					this.assertEqual(grid.cells[2][1].outputValue, CellValue.fromValue(42));
1514
+					this.assertEqual(grid.cells[2][2].outputValue, CellValue.fromValue(72));
1515
+				}
1516
+
1517
+				test_dependencies() {
1518
+					const grid = new SpreadsheetGrid(2, 4);
1519
+					grid.cells[0][0].originalValue = CellValue.fromValue(1);
1520
+					grid.cells[1][0].originalValue = CellValue.fromCellString('=A1+B2');
1521
+					grid.cells[0][1].originalValue = CellValue.fromValue(2);
1522
+					grid.cells[1][1].originalValue = CellValue.fromCellString('=A2+B3');
1523
+					grid.cells[0][2].originalValue = CellValue.fromValue(3);
1524
+					grid.cells[1][2].originalValue = CellValue.fromCellString('=A3+B4');
1525
+					grid.cells[0][3].originalValue = CellValue.fromValue(4);
1526
+					grid.cells[1][3].originalValue = CellValue.fromCellString('=A4');
1527
+					const expressionSet = new CellExpressionSet(grid);
1528
+					expressionSet.calculateCells();
1529
+					this.assertEqual(grid.cells[1][0].outputValue, CellValue.fromValue(10));
1530
+					this.assertEqual(grid.cells[1][1].outputValue, CellValue.fromValue(9));
1531
+					this.assertEqual(grid.cells[1][2].outputValue, CellValue.fromValue(7));
1532
+					this.assertEqual(grid.cells[1][3].outputValue, CellValue.fromValue(4));
1533
+				}
1534
+
1535
+				_test_simple_formula(formula, expected) {
1536
+					const grid = new SpreadsheetGrid(1, 1);
1537
+					grid.cells[0][0].originalValue = CellValue.fromCellString(formula);
1538
+					const expressionSet = new CellExpressionSet(grid);
1539
+					expressionSet.calculateCells();
1540
+					const result = grid.cells[0][0].outputValue;
1541
+					const exp = (expected instanceof CellValue) ? expected : CellValue.fromValue(expected);
1542
+					this.assertEqual(result.type, exp.type);
1543
+					this.assertEqual(result.decimals, exp.decimals);
1544
+					this.assertEqual(result.formattedValue, exp.formattedValue);
1545
+					this.assertEqual(result.value, exp.value);
1546
+				}
1547
+
1548
+				test_func_abs() {
1549
+					this._test_simple_formula('=ABS(-3)', 3);
1550
+					this._test_simple_formula('=ABS(4)', 4);
1551
+				}
1552
+
1553
+				test_func_and() {
1554
+					this._test_simple_formula('=AND(FALSE, FALSE)', false);
1555
+					this._test_simple_formula('=AND(FALSE, TRUE)', false);
1556
+					this._test_simple_formula('=AND(TRUE, FALSE)', false);
1557
+					this._test_simple_formula('=AND(TRUE, TRUE)', true);
1558
+				}
1559
+
1560
+				test_func_average() {
1561
+					this._test_simple_formula('=AVERAGE(4, 6, 2, 4)', 4);
1562
+				}
1563
+
1564
+				test_func_ceiling() {
1565
+					this._test_simple_formula('=CEILING(3.1)', 4);
1566
+					this._test_simple_formula('=CEILING(3)', 3);
1567
+					this._test_simple_formula('=CEILING(-3.1)', -3);
1568
+				}
1569
+
1570
+				test_func_exp() {
1571
+					this._test_simple_formula('=EXP(1)', 2.718281828459045);
1572
+				}
1573
+
1574
+				test_func_floor() {
1575
+					this._test_simple_formula('=FLOOR(3.1)', 3);
1576
+					this._test_simple_formula('=FLOOR(3)', 3);
1577
+					this._test_simple_formula('=FLOOR(-3.1)', -4);
1578
+				}
1579
+
1580
+				test_func_if() {
1581
+					this._test_simple_formula('=IF(FALSE, 4, 6)', 6);
1582
+					this._test_simple_formula('=IF(TRUE, 4, 6)', 4);
1583
+				}
1584
+
1585
+				test_func_ifs() {
1586
+					this._test_simple_formula('=IFS(TRUE, 1, FALSE, 2, FALSE, 3, FALSE, 4, 5)', 1);
1587
+					this._test_simple_formula('=IFS(FALSE, 1, TRUE, 2, FALSE, 3, FALSE, 4, 5)', 2);
1588
+					this._test_simple_formula('=IFS(FALSE, 1, FALSE, 2, TRUE, 3, FALSE, 4, 5)', 3);
1589
+					this._test_simple_formula('=IFS(FALSE, 1, FALSE, 2, FALSE, 3, TRUE, 4, 5)', 4);
1590
+					this._test_simple_formula('=IFS(FALSE, 1, FALSE, 2, FALSE, 3, FALSE, 4, 5)', 5);
1591
+				}
1592
+
1593
+				test_func_ln() {
1594
+					this._test_simple_formula('=LN(2.718281828459045)', 1);
1595
+				}
1596
+
1597
+				test_func_log() {
1598
+					this._test_simple_formula('=LOG(1000, 10)', 3);
1599
+				}
1600
+
1601
+				test_func_lower() {
1602
+					this._test_simple_formula('=LOWER("MiXeD")', 'mixed');
1603
+				}
1604
+
1605
+				test_func_max() {
1606
+					this._test_simple_formula('=MAX(4, 8, 5, 2)', 8);
1607
+				}
1608
+
1609
+				test_func_min() {
1610
+					this._test_simple_formula('=MIN(4, 8, 5, 2)', 2);
1611
+				}
1612
+
1613
+				test_func_mod() {
1614
+					this._test_simple_formula('=MOD(37, 4)', 1);
1615
+				}
1616
+
1617
+				test_func_not() {
1618
+					this._test_simple_formula('=NOT(TRUE)', false);
1619
+					this._test_simple_formula('=NOT(FALSE)', true);
1620
+				}
1621
+
1622
+				test_func_or() {
1623
+					this._test_simple_formula('=OR(FALSE, FALSE)', false);
1624
+					this._test_simple_formula('=OR(FALSE, TRUE)', true);
1625
+					this._test_simple_formula('=OR(TRUE, FALSE)', true);
1626
+					this._test_simple_formula('=OR(TRUE, TRUE)', true);
1627
+					this._test_simple_formula('=OR(FALSE, FALSE, FALSE, TRUE)', true);
1628
+				}
1629
+
1630
+				test_func_power() {
1631
+					this._test_simple_formula('=POWER(2, 3)', 8);
1632
+				}
1633
+
1634
+				test_func_round() {
1635
+					this._test_simple_formula('=ROUND(3.1)', 3);
1636
+					this._test_simple_formula('=ROUND(3.5)', 4);
1637
+					this._test_simple_formula('=ROUND(4)', 4);
1638
+					this._test_simple_formula('=ROUND(-3.1)', -3);
1639
+					this._test_simple_formula('=ROUND(-3.5)', -3);
1640
+					this._test_simple_formula('=ROUND(-3.9)', -4);
1641
+				}
1642
+
1643
+				test_func_sqrt() {
1644
+					this._test_simple_formula('=SQRT(16)', 4);
1645
+				}
1646
+
1647
+				test_func_substitute() {
1648
+					this._test_simple_formula('=SUBSTITUTE("cat sat on the mat", "at", "ot")', 'cot sot on the mot');
1649
+				}
1650
+
1651
+				test_func_sum() {
1652
+					this._test_simple_formula('=SUM(1, 2, 3, 4, 5)', 15);
1653
+				}
1654
+
1655
+				test_func_upper() {
1656
+					this._test_simple_formula('=UPPER("mIxEd")', 'MIXED');
1657
+				}
1658
+
1659
+				test_func_xor() {
1660
+					this._test_simple_formula('=XOR(FALSE, FALSE)', false);
1661
+					this._test_simple_formula('=XOR(FALSE, TRUE)', true);
1662
+					this._test_simple_formula('=XOR(TRUE, FALSE)', true);
1663
+					this._test_simple_formula('=XOR(TRUE, TRUE)', false);
1664
+					this._test_simple_formula('=XOR(FALSE, FALSE, TRUE)', true);
1665
+					this._test_simple_formula('=XOR(TRUE, FALSE, TRUE)', false);
1666
+				}
1667
+
1668
+				test_format() {
1669
+					this._test_simple_formula('=2.718281828459045 ; number 3', new CellValue('2.718', 2.718281828459045, 'number', 3));
1670
+					this._test_simple_formula('=2.718281828459045 ; percent 2', new CellValue('271.83%', 2.718281828459045, 'percent', 2));
1671
+					this._test_simple_formula('=2.718281828459045 ; currency 2', new CellValue('$2.72', 2.718281828459045, 'currency', 2));
1672
+				}
1673
+			}
1405
 		</script>
1674
 		</script>
1406
 	</head>
1675
 	</head>
1407
 	<body>
1676
 	<body>

Loading…
Peruuta
Tallenna