Sfoglia il codice sorgente

Adding PHP spreadsheet unit tests, various fixes along the way

main
Rocketsoup 1 anno fa
parent
commit
3bba88f8fc

+ 2
- 2
jstest/spreadsheet/CellValueTests.js Vedi File

@@ -137,9 +137,9 @@ class CellValueTests extends BaseTest {
137 137
 		value = CellValue.fromValue('foo');
138 138
 		this.assertEqual(CellValue.TYPE_STRING, value.type);
139 139
 		this.assertEqual('foo', value.formattedValue);
140
-		value = CellValue.fromValue('123');
140
+		value = CellValue.fromValue('0123456');
141 141
 		this.assertEqual(CellValue.TYPE_STRING, value.type);
142
-		this.assertEqual('123', value.formattedValue);
142
+		this.assertEqual('0123456', value.formattedValue);
143 143
 	}
144 144
 
145 145
 	test_fromValue_formula() {

+ 29
- 27
php/spreadsheet.php Vedi File

@@ -661,7 +661,7 @@ class CellExpressionSet {
661 661
 		}
662 662
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
663 663
 		for ($i = 0; $i < count($evaled) - 1; $i += 2) {
664
-			$test = $evaled[$i].booleanValue();
664
+			$test = $evaled[$i]->booleanValue();
665 665
 			if ($test === null) {
666 666
 				throw new CellEvaluationException("IFS expects a boolean for argument " . ($i + 1));
667 667
 			}
@@ -807,7 +807,7 @@ class CellExpressionSet {
807 807
 			throw new CellEvaluationException("NOT expects one argument");
808 808
 		}
809 809
 		$evaled = array_map(fn($arg) => $this->evaluate($arg, $address), $args);
810
-		$b = $evaled[0].booleanValue();
810
+		$b = $evaled[0]->booleanValue();
811 811
 		if ($b === null) {
812 812
 			throw new CellEvaluationException("NOT expects boolean argument");
813 813
 		}
@@ -865,7 +865,7 @@ class CellExpressionSet {
865 865
 		$val = $evaled[0];
866 866
 		$places = count($evaled) > 1 ? $evaled[1]->value : 0;
867 867
 		$divider = pow(10.0, $places);
868
-		$newValue = round($val->value * $divider) / $divider;
868
+		$newValue = floor($val->value * $divider + 0.5) / $divider;
869 869
 		return CellValue::fromValue($newValue, $val->type);
870 870
 	}
871 871
 
@@ -901,7 +901,7 @@ class CellExpressionSet {
901 901
 		if ($text === null || $search === null || $replace === null) {
902 902
 			throw new CellEvaluationException("SUBSTITUTE expects 3 string arguments");
903 903
 		}
904
-		$result = str_replace($search, $replace, $text);
904
+		$result = str_ireplace($search, $replace, $text);
905 905
 		return CellValue::fromValue($result);
906 906
 	}
907 907
 
@@ -1841,14 +1841,14 @@ class CellAddress {
1841 1841
 	 * not be altered on that axis.
1842 1842
 	 *
1843 1843
 	 * Examples:
1844
-	 * - C6.transpose(A5, A9) = C10  (A9-A5 = +4 rows, C6 + 4 rows = C10)
1845
-	 * - C6.transpose(A5, B9) = D10  (B9-A5 = +4 rows +1 cols, C6 + 4 rows + 1
1844
+	 * - C6->transpose(A5, A9) = C10  (A9-A5 = +4 rows, C6 + 4 rows = C10)
1845
+	 * - C6->transpose(A5, B9) = D10  (B9-A5 = +4 rows +1 cols, C6 + 4 rows + 1
1846 1846
 	 *   col = D10)
1847
-	 * - C$6.transpose(A5, A9) = C6  (A9-A5 = +4 rows, but row is fixed, so
1847
+	 * - C$6->transpose(A5, A9) = C6  (A9-A5 = +4 rows, but row is fixed, so
1848 1848
 	 *   still C6)
1849
-	 * - B.transpose(A5, A9) = B9  (A9-A4 = +4 rows, B has no row so last row
1849
+	 * - B->transpose(A5, A9) = B9  (A9-A4 = +4 rows, B has no row so last row
1850 1850
 	 *   used = B9)
1851
-	 * - A1.transpose(C3, A1) = null (out of bounds)
1851
+	 * - A1->transpose(C3, A1) = null (out of bounds)
1852 1852
 	 * 
1853 1853
 	 * @param CellAddress $relativeFrom  original address of the formula
1854 1854
 	 * @param CellAddress $relativeTo  address where the formula is being
@@ -2003,7 +2003,7 @@ class CellAddressRange {
2003 2003
 	 * 
2004 2004
 	 * Example:
2005 2005
 	 * ```
2006
-	 * foreach (range.cellsIn(grid) as $addressString => $cell) {
2006
+	 * foreach ($range->cellsIn($grid) as $addressString => $cell) {
2007 2007
 	 *     ...
2008 2008
 	 * }
2009 2009
 	 * ```
@@ -2012,17 +2012,10 @@ class CellAddressRange {
2012 2012
 	 * @return object iterable object
2013 2013
 	 */
2014 2014
 	public function cellsIn(SpreadsheetGrid $grid): Iterator {
2015
-		$minCol = $this->minColumnIndex;
2016
-		$maxCol = $this->maxColumnIndex;
2017
-		$minRow = $this->minRowIndex;
2018
-		$maxRow = $this->maxRowIndex;
2019
-		if ($minRow === -1) {
2020
-			$minRow = 0;
2021
-		}
2022
-		if ($maxRow === -1) {
2023
-			$maxRow = $grid->rowCount - 1;
2024
-		}
2025
-		$maxCol = min($maxCol, $grid->columnCount);
2015
+		$minCol = max(0, $this->minColumnIndex);
2016
+		$maxCol = $this->maxColumnIndex < 0 ? $grid->columnCount - 1 : min($this->maxColumnIndex, $grid->columnCount - 1);
2017
+		$minRow = max(0, $this->minRowIndex);
2018
+		$maxRow = $this->maxRowIndex < 0 ? $grid->rowCount - 1 : min($this->maxRowIndex, $grid->rowCount - 1);
2026 2019
 		return new class($grid, $minCol, $maxCol, $minRow, $maxRow) implements Iterator {
2027 2020
 			private SpreadsheetGrid $grid;
2028 2021
 			private int $minCol;
@@ -2152,7 +2145,7 @@ class CellValue {
2152 2145
 
2153 2146
 	/**
2154 2147
 	 * Constructs a cell value explicitly. Values are not validated. Consider
2155
-	 * using `.fromCellString()` or `.fromValue()` to populate values more
2148
+	 * using `->fromCellString()` or `->fromValue()` to populate values more
2156 2149
 	 * intelligently and consistently.
2157 2150
 	 */
2158 2151
 	public function __construct(
@@ -2205,11 +2198,17 @@ class CellValue {
2205 2198
 			}
2206 2199
 			return new CellValue('#ERROR', $value->getMessage(), CellValue::TYPE_ERROR);
2207 2200
 		}
2208
-		if (is_bool($value)) {
2201
+		if (gettype($value) === 'boolean') {
2209 2202
 			$formatted = CellValue::formatType($value, CellValue::TYPE_BOOLEAN, 0);
2210 2203
 			return new CellValue($formatted, $value, CellValue::TYPE_BOOLEAN);
2211 2204
 		}
2212
-		if (is_numeric($value)) {
2205
+		if (gettype($value) === 'integer') {
2206
+			$resolvedType = $type ?? CellValue::TYPE_NUMBER;
2207
+			$resolvedDecimals = 0;
2208
+			$formatted = CellValue::formatType($value, $resolvedType, $resolvedDecimals);
2209
+			return new CellValue($formatted, $value, $resolvedType, $resolvedDecimals);
2210
+		}
2211
+		if (gettype($value) === 'double') {
2213 2212
 			$resolvedType = $type ?? CellValue::TYPE_NUMBER;
2214 2213
 			$resolvedDecimals = ($decimals !== null) ? $decimals :
2215 2214
 				($resolvedType === CellValue::TYPE_CURRENCY ? 2 :
@@ -2323,7 +2322,7 @@ class CellValue {
2323 2322
 			$wholes = mb_eregi_replace(',', '', $groups[1]);
2324 2323
 			$this->type = CellValue::TYPE_NUMBER;
2325 2324
 			$this->decimals = 0;
2326
-			$this->value = floatval($wholes);
2325
+			$this->value = intval($wholes);
2327 2326
 			$this->formattedValue = CellValue::formatNumber($this->value, $this->decimals);
2328 2327
 			return;
2329 2328
 		}
@@ -2437,8 +2436,11 @@ class CellValue {
2437 2436
 	 */
2438 2437
 	public function modulo($b) {
2439 2438
 		return self::binaryNumericOperation($this, $b, '%', function($aVal, $bVal) {
2440
-			if ($bVal === 0) throw new CellEvaluationException("Division by zero", '#NAN');
2441
-			return $aVal % $bVal;
2439
+			if ($bVal == 0) throw new CellEvaluationException("Division by zero", '#NAN');
2440
+			if (gettype($aVal) === 'integer' && gettype($bVal) === 'integer') {
2441
+				return $aVal % $bVal;
2442
+			}
2443
+			return fmod($aVal, $bVal);
2442 2444
 		});
2443 2445
 	}
2444 2446
 

+ 66
- 0
phptest/spreadsheet/CellAddressRangeTests.php Vedi File

@@ -0,0 +1,66 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+use PHPUnit\Framework\TestCase;
5
+
6
+require_once __DIR__ . '/../../php/markdown.php';
7
+require_once __DIR__ . '/../../php/spreadsheet.php';
8
+
9
+final class CellAddressRangeTests extends TestCase {
10
+	public function test_iterator() {
11
+		$grid = new SpreadsheetGrid(3, 4);
12
+		$range = new CellAddressRange(new CellAddress(0, 0), new CellAddress(2, 3));
13
+		$visited = [];
14
+		$sanity = 100;
15
+		foreach ($range->cellsIn($grid) as $address_string => $cell) {
16
+			array_push($visited, $address_string);
17
+			if ($sanity-- < 0) break;
18
+		}
19
+		$actual = implode(',', $visited);
20
+		$expected = 'A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4';
21
+		$this->assertSame($expected, $actual);
22
+	}
23
+
24
+	public function test_iterator_column() {
25
+		$grid = new SpreadsheetGrid(3, 4);
26
+		$range = new CellAddressRange(new CellAddress(1, -1), new CellAddress(2, -1));
27
+		$visited = [];
28
+		$sanity = 100;
29
+		foreach ($range->cellsIn($grid) as $address_string => $cell) {
30
+			array_push($visited, $address_string);
31
+			if ($sanity-- < 0) break;
32
+		}
33
+		$actual = implode(',', $visited);
34
+		$expected = 'B1,B2,B3,B4,C1,C2,C3,C4';
35
+		$this->assertSame($expected, $actual);
36
+	}
37
+
38
+	public function test_iterator_beyondBounds() {
39
+		$grid = new SpreadsheetGrid(3, 4);
40
+		$range = new CellAddressRange(new CellAddress(0, 1), new CellAddress(9, 9));
41
+		$visited = [];
42
+		$sanity = 100;
43
+		foreach ($range->cellsIn($grid) as $address_string => $cell) {
44
+			array_push($visited, $address_string);
45
+			if ($sanity-- < 0) break;
46
+		}
47
+		$actual = implode(',', $visited);
48
+		$expected = 'A2,A3,A4,B2,B3,B4,C2,C3,C4';
49
+		$this->assertSame($expected, $actual);
50
+	}
51
+
52
+	public function test_iterator_outOfBounds() {
53
+		$grid = new SpreadsheetGrid(3, 4);
54
+		$range = new CellAddressRange(new CellAddress(5, 0), new CellAddress(5, 9));
55
+		$visited = [];
56
+		$sanity = 100;
57
+		foreach ($range->cellsIn($grid) as $address_string => $cell) {
58
+			array_push($visited, $address_string);
59
+			if ($sanity-- < 0) break;
60
+		}
61
+		$actual = implode(',', $visited);
62
+		$expected = '';
63
+		$this->assertSame($expected, $actual);
64
+	}
65
+}
66
+?>

+ 296
- 0
phptest/spreadsheet/CellValueTests.php Vedi File

@@ -0,0 +1,296 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+use PHPUnit\Framework\TestCase;
5
+
6
+require_once __DIR__ . '/../../php/markdown.php';
7
+require_once __DIR__ . '/../../php/spreadsheet.php';
8
+
9
+final class CellValueTests extends TestCase {
10
+	public function test_fromCellString_blank() {
11
+		$value = CellValue::fromCellString('');
12
+		$this->assertSame(CellValue::TYPE_BLANK, $value->type);
13
+		$this->assertSame('', $value->formattedValue);
14
+		$this->assertSame(null, $value->value);
15
+		$value = CellValue::fromCellString(' ');
16
+		$this->assertSame(CellValue::TYPE_BLANK, $value->type);
17
+		$this->assertSame('', $value->formattedValue);
18
+		$this->assertSame(null, $value->value);
19
+	}
20
+
21
+	public function test_fromCellString_number() {
22
+		$value = CellValue::fromCellString('123');
23
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
24
+		$this->assertSame('123', $value->formattedValue);
25
+		$this->assertEqualsWithDelta(123, $value->value, 0.000_001);
26
+		$this->assertSame(0, $value->decimals);
27
+		$value = CellValue::fromCellString('-0');
28
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
29
+		$this->assertSame('0', $value->formattedValue);
30
+		$this->assertEqualsWithDelta(0, $value->value, 0.000_001);
31
+		$this->assertSame(0, $value->decimals);
32
+		$value = CellValue::fromCellString('1,234');
33
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
34
+		$this->assertSame('1,234', $value->formattedValue);
35
+		$this->assertEqualsWithDelta(1234, $value->value, 0.000_001);
36
+		$this->assertSame(0, $value->decimals);
37
+		$value = CellValue::fromCellString('-1,234,567.89');
38
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
39
+		$this->assertSame('-1,234,567.89', $value->formattedValue);
40
+		$this->assertEqualsWithDelta(-1234567.89, $value->value, 0.000_001);
41
+		$this->assertSame(2, $value->decimals);
42
+	}
43
+
44
+	public function test_fromCellString_percent() {
45
+		$value = CellValue::fromCellString('123%');
46
+		$this->assertSame(CellValue::TYPE_PERCENT, $value->type);
47
+		$this->assertSame('123%', $value->formattedValue);
48
+		$this->assertEqualsWithDelta(1.23, $value->value, 0.000_001);
49
+		$this->assertSame(0, $value->decimals);
50
+		$value = CellValue::fromCellString('-12.3%');
51
+		$this->assertSame(CellValue::TYPE_PERCENT, $value->type);
52
+		$this->assertSame('-12.3%', $value->formattedValue);
53
+		$this->assertEqualsWithDelta(-0.123, $value->value, 0.000_001);
54
+		$this->assertSame(1, $value->decimals);
55
+	}
56
+
57
+	public function test_fromCellString_currency() {
58
+		$value = CellValue::fromCellString('$123');
59
+		$this->assertSame(CellValue::TYPE_CURRENCY, $value->type);
60
+		$this->assertSame('$123', $value->formattedValue);
61
+		$this->assertEqualsWithDelta(123, $value->value, 0.000_001);
62
+		$this->assertSame(0, $value->decimals);
63
+		$value = CellValue::fromCellString('-$12.34');
64
+		$this->assertSame(CellValue::TYPE_CURRENCY, $value->type);
65
+		$this->assertSame('-$12.34', $value->formattedValue);
66
+		$this->assertEqualsWithDelta(-12.34, $value->value, 0.000_001);
67
+		$this->assertSame(2, $value->decimals);
68
+	}
69
+
70
+	public function test_fromCellString_boolean() {
71
+		$value = CellValue::fromCellString('true');
72
+		$this->assertSame(CellValue::TYPE_BOOLEAN, $value->type);
73
+		$this->assertSame('TRUE', $value->formattedValue);
74
+		$this->assertSame(true, $value->value);
75
+		$value = CellValue::fromCellString('false');
76
+		$this->assertSame(CellValue::TYPE_BOOLEAN, $value->type);
77
+		$this->assertSame('FALSE', $value->formattedValue);
78
+		$this->assertSame(false, $value->value);
79
+	}
80
+
81
+	public function test_fromCellString_string() {
82
+		$value = CellValue::fromCellString('some text');
83
+		$this->assertSame(CellValue::TYPE_STRING, $value->type);
84
+		$this->assertSame('some text', $value->formattedValue);
85
+		$this->assertSame('some text', $value->value);
86
+		$value = CellValue::fromCellString("'0123");
87
+		$this->assertSame(CellValue::TYPE_STRING, $value->type);
88
+		$this->assertSame('0123', $value->formattedValue);
89
+		$this->assertSame('0123', $value->value);
90
+		$value = CellValue::fromCellString("'=123");
91
+		$this->assertSame(CellValue::TYPE_STRING, $value->type);
92
+		$this->assertSame('=123', $value->formattedValue);
93
+		$this->assertSame('=123', $value->value);
94
+	}
95
+
96
+	public function test_fromCellString_formula() {
97
+		$value = CellValue::fromCellString('=A*B');
98
+		$this->assertSame(CellValue::TYPE_FORMULA, $value->type);
99
+		$this->assertSame('=A*B', $value->formattedValue);
100
+		$this->assertSame('=A*B', $value->value);
101
+		$value = CellValue::fromCellString('=MAX(A, 3)');
102
+		$this->assertSame(CellValue::TYPE_FORMULA, $value->type);
103
+		$this->assertSame('=MAX(A, 3)', $value->formattedValue);
104
+		$this->assertSame('=MAX(A, 3)', $value->value);
105
+	}
106
+
107
+	public function test_fromValue_null() {
108
+		$value = CellValue::fromValue(null);
109
+		$this->assertSame(CellValue::TYPE_BLANK, $value->type);
110
+	}
111
+
112
+	public function test_fromValue_number() {
113
+		$value = CellValue::fromValue(123);
114
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
115
+		$this->assertSame('123', $value->formattedValue);
116
+		$value = CellValue::fromValue(3.141592);
117
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
118
+		$this->assertSame('3.141592', $value->formattedValue);
119
+		$value = CellValue::fromValue(123456789);
120
+		$this->assertSame(CellValue::TYPE_NUMBER, $value->type);
121
+		$this->assertSame('123,456,789', $value->formattedValue);
122
+	}
123
+
124
+	public function test_fromValue_boolean() {
125
+		$value = CellValue::fromValue(true);
126
+		$this->assertSame(CellValue::TYPE_BOOLEAN, $value->type);
127
+		$this->assertSame('TRUE', $value->formattedValue);
128
+		$value = CellValue::fromValue(false);
129
+		$this->assertSame(CellValue::TYPE_BOOLEAN, $value->type);
130
+		$this->assertSame('FALSE', $value->formattedValue);
131
+	}
132
+
133
+	public function test_fromValue_string() {
134
+		$value = CellValue::fromValue('foo');
135
+		$this->assertSame(CellValue::TYPE_STRING, $value->type);
136
+		$this->assertSame('foo', $value->formattedValue);
137
+		$value = CellValue::fromValue('0123456');
138
+		$this->assertSame(CellValue::TYPE_STRING, $value->type);
139
+		$this->assertSame('0123456', $value->formattedValue);
140
+	}
141
+
142
+	public function test_fromValue_formula() {
143
+		$value = CellValue::fromValue('=A*B');
144
+		$this->assertSame(CellValue::TYPE_FORMULA, $value->type);
145
+		$this->assertSame('=A*B', $value->formattedValue);
146
+	}
147
+
148
+	private function assertSameCellValue(CellValue $expected, CellValue $actual) {
149
+		$this->assertSame($expected->formattedValue, $actual->formattedValue);
150
+		$this->assertSame($expected->value, $actual->value);
151
+		$this->assertSame($expected->type, $actual->type);
152
+		$this->assertSame($expected->decimals, $actual->decimals);
153
+	}
154
+
155
+	public function test_operation_add() {
156
+		$a = CellValue::fromValue(3);
157
+		$b = CellValue::fromValue(4);
158
+		$actual = $a->add($b);
159
+		$expected = new CellValue('7', 7, CellValue::TYPE_NUMBER, 0);
160
+		$this->assertSameCellValue($expected, $actual);
161
+
162
+		$a = CellValue::fromCellString('100%');
163
+		$b = CellValue::fromCellString('50%');
164
+		$actual = $a->add($b);
165
+		$expected = new CellValue('150%', 1.5, CellValue::TYPE_PERCENT, 0);
166
+		$this->assertSameCellValue($expected, $actual);
167
+
168
+		$a = CellValue::fromCellString('$123');
169
+		$b = CellValue::fromCellString('$321');
170
+		$actual = $a->add($b);
171
+		$expected = new CellValue('$444.00', 444.0, CellValue::TYPE_CURRENCY, 2);
172
+		$this->assertSameCellValue($expected, $actual);
173
+	}
174
+
175
+	public function test_operation_subtract() {
176
+		$a = CellValue::fromValue(9);
177
+		$b = CellValue::fromValue(4);
178
+		$actual = $a->subtract($b);
179
+		$expected = new CellValue('5', 5, CellValue::TYPE_NUMBER, 0);
180
+		$this->assertSameCellValue($expected, $actual);
181
+
182
+		$a = CellValue::fromCellString('100%');
183
+		$b = CellValue::fromCellString('50%');
184
+		$actual = $a->subtract($b);
185
+		$expected = new CellValue('50%', 0.5, CellValue::TYPE_PERCENT, 0);
186
+		$this->assertSameCellValue($expected, $actual);
187
+
188
+		$a = CellValue::fromCellString('$321');
189
+		$b = CellValue::fromCellString('$123');
190
+		$actual = $a->subtract($b);
191
+		$expected = new CellValue('$198.00', 198.0, CellValue::TYPE_CURRENCY, 2);
192
+		$this->assertSameCellValue($expected, $actual);
193
+	}
194
+
195
+	public function test_operation_multiply() {
196
+		$a = CellValue::fromValue(3);
197
+		$b = CellValue::fromValue(4);
198
+		$actual = $a->multiply($b);
199
+		$expected = new CellValue('12', 12, CellValue::TYPE_NUMBER, 0);
200
+		$this->assertSameCellValue($expected, $actual);
201
+
202
+		$a = CellValue::fromCellString('150%');
203
+		$b = CellValue::fromCellString('50%');
204
+		$actual = $a->multiply($b);
205
+		$expected = new CellValue('75%', 0.75, CellValue::TYPE_PERCENT, 0);
206
+		$this->assertSameCellValue($expected, $actual);
207
+
208
+		$a = CellValue::fromCellString('$321');
209
+		$b = CellValue::fromCellString('50%');
210
+		$actual = $a->multiply($b);
211
+		$expected = new CellValue('$160.50', 160.50, CellValue::TYPE_CURRENCY, 2);
212
+		$this->assertSameCellValue($expected, $actual);
213
+	}
214
+
215
+	public function test_operation_divide() {
216
+		$a = CellValue::fromValue(12);
217
+		$b = CellValue::fromValue(4);
218
+		$actual = $a->divide($b);
219
+		$expected = new CellValue('3', 3, CellValue::TYPE_NUMBER, 0);
220
+		$this->assertSameCellValue($expected, $actual);
221
+
222
+		$a = CellValue::fromCellString('150%');
223
+		$b = CellValue::fromCellString('50%');
224
+		$actual = $a->divide($b);
225
+		$expected = new CellValue('300%', 3.0, CellValue::TYPE_PERCENT, 0);
226
+		$this->assertSameCellValue($expected, $actual);
227
+
228
+		$a = CellValue::fromCellString('$321');
229
+		$b = CellValue::fromCellString('200%');
230
+		$actual = $a->divide($b);
231
+		$expected = new CellValue('$160.50', 160.50, CellValue::TYPE_CURRENCY, 2);
232
+		$this->assertSameCellValue($expected, $actual);
233
+	}
234
+
235
+	public function test_operation_modulo() {
236
+		$a = CellValue::fromValue(7);
237
+		$b = CellValue::fromValue(4);
238
+		$actual = $a->modulo($b);
239
+		$expected = new CellValue('3', 3, CellValue::TYPE_NUMBER, 0);
240
+		$this->assertSameCellValue($expected, $actual);
241
+
242
+		$a = CellValue::fromCellString('175%');
243
+		$b = CellValue::fromCellString('50%');
244
+		$actual = $a->modulo($b);
245
+		$expected = new CellValue('25%', 0.25, CellValue::TYPE_PERCENT, 0);
246
+		$this->assertSameCellValue($expected, $actual);
247
+
248
+		$a = CellValue::fromCellString('$327');
249
+		$b = CellValue::fromCellString('$20');
250
+		$actual = $a->modulo($b);
251
+		$expected = new CellValue('$7.00', 7.00, CellValue::TYPE_CURRENCY, 2);
252
+		$this->assertSameCellValue($expected, $actual);
253
+	}
254
+
255
+	public function test_operation_unaryNot() {
256
+		$this->assertSameCellValue(CellValue::fromValue(false), CellValue::fromValue(true)->not());
257
+		$this->assertSameCellValue(CellValue::fromValue(true), CellValue::fromValue(false)->not());
258
+	}
259
+
260
+	public function test_operation_comparators() {
261
+		$a = CellValue::fromValue(3);
262
+		$b = CellValue::fromValue(4);
263
+		$t = CellValue::fromValue(true);
264
+		$f = CellValue::fromValue(false);
265
+
266
+		$this->assertSameCellValue($t, $a->lt($b));
267
+		$this->assertSameCellValue($t, $a->lte($b));
268
+		$this->assertSameCellValue($f, $a->gt($b));
269
+		$this->assertSameCellValue($f, $a->gte($b));
270
+		$this->assertSameCellValue($f, $a->eq($b));
271
+		$this->assertSameCellValue($t, $a->neq($b));
272
+
273
+		$this->assertSameCellValue($f, $b->lt($a));
274
+		$this->assertSameCellValue($f, $b->lte($a));
275
+		$this->assertSameCellValue($t, $b->gt($a));
276
+		$this->assertSameCellValue($t, $b->gte($a));
277
+		$this->assertSameCellValue($f, $b->eq($a));
278
+		$this->assertSameCellValue($t, $b->neq($a));
279
+
280
+		$this->assertSameCellValue($f, $a->lt($a));
281
+		$this->assertSameCellValue($t, $a->lte($a));
282
+		$this->assertSameCellValue($f, $a->gt($a));
283
+		$this->assertSameCellValue($t, $a->gte($a));
284
+		$this->assertSameCellValue($t, $a->eq($a));
285
+		$this->assertSameCellValue($f, $a->neq($a));
286
+	}
287
+
288
+	public function test_operation_concatenate() {
289
+		$a = CellValue::fromValue('abc');
290
+		$b = CellValue::fromValue('xyz');
291
+		$actual = $a->concatenate($b);
292
+		$expected = CellValue::fromValue('abcxyz');
293
+		$this->assertSameCellValue($expected, $actual);
294
+	}
295
+}
296
+?>

+ 232
- 0
phptest/spreadsheet/ExpressionSetTests.php Vedi File

@@ -0,0 +1,232 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+use PHPUnit\Framework\TestCase;
5
+
6
+require_once __DIR__ . '/../../php/markdown.php';
7
+require_once __DIR__ . '/../../php/spreadsheet.php';
8
+
9
+final class ExpressionSetTests extends TestCase {
10
+	private function assertSameCellValue(CellValue $expected, CellValue $actual) {
11
+		$this->assertSame($expected->formattedValue, $actual->formattedValue);
12
+		if (gettype($expected->value) === 'double' && gettype($actual->value) === 'double') {
13
+			$this->assertEqualsWithDelta($expected->value, $actual->value, 0.000_001);
14
+		} else {
15
+			$this->assertSame($expected->value, $actual->value);
16
+		}
17
+		$this->assertSame($expected->type, $actual->type);
18
+		$this->assertSame($expected->decimals, $actual->decimals);
19
+	}
20
+
21
+	public function test_simpleMath() {
22
+		$grid = new SpreadsheetGrid(1, 1);
23
+		$grid->cells[0][0]->originalValue = CellValue::fromCellString('=7*3');
24
+		$expressionSet = new CellExpressionSet($grid);
25
+		$expressionSet->calculateCells();
26
+		$expected = CellValue::fromValue(21);
27
+		$actual = $grid->cells[0][0]->outputValue;
28
+		$this->assertSameCellValue($expected, $actual);
29
+	}
30
+
31
+	public function test_reference() {
32
+		$grid = new SpreadsheetGrid(3, 1);
33
+		$grid->cells[0][0]->originalValue = CellValue::fromValue(123);
34
+		$grid->cells[1][0]->originalValue = CellValue::fromValue(3);
35
+		$grid->cells[2][0]->originalValue = CellValue::fromCellString('=A1*B1');
36
+		$expressionSet = new CellExpressionSet($grid);
37
+		$expressionSet->calculateCells();
38
+		$expected = CellValue::fromValue(369);
39
+		$actual = $grid->cells[2][0]->outputValue;
40
+		$this->assertSameCellValue($expected, $actual);
41
+	}
42
+
43
+	public function test_infixPriority() {
44
+		$grid = new SpreadsheetGrid(1, 1);
45
+		$grid->cells[0][0]->originalValue = CellValue::fromCellString('=4*9/3+7-4');
46
+		$expressionSet = new CellExpressionSet($grid);
47
+		$expressionSet->calculateCells();
48
+		$expected = CellValue::fromValue(15);
49
+		$actual = $grid->cells[0][0]->outputValue;
50
+		$this->assertSameCellValue($expected, $actual);
51
+	}
52
+
53
+	public function test_filledFormula() {
54
+		$grid = new SpreadsheetGrid(3, 3);
55
+		$grid->cells[0][0]->originalValue = CellValue::fromValue(4);
56
+		$grid->cells[1][0]->originalValue = CellValue::fromValue(5);
57
+		$grid->cells[2][0]->originalValue = CellValue::fromCellString('=A1*B1 FILL');
58
+		$grid->cells[0][1]->originalValue = CellValue::fromValue(6);
59
+		$grid->cells[1][1]->originalValue = CellValue::fromValue(7);
60
+		$grid->cells[0][2]->originalValue = CellValue::fromValue(8);
61
+		$grid->cells[1][2]->originalValue = CellValue::fromValue(9);
62
+		$expressionSet = new CellExpressionSet($grid);
63
+		$expressionSet->calculateCells();
64
+		$this->assertSameCellValue(CellValue::fromValue(20), $grid->cells[2][0]->outputValue);
65
+		$this->assertSameCellValue(CellValue::fromValue(42), $grid->cells[2][1]->outputValue);
66
+		$this->assertSameCellValue(CellValue::fromValue(72), $grid->cells[2][2]->outputValue);
67
+	}
68
+
69
+	public function test_dependencies() {
70
+		$grid = new SpreadsheetGrid(2, 4);
71
+		$grid->cells[0][0]->originalValue = CellValue::fromValue(1);
72
+		$grid->cells[1][0]->originalValue = CellValue::fromCellString('=A1+B2');
73
+		$grid->cells[0][1]->originalValue = CellValue::fromValue(2);
74
+		$grid->cells[1][1]->originalValue = CellValue::fromCellString('=A2+B3');
75
+		$grid->cells[0][2]->originalValue = CellValue::fromValue(3);
76
+		$grid->cells[1][2]->originalValue = CellValue::fromCellString('=A3+B4');
77
+		$grid->cells[0][3]->originalValue = CellValue::fromValue(4);
78
+		$grid->cells[1][3]->originalValue = CellValue::fromCellString('=A4');
79
+		$expressionSet = new CellExpressionSet($grid);
80
+		$expressionSet->calculateCells();
81
+		$this->assertSameCellValue(CellValue::fromValue(10), $grid->cells[1][0]->outputValue);
82
+		$this->assertSameCellValue(CellValue::fromValue(9), $grid->cells[1][1]->outputValue);
83
+		$this->assertSameCellValue(CellValue::fromValue(7), $grid->cells[1][2]->outputValue);
84
+		$this->assertSameCellValue(CellValue::fromValue(4), $grid->cells[1][3]->outputValue);
85
+	}
86
+
87
+	private function test_simple_formula($expected, $formula) {
88
+		$grid = new SpreadsheetGrid(1, 1);
89
+		$grid->cells[0][0]->originalValue = CellValue::fromCellString($formula);
90
+		$expressionSet = new CellExpressionSet($grid);
91
+		$expressionSet->calculateCells();
92
+		$actual = $grid->cells[0][0]->outputValue;
93
+		$exp = ($expected instanceof CellValue) ? $expected : CellValue::fromValue($expected);
94
+		$this->assertSameCellValue($exp, $actual);
95
+	}
96
+
97
+	public function test_func_abs() {
98
+		$this->test_simple_formula(3, '=ABS(-3)');
99
+		$this->test_simple_formula(4, '=ABS(4)');
100
+	}
101
+
102
+	public function test_func_and() {
103
+		$this->test_simple_formula(false, '=AND(FALSE, FALSE)');
104
+		$this->test_simple_formula(false, '=AND(FALSE, TRUE)');
105
+		$this->test_simple_formula(false, '=AND(TRUE, FALSE)');
106
+		$this->test_simple_formula(true, '=AND(TRUE, TRUE)');
107
+		$this->test_simple_formula(true, '=AND(TRUE, TRUE, TRUE, TRUE)');
108
+		$this->test_simple_formula(false, '=AND(TRUE, TRUE, TRUE, FALSE)');
109
+	}
110
+
111
+	public function test_func_average() {
112
+		$this->test_simple_formula(4, '=AVERAGE(4, 6, 2, 4)');
113
+		$this->test_simple_formula(4, '=AVERAGE(4, 6, 2, "foo", 4)');
114
+	}
115
+
116
+	public function test_func_ceiling() {
117
+		$this->test_simple_formula(4.0, '=CEILING(3.1)');
118
+		$this->test_simple_formula(3.0, '=CEILING(3)');
119
+		$this->test_simple_formula(-3.0, '=CEILING(-3.1)');
120
+	}
121
+
122
+	public function test_func_exp() {
123
+		$this->test_simple_formula(2.718281828459045, '=EXP(1)');
124
+	}
125
+
126
+	public function test_func_floor() {
127
+		$this->test_simple_formula(3.0, '=FLOOR(3.1)');
128
+		$this->test_simple_formula(3.0, '=FLOOR(3)');
129
+		$this->test_simple_formula(-4.0, '=FLOOR(-3.1)');
130
+	}
131
+
132
+	public function test_func_if() {
133
+		$this->test_simple_formula(6, '=IF(FALSE, 4, 6)');
134
+		$this->test_simple_formula(4, '=IF(TRUE, 4, 6)');
135
+	}
136
+
137
+	public function test_func_ifs() {
138
+		$this->test_simple_formula(1, '=IFS(TRUE, 1, FALSE, 2, FALSE, 3, FALSE, 4, 5)');
139
+		$this->test_simple_formula(2, '=IFS(FALSE, 1, TRUE, 2, FALSE, 3, FALSE, 4, 5)');
140
+		$this->test_simple_formula(3, '=IFS(FALSE, 1, FALSE, 2, TRUE, 3, FALSE, 4, 5)');
141
+		$this->test_simple_formula(4, '=IFS(FALSE, 1, FALSE, 2, FALSE, 3, TRUE, 4, 5)');
142
+		$this->test_simple_formula(5, '=IFS(FALSE, 1, FALSE, 2, FALSE, 3, FALSE, 4, 5)');
143
+	}
144
+
145
+	public function test_func_ln() {
146
+		$this->test_simple_formula(1.0, '=LN(2.718281828459045)');
147
+	}
148
+
149
+	public function test_func_log() {
150
+		$this->test_simple_formula(3.0, '=LOG(1000, 10)');
151
+		$this->test_simple_formula(6.0, '=LOG(64, 2)');
152
+	}
153
+
154
+	public function test_func_lower() {
155
+		$this->test_simple_formula('mixed', '=LOWER("MiXeD")');
156
+	}
157
+
158
+	public function test_func_max() {
159
+		$this->test_simple_formula(8, '=MAX(4, 8, 5, 2)');
160
+	}
161
+
162
+	public function test_func_min() {
163
+		$this->test_simple_formula(2, '=MIN(4, 8, 5, 2)');
164
+	}
165
+
166
+	public function test_func_mod() {
167
+		$this->test_simple_formula(1, '=MOD(37, 4)');
168
+	}
169
+
170
+	public function test_func_not() {
171
+		$this->test_simple_formula(false, '=NOT(TRUE)');
172
+		$this->test_simple_formula(true, '=NOT(FALSE)');
173
+	}
174
+
175
+	public function test_func_or() {
176
+		$this->test_simple_formula(false, '=OR(FALSE, FALSE)');
177
+		$this->test_simple_formula(true, '=OR(FALSE, TRUE)');
178
+		$this->test_simple_formula(true, '=OR(TRUE, FALSE)');
179
+		$this->test_simple_formula(true, '=OR(TRUE, TRUE)');
180
+		$this->test_simple_formula(true, '=OR(FALSE, FALSE, FALSE, TRUE)');
181
+	}
182
+
183
+	public function test_func_power() {
184
+		$this->test_simple_formula(8, '=POWER(2, 3)');
185
+	}
186
+
187
+	public function test_func_round() {
188
+		$this->test_simple_formula(3.0, '=ROUND(3.1)');
189
+		$this->test_simple_formula(4.0, '=ROUND(3.5)');
190
+		$this->test_simple_formula(4.0, '=ROUND(4)');
191
+		$this->test_simple_formula(-3.0, '=ROUND(-3.1)');
192
+		$this->test_simple_formula(-3.0, '=ROUND(-3.5)');
193
+		$this->test_simple_formula(-4.0, '=ROUND(-3.9)');
194
+		$this->test_simple_formula(3.1, '=ROUND(3.1415926535, 1)');
195
+		$this->test_simple_formula(3.14, '=ROUND(3.1415926535, 2)');
196
+		$this->test_simple_formula(30.0, '=ROUND(31.415926535, -1)');
197
+	}
198
+
199
+	public function test_func_sqrt() {
200
+		$this->test_simple_formula(4.0, '=SQRT(16)');
201
+	}
202
+
203
+	public function test_func_substitute() {
204
+		$this->test_simple_formula('cot sot on the mot', '=SUBSTITUTE("cat sat on the mat", "at", "ot")');
205
+		$this->test_simple_formula('cot sot on the mot', '=SUBSTITUTE("cAt saT on the mat", "at", "ot")');
206
+		$this->test_simple_formula('cot sot on the mot', '=SUBSTITUTE("c.*t s.*t on the m.*t", ".*t", "ot")');
207
+	}
208
+
209
+	public function test_func_sum() {
210
+		$this->test_simple_formula(15, '=SUM(1, 2, 3, 4, 5)');
211
+	}
212
+
213
+	public function test_func_upper() {
214
+		$this->test_simple_formula('MIXED', '=UPPER("mIxEd")');
215
+	}
216
+
217
+	public function test_func_xor() {
218
+		$this->test_simple_formula(false, '=XOR(FALSE, FALSE)');
219
+		$this->test_simple_formula(true, '=XOR(FALSE, TRUE)');
220
+		$this->test_simple_formula(true, '=XOR(TRUE, FALSE)');
221
+		$this->test_simple_formula(false, '=XOR(TRUE, TRUE)');
222
+		$this->test_simple_formula(true, '=XOR(FALSE, FALSE, TRUE)');
223
+		$this->test_simple_formula(false, '=XOR(TRUE, FALSE, TRUE)');
224
+	}
225
+
226
+	public function test_format() {
227
+		$this->test_simple_formula(new CellValue('2.718', 2.718281828459045, 'number', 3), '=2.718281828459045 ; number 3');
228
+		$this->test_simple_formula(new CellValue('271.83%', 2.718281828459045, 'percent', 2), '=2.718281828459045 ; percent 2');
229
+		$this->test_simple_formula(new CellValue('$2.72', 2.718281828459045, 'currency', 2), '=2.718281828459045 ; currency 2');
230
+	}
231
+}
232
+?>

+ 77
- 0
phptest/spreadsheet/SpreadsheetMarkdownIntegrationTests.php Vedi File

@@ -0,0 +1,77 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+use PHPUnit\Framework\TestCase;
5
+
6
+require_once __DIR__ . '/../../php/markdown.php';
7
+require_once __DIR__ . '/../../php/spreadsheet.php';
8
+
9
+final class SpreadsheetMarkdownIntegrationTests extends TestCase {
10
+	private ?Markdown $parser;
11
+
12
+	protected function setUp(): void {
13
+		$this->parser = new Markdown(array_merge(Markdown::allReaders(), [ new MDSpreadsheetReader() ]));
14
+	}
15
+
16
+	private function md(string $markdown): string {
17
+		return $this->normalizeWhitespace($this->parser->toHTML($markdown));
18
+	}
19
+
20
+	private function normalizeWhitespace(string $str): string {
21
+		$str = mb_eregi_replace('\\s+', ' ', $str);
22
+		$str = mb_eregi_replace('>\\s+<', '><', $str);
23
+		return trim($str);
24
+	}
25
+
26
+	public function test_integration() {
27
+		$markdown = "| A | B | C |\n| --- | --- | --- |\n| 3 | 4 | =A*B |";
28
+		$expected = '<table><thead>' .
29
+			'<tr><th>A</th><th>B</th><th>C</th></tr>' .
30
+			'</thead><tbody><tr>' .
31
+			'<td class="spreadsheet-type-number" data-numeric-value="3" data-string-value="3">3</td>' .
32
+			'<td class="spreadsheet-type-number" data-numeric-value="4" data-string-value="4">4</td>' .
33
+			'<td class="calculated spreadsheet-type-number" data-numeric-value="12" data-string-value="12">12</td>' .
34
+			'</tr></tbody></table>';
35
+		$actual = $this->md($markdown);
36
+		$this->assertSame($expected, $actual);
37
+	}
38
+
39
+	public function test_bailOut() {
40
+		// If no formulas found table isn't modified
41
+		$markdown = "| A | B | C |\n| --- | --- | --- |\n| 3 | 4 | A*B |";
42
+		$expected = '<table><thead>' .
43
+			'<tr><th>A</th><th>B</th><th>C</th></tr>' .
44
+			'</thead><tbody><tr>' .
45
+			'<td>3</td>' .
46
+			'<td>4</td>' .
47
+			'<td>A*B</td>' .
48
+			'</tr></tbody></table>';
49
+		$actual = $this->md($markdown);
50
+		$this->assertSame($expected, $actual);
51
+	}
52
+
53
+	public function test_observedError1() {
54
+		// Saw "Uncaught Error: columnIndex must be number, got string" from this
55
+		$markdown = "| Unit Price | Qty | Subtotal |\n| ---: | ---: | ---: |\n| $1.23 | 2 | =A*B FILL |\n| $4.99 | 6 | |\n| Total | | =SUM(C:C) |";
56
+		$expected = '<table><thead><tr>' .
57
+			'<th style="text-align: right;">Unit Price</th>' .
58
+			'<th style="text-align: right;">Qty</th>' .
59
+			'<th>Subtotal</th>' .
60
+			'</tr></thead><tbody><tr>' .
61
+			'<td class="spreadsheet-type-currency" style="text-align: right;" data-numeric-value="1.23" data-string-value="1.23">$1.23</td>' .
62
+			'<td class="spreadsheet-type-number" style="text-align: right;" data-numeric-value="2" data-string-value="2">2</td>' .
63
+			'<td class="calculated spreadsheet-type-currency" data-numeric-value="2.46" data-string-value="2.46">$2.46</td>' .
64
+			'</tr><tr>' .
65
+			'<td class="spreadsheet-type-currency" style="text-align: right;" data-numeric-value="4.99" data-string-value="4.99">$4.99</td>' .
66
+			'<td class="spreadsheet-type-number" style="text-align: right;" data-numeric-value="6" data-string-value="6">6</td>' .
67
+			'<td class="calculated spreadsheet-type-currency" data-numeric-value="29.94" data-string-value="29.94">$29.94</td>' .
68
+			'</tr><tr>' .
69
+			'<td class="spreadsheet-type-string" style="text-align: right;" data-string-value="Total">Total</td>' .
70
+			'<td class="spreadsheet-type-blank" style="text-align: right;" data-numeric-value="0" data-string-value=""></td>' .
71
+			'<td class="calculated spreadsheet-type-currency" data-numeric-value="32.4" data-string-value="32.4">$32.40</td>' .
72
+			'</tr></tbody></table>';
73
+		$actual = $this->md($markdown);
74
+		$this->assertSame($expected, $actual);
75
+	}
76
+}
77
+?>

+ 4
- 0
runphptests.sh Vedi File

@@ -4,3 +4,7 @@ php lib/phpunit.phar --display-warnings --display-notices \
4 4
 	phptest/TokenTests.php \
5 5
 	phptest/InlineTests.php \
6 6
 	phptest/BlockTests.php \
7
+	phptest/spreadsheet/CellAddressRangeTests.php \
8
+	phptest/spreadsheet/CellValueTests.php \
9
+	phptest/spreadsheet/ExpressionSetTests.php \
10
+	phptest/spreadsheet/SpreadsheetMarkdownIntegrationTests.php

Loading…
Annulla
Salva