Pārlūkot izejas kodu

Unit tests and fixes for all spreadsheet functions

main
Rocketsoup 1 gadu atpakaļ
vecāks
revīzija
519c76e9f0
2 mainītis faili ar 396 papildinājumiem un 86 dzēšanām
  1. 126
    85
      js/spreadsheet.js
  2. 270
    1
      testjs.html

+ 126
- 85
js/spreadsheet.js Parādīt failu

@@ -78,50 +78,51 @@ class CellExpressionTokenType {
78 78
 
79 79
 class CellExpressionOperation {
80 80
 	/** Arg is int or float */
81
-	static Number = new this('Number');
81
+	static Number = new CellExpressionOperation('Number');
82 82
 	/** Arg is string without quotes */
83
-	static String = new this('String');
83
+	static String = new CellExpressionOperation('String');
84 84
 	/** Arg is bool */
85
-	static Boolean = new this('Boolean');
85
+	static Boolean = new CellExpressionOperation('Boolean');
86 86
 	/** Arg is reference address (e.g. "A5") */
87
-	static Reference = new this('Reference');
87
+	static Reference = new CellExpressionOperation('Reference');
88 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 91
 	/** Args are two operand CellExpressions. */
92
-	static Add = new this('Add');
92
+	static Add = new CellExpressionOperation('Add');
93 93
 	/** Args are two operand CellExpressions */
94
-	static Subtract = new this('Subtract');
94
+	static Subtract = new CellExpressionOperation('Subtract');
95 95
 	/** Args are two operand CellExpressions */
96
-	static Multiply = new this('Multiply');
96
+	static Multiply = new CellExpressionOperation('Multiply');
97 97
 	/** Args are two operand CellExpressions */
98
-	static Divide = new this('Divide');
98
+	static Divide = new CellExpressionOperation('Divide');
99 99
 
100 100
 	/** Args are two operand CellExpressions. */
101
-	static Concatenate = new this('Concatenate');
101
+	static Concatenate = new CellExpressionOperation('Concatenate');
102 102
 
103 103
 	/** Arg is operand expression */
104
-	static UnaryMinus = new this('UnaryMinus');
104
+	static UnaryMinus = new CellExpressionOperation('UnaryMinus');
105 105
 
106 106
 	/** Args are two operand CellExpressions. */
107
-	static GreaterThan = new this('GreaterThan');
107
+	static GreaterThan = new CellExpressionOperation('GreaterThan');
108 108
 	/** Args are two operand CellExpressions. */
109
-	static GreaterThanEqual = new this('GreaterThanEqual');
109
+	static GreaterThanEqual = new CellExpressionOperation('GreaterThanEqual');
110 110
 	/** Args are two operand CellExpressions. */
111
-	static LessThan = new this('LessThan');
111
+	static LessThan = new CellExpressionOperation('LessThan');
112 112
 	/** Args are two operand CellExpressions. */
113
-	static LessThanEqual = new this('LessThanEqual');
113
+	static LessThanEqual = new CellExpressionOperation('LessThanEqual');
114 114
 	/** Args are two operand CellExpressions. */
115
-	static Equal = new this('Equal');
115
+	static Equal = new CellExpressionOperation('Equal');
116 116
 	/** Args are two operand CellExpressions. */
117
-	static Unequal = new this('Unequal');
117
+	static Unequal = new CellExpressionOperation('Unequal');
118 118
 
119 119
 	/** Arg is operand expression. */
120
-	static UnaryNot = new this('UnaryNot');
120
+	static UnaryNot = new CellExpressionOperation('UnaryNot');
121 121
 
122 122
 	/** Args are 0+ CellExpressions */
123
-	static Function = new this('Function');
123
+	static Function = new CellExpressionOperation('Function');
124 124
 
125
+	/** @type {string} */
125 126
 	name;
126 127
 
127 128
 	constructor(name) {
@@ -129,7 +130,7 @@ class CellExpressionOperation {
129 130
 	}
130 131
 
131 132
 	toString() {
132
-		return this.name;
133
+		return `${this.constructor.name}.${this.name}`;
133 134
 	}
134 135
 
135 136
 	equals(other) {
@@ -211,10 +212,16 @@ class CellExpressionSet {
211 212
 				cell.isCalculated = true;
212 213
 				if (result instanceof CellValue) {
213 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 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 226
 			} catch (e) {
220 227
 				if (e instanceof CellDependencyException) {
@@ -298,7 +305,7 @@ class CellExpressionSet {
298 305
 					throw new CellEvaluationException(`No cell at ${refAddress.name}`, '#REF');
299 306
 				}
300 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 310
 				return cell.outputValue;
304 311
 			}
@@ -387,7 +394,7 @@ class CellExpressionSet {
387 394
 			case CellExpressionOperation.Function:
388 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,7 +625,7 @@ class CellExpressionSet {
618 625
 			throw new CellEvaluationException("IFS expects an odd number of arguments");
619 626
 		}
620 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 629
 			const test = evaled[i].booleanValue();
623 630
 			if (test === null) {
624 631
 				throw new CellEvaluationException(`IFS expects a boolean for argument ${i + 1}`);
@@ -678,7 +685,7 @@ class CellExpressionSet {
678 685
 	 * @returns {CellValue}
679 686
 	 */
680 687
 	#funcMax(args, address) {
681
-		const maxValue = null;
688
+		var maxValue = null;
682 689
 		const flattened = this.#flattenedNumericArguments('MAX', args, address);
683 690
 		if (flattened.length == 0) {
684 691
 			throw new CellEvaluationException("MAX requires at least one numeric argument");
@@ -781,7 +788,7 @@ class CellExpressionSet {
781 788
 	#funcRound(args, address) {
782 789
 		const evaled = this.#assertNumericArguments('ROUND', 1, 2, args, address);
783 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 792
 		const divider = Math.pow(10.0, places);
786 793
 		const newValue = Math.round(val.value * divider) / divider;
787 794
 		return CellValue.fromValue(newValue, val.type);
@@ -820,7 +827,7 @@ class CellExpressionSet {
820 827
 		if (text === null || search === null || replace === null) {
821 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 831
 		return CellValue.fromValue(result);
825 832
 	}
826 833
 
@@ -879,7 +886,7 @@ class CellExpressionSet {
879 886
 			}
880 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,13 +968,25 @@ class CellExpression {
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 984
 	 * @param {CellAddress} start
966 985
 	 * @param {CellAddress} end
967 986
 	 * @returns {CellExpression|null}
968 987
 	 */
969 988
 	transpose(start, end) {
970
-		var transposed = structuredClone(this);
989
+		var transposed = this.#clone(); // structuredClone makes a mess of typing
971 990
 		transposed.arguments = [];
972 991
 		for (const argument of this.arguments) {
973 992
 			if (argument instanceof CellExpression) {
@@ -992,15 +1011,15 @@ class CellExpression {
992 1011
 	 */
993 1012
 	static expressionToTokens(text) {
994 1013
 		var tokens = [];
995
-		var pos = 0;
1014
+		var pos = [0];
996 1015
 		this.#skipWhitespace(text, pos);
997
-		if (text.substring(pos, pos + 1) == '=') {
1016
+		if (text.substring(pos[0], pos[0] + 1) == '=') {
998 1017
 			// Ignore equals
999
-			pos++;
1018
+			pos[0]++;
1000 1019
 		}
1001 1020
 		this.#skipWhitespace(text, pos);
1002 1021
 		var l = text.length;
1003
-		while (pos < l) {
1022
+		while (pos[0] < l) {
1004 1023
 			tokens.push(this.#readNextToken(text, pos));
1005 1024
 			this.#skipWhitespace(text, pos);
1006 1025
 		}
@@ -1208,21 +1227,24 @@ class CellExpression {
1208 1227
 	 * @returns {string|null}
1209 1228
 	 */
1210 1229
 	static #readChars(text, pos, charTest, minimumLength = null, maximumLength = null) {
1211
-		var p = [ pos[0] ];
1230
+		var p = pos[0];
1212 1231
 		const l = text.length;
1213 1232
 		var s = '';
1214 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 1236
 			if (!charTest(ch)) break;
1218 1237
 			s += ch;
1219 1238
 			sl++;
1220
-			p[0]++;
1239
+			p++;
1240
+		}
1241
+		if (p < l && charTest(text.substring(p, p + 1))) {
1242
+			return null;
1221 1243
 		}
1222 1244
 		if (minimumLength !== null && sl < minimumLength) {
1223 1245
 			return null;
1224 1246
 		}
1225
-		pos[0] = p[0];
1247
+		pos[0] = p;
1226 1248
 		return s;
1227 1249
 	}
1228 1250
 
@@ -1509,7 +1531,7 @@ class CellExpression {
1509 1531
 		if (start != end) return null;
1510 1532
 		if (!tokens[start].type.isPotentialAddress()) return null;
1511 1533
 		const ref = tokens[start].content.toUpperCase();
1512
-		const refAddress = new CellAddress(ref, address);
1534
+		const refAddress = CellAddress.fromString(ref, address, true);
1513 1535
 		return new CellExpression(CellExpressionOperation.Reference, [ refAddress ]);
1514 1536
 	}
1515 1537
 
@@ -1547,22 +1569,19 @@ class CellExpression {
1547 1569
 				const op = tokens[i].type.name;
1548 1570
 				const priority = opPriorities[op] ?? false;
1549 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 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 1577
 		var bestCandidate = null;
1578
+		var operand1, operand2;
1560 1579
 		for (const candidate of candidates) {
1561 1580
 			try {
1562 1581
 				i = candidate.i;
1563
-				const operand1 = this.#tryExpression(tokens, start, i - 1, address);
1582
+				operand1 = this.#tryExpression(tokens, start, i - 1, address);
1564 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 1585
 				if (operand2 === null) continue;
1567 1586
 				bestCandidate = candidate;
1568 1587
 				break;
@@ -1573,11 +1592,11 @@ class CellExpression {
1573 1592
 			}
1574 1593
 		}
1575 1594
 		if (bestCandidate === null) {
1576
-			console.error("No best candidate found");
1595
+			//console.info("No best candidate found");
1577 1596
 			return null;
1578 1597
 		}
1579 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 1600
 		switch (tokens[bestCandidate.i].type) {
1582 1601
 			case CellExpressionTokenType.Plus:
1583 1602
 				return new CellExpression(CellExpressionOperation.Add, [ operand1, operand2 ]);
@@ -1662,7 +1681,7 @@ class CellAddress {
1662 1681
 	/**
1663 1682
 	 * @type {string}
1664 1683
 	 */
1665
-	get name() { this.#name; }
1684
+	get name() { return this.#name; }
1666 1685
 
1667 1686
 	#isColumnFixed = false;
1668 1687
 	/** 
@@ -1679,6 +1698,12 @@ class CellAddress {
1679 1698
 	 */
1680 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 1707
 	#isRowFixed = false;
1683 1708
 	/**
1684 1709
 	 * Whether the row should remain unchanged when transposed. This is
@@ -1695,6 +1720,11 @@ class CellAddress {
1695 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 1728
 	 * Whether this address has both a definite column and row.
1699 1729
 	 * @type {boolean}
1700 1730
 	 */
@@ -1709,14 +1739,17 @@ class CellAddress {
1709 1739
 	 *   during transpositions. Denoted with a `$` in front of the row digits.
1710 1740
 	 */
1711 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 1748
 		this.#columnIndex = columnIndex;
1713 1749
 		this.#rowIndex = rowIndex;
1714 1750
 		this.#isColumnFixed = isColumnFixed;
1715 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,7 +1758,7 @@ class CellAddress {
1725 1758
 	 * @returns {boolean}
1726 1759
 	 */
1727 1760
 	static isAddress(text) {
1728
-		return this.fromAddress(text) != null;
1761
+		return this.fromString(text) != null;
1729 1762
 	}
1730 1763
 
1731 1764
 	/**
@@ -1753,17 +1786,17 @@ class CellAddress {
1753 1786
 		if (!relativeFrom.isResolved || !relativeTo.isResolved) {
1754 1787
 			throw new CellEvaluationException("Can only transpose to and from resolved addresses");
1755 1788
 		}
1756
-		newColumnIndex = this.columnIndex;
1789
+		var newColumnIndex = this.columnIndex;
1757 1790
 		if (!this.isColumnFixed) {
1758
-			columnDelta = relativeTo.columnIndex - relativeFrom.columnIndex;
1791
+			const columnDelta = relativeTo.columnIndex - relativeFrom.columnIndex;
1759 1792
 			newColumnIndex += columnDelta;
1760 1793
 		}
1761
-		newRowIndex = this.rowIndex;
1794
+		var newRowIndex = this.rowIndex;
1762 1795
 		if (!this.isResolved && resolveToRow) {
1763 1796
 			newRowIndex = relativeFrom.rowIndex;
1764 1797
 		}
1765 1798
 		if (newRowIndex != -1 && !this.isRowAbsolute) {
1766
-			rowDelta = relativeTo.rowIndex - relativeFrom.rowIndex;
1799
+			const rowDelta = relativeTo.rowIndex - relativeFrom.rowIndex;
1767 1800
 			newRowIndex += rowDelta;
1768 1801
 		}
1769 1802
 		if (newColumnIndex < 0 || newRowIndex < 0) return null;
@@ -1788,10 +1821,10 @@ class CellAddress {
1788 1821
 	 * Converts column letters (e.g. `A`, `C`, `AA`) to a 0-based column index.
1789 1822
 	 * Assumes a validated well-formed column letter or else behavior is undefined.
1790 1823
 	 *
1791
-	 * @param {string} letter
1824
+	 * @param {string} letters
1792 1825
 	 * @returns {number} column index
1793 1826
 	 */
1794
-	static #lettersToColumnIndex(letter) {
1827
+	static #lettersToColumnIndex(letters) {
1795 1828
 		const ACodepoint = 'A'.codePointAt(0);
1796 1829
 		var columnIndex = 0;
1797 1830
 		for (var i = letters.length - 1; i >= 0; i--) {
@@ -1820,6 +1853,15 @@ class CellAddress {
1820 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 1866
 	 * @param {string} address - cell address string
1825 1867
 	 * @param {CellAddress|null} relativeTo - address to resolve relative addresses against
@@ -1885,10 +1927,10 @@ class CellAddressRange {
1885 1927
 	 * @returns {object} iterable object
1886 1928
 	 */
1887 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 1934
 		const iterable = {};
1893 1935
 		iterable[Symbol.iterator] = function() {
1894 1936
 			var currentCol = minCol;
@@ -1950,6 +1992,12 @@ class CellValue {
1950 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 2001
 	 * Type of value. One of the `TYPE_` constants.
1954 2002
 	 * @type {string}
1955 2003
 	 */
@@ -2029,9 +2077,9 @@ class CellValue {
2029 2077
 		}
2030 2078
 		if (value instanceof Error) {
2031 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 2084
 		if (typeof value == 'boolean') {
2037 2085
 			const formatted = CellValue.formatType(value, CellValue.TYPE_BOOLEAN, 0);
@@ -2529,7 +2577,7 @@ class CellValue {
2529 2577
 	 * @returns {string}
2530 2578
 	 */
2531 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,7 +2586,7 @@ class CellValue {
2538 2586
 	 * @returns {string}
2539 2587
 	 */
2540 2588
 	static #formatCurrency(dollars, decimals) {
2541
-		var s = (dollars).toLocaleString(undefined, { minimumFractionDigits: decimals });
2589
+		var s = (dollars).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
2542 2590
 		if (s.startsWith('-')) {
2543 2591
 			return '-$' + s.substring(1);
2544 2592
 		}
@@ -2552,7 +2600,7 @@ class CellValue {
2552 2600
 	 */
2553 2601
 	static #formatPercent(value, decimals) {
2554 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,16 +2614,9 @@ class CellValue {
2566 2614
 			return CellValue.#autodecimals(value.value);
2567 2615
 		}
2568 2616
 		if (typeof value == 'number') {
2569
-			var s = `${Math.abs(value)}`;
2617
+			var s = (value).toLocaleString(undefined, { maximumFractionDigits: maxDigits });
2570 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 2620
 			return Math.min(maxDigits, fraction.length);
2580 2621
 		}
2581 2622
 		return 0;
@@ -2683,12 +2724,12 @@ class SpreadsheetCell {
2683 2724
 	/**
2684 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 2735
 	 * @type {CellValue|null}
@@ -2699,7 +2740,7 @@ class SpreadsheetCell {
2699 2740
 	isCalculated = false;
2700 2741
 
2701 2742
 	/** @type {CellExpression|null} */
2702
-	parsedExpression;
2743
+	parsedExpression = null;
2703 2744
 
2704 2745
 	/**
2705 2746
 	 * @type {CellValue|null}

+ 270
- 1
testjs.html Parādīt failu

@@ -123,7 +123,7 @@
123 123
 				assertFalse(test, failMessage=null) {
124 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 127
 					if (MDUtils.equal(a, b, floatDifferenceRatio)) return;
128 128
 					const aVal = (typeof a == 'string') ? `"${a}"` : `${a}`;
129 129
 					const bVal = (typeof b == 'string') ? `"${b}"` : `${b}`;
@@ -430,6 +430,8 @@
430 430
 					// InlineTests,
431 431
 					// BlockTests,
432 432
 					CellValueTests,
433
+					CellAddressRangeTests,
434
+					ExpressionSetTests,
433 435
 				];
434 436
 				TestClassRunner.runAll(testClasses);
435 437
 			}
@@ -1366,6 +1368,11 @@
1366 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 1376
 				test_operation_comparators() {
1370 1377
 					const a = CellValue.fromValue(3);
1371 1378
 					const b = CellValue.fromValue(4);
@@ -1402,6 +1409,268 @@
1402 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 1674
 		</script>
1406 1675
 	</head>
1407 1676
 	<body>

Notiek ielāde…
Atcelt
Saglabāt