PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / classes / class-evaluate-legacy.php

class-evaluate-legacy.php in TablePress – Tables in WordPress made easy 3.4, at classes/class-evaluate-legacy.php

257 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TablePress Formula Evaluation Legacy Class
4 *
5 * @package TablePress
6 * @subpackage Formulas
7 * @author Tobias Bäthge
8 * @since 1.5.0
9 */
10
11 declare(strict_types=1);
12
13 // Prohibit direct script loading.
14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
15
16 /**
17 * TablePress Formula Evaluation Legacy Class
18 *
19 * Before TablePress 1.5, this was part of the TablePress_Render class.
20 *
21 * @package TablePress
22 * @subpackage Formulas
23 * @author Tobias Bäthge
24 * @since 1.5.0
25 */
26 class TablePress_Evaluate_Legacy {
27
28 /**
29 * Instance of the EvalMath class.
30 *
31 * @since 1.0.0
32 */
33 protected \EvalMath $evalmath;
34
35 /**
36 * Table data in which formulas shall be evaluated.
37 *
38 * @since 1.5.0
39 * @var array<int, array<int, string>>
40 */
41 protected array $table_data;
42
43 /**
44 * Storage for cell ranges that have been replaced in formulas.
45 *
46 * @since 1.0.0
47 * @var array<string, string>
48 */
49 protected array $known_ranges = array();
50
51 /**
52 * Initialize the Formula Evaluation class, include the EvalMath class.
53 *
54 * @since 1.0.0
55 */
56 public function __construct() {
57 $this->evalmath = TablePress::load_class( 'EvalMath', 'evalmath.class.php', 'libraries' );
58 // Don't raise PHP warnings.
59 $this->evalmath->suppress_errors = true;
60 }
61
62 /**
63 * Evaluate formulas in the passed table.
64 *
65 * @since 1.0.0
66 *
67 * @param array<int, array<int, string>> $table_data Table data in which formulas shall be evaluated.
68 * @param string $table_id ID of the passed table.
69 * @return array<int, array<int, string>> Table data with evaluated formulas.
70 */
71 public function evaluate_table_data( array $table_data, string $table_id ): array {
72 $this->table_data = $table_data;
73
74 $num_rows = count( $this->table_data );
75 // Exit early if there's no actual table data (e.g. after using the Row Filter module).
76 if ( 0 === $num_rows ) {
77 return $this->table_data;
78 }
79
80 $num_columns = count( $this->table_data[0] );
81
82 // Make fixed table data available as variables in formulas.
83 $this->evalmath->variables['table_id'] = $table_id;
84 $this->evalmath->variables['num_rows'] = $num_rows;
85 $this->evalmath->variables['num_columns'] = $num_columns;
86
87 // Use two for-loops instead of foreach here to be sure to always work on the "live" table data and not some in-memory copy.
88 for ( $row_idx = 0; $row_idx < $num_rows; $row_idx++ ) {
89 for ( $col_idx = 0; $col_idx < $num_columns; $col_idx++ ) {
90 $this->table_data[ $row_idx ][ $col_idx ] = $this->_evaluate_cell( $this->table_data[ $row_idx ][ $col_idx ], $row_idx, $col_idx );
91 }
92 }
93
94 return $this->table_data;
95 }
96
97 /**
98 * Parse and evaluate the content of a cell.
99 *
100 * @since 1.0.0
101 *
102 * @param string $content Content of a cell.
103 * @param int $row_idx Row index of the cell.
104 * @param int $col_idx Column index of the cell.
105 * @param string[] $parents Optional. List of cells that depend on this cell (to prevent circle references).
106 * @return string Result of the parsing/evaluation.
107 */
108 protected function _evaluate_cell( string $content, int $row_idx, int $col_idx, array $parents = array() ): string {
109 if ( '' === $content || '=' === $content || '=' !== $content[0] ) {
110 return $content;
111 }
112
113 // Cut off the leading =.
114 $content = substr( $content, 1 );
115
116 // Support putting formulas in strings, like =Total: {A3+A4}.
117 $expressions = array();
118 if ( preg_match_all( '#{(.+?)}#', $content, $expressions, PREG_SET_ORDER ) ) {
119 $formula_in_string = true;
120 } else {
121 $formula_in_string = false;
122 // Fill array so that it has the same structure as if it came from preg_match_all().
123 $expressions[] = array( $content, $content );
124 }
125
126 foreach ( $expressions as $expression ) {
127 $orig_expression = $expression[0];
128 $expression = $expression[1];
129
130 $replaced_references = array();
131 $replaced_ranges = array();
132
133 // Remove all whitespace characters.
134 $expression = str_replace( array( "\n", "\r", "\t", ' ' ), '', $expression );
135
136 // Expand cell ranges (like A3:A6) to a list of single cells (like A3,A4,A5,A6).
137 if ( preg_match_all( '#([A-Z]+)([0-9]+):([A-Z]+)([0-9]+)#', $expression, $referenced_cell_ranges, PREG_SET_ORDER ) ) {
138 foreach ( $referenced_cell_ranges as $cell_range ) {
139 if ( in_array( $cell_range[0], $replaced_ranges, true ) ) {
140 continue;
141 }
142
143 $replaced_ranges[] = $cell_range[0];
144
145 if ( isset( $this->known_ranges[ $cell_range[0] ] ) ) {
146 $expression = (string) preg_replace( '#(?<![A-Z])' . preg_quote( $cell_range[0], '#' ) . '(?![0-9])#', $this->known_ranges[ $cell_range[0] ], $expression );
147 continue;
148 }
149
150 // No -1 necessary for this transformation, as we don't actually access the table.
151 $first_col = TablePress::letter_to_number( $cell_range[1] );
152 $first_row = (int) $cell_range[2];
153 $last_col = TablePress::letter_to_number( $cell_range[3] );
154 $last_row = (int) $cell_range[4];
155
156 $col_start = min( $first_col, $last_col );
157 $col_end = max( $first_col, $last_col ) + 1; // +1 for loop below
158 $row_start = min( $first_row, $last_row );
159 $row_end = max( $first_row, $last_row ) + 1; // +1 for loop below
160
161 $cell_list = array();
162 for ( $col = $col_start; $col < $col_end; $col++ ) {
163 for ( $row = $row_start; $row < $row_end; $row++ ) {
164 $column = TablePress::number_to_letter( $col );
165 $cell_list[] = "{$column}{$row}";
166 }
167 }
168 $cell_list = implode( ',', $cell_list );
169
170 $expression = (string) preg_replace( '#(?<![A-Z])' . preg_quote( $cell_range[0], '#' ) . '(?![0-9])#', $cell_list, $expression );
171 $this->known_ranges[ $cell_range[0] ] = $cell_list;
172 }
173 }
174
175 // Parse and evaluate single cell references (like A3 or XY312), while prohibiting circle references.
176 if ( preg_match_all( '#([A-Z]+)([0-9]+)(?![0-9A-Z\(])#', $expression, $referenced_cells, PREG_SET_ORDER ) ) {
177 foreach ( $referenced_cells as $cell_reference ) {
178 if ( in_array( $cell_reference[0], $parents, true ) ) {
179 return '!ERROR! Circle Reference';
180 }
181
182 if ( in_array( $cell_reference[0], $replaced_references, true ) ) {
183 continue;
184 }
185
186 $replaced_references[] = $cell_reference[0];
187
188 $ref_col = TablePress::letter_to_number( $cell_reference[1] ) - 1;
189 $ref_row = (int) $cell_reference[2] - 1;
190
191 if ( ! isset( $this->table_data[ $ref_row ][ $ref_col ] ) ) {
192 return "!ERROR! Cell {$cell_reference[0]} does not exist";
193 }
194
195 $ref_parents = $parents;
196 $ref_parents[] = $cell_reference[0];
197
198 $result = $this->_evaluate_cell( $this->table_data[ $ref_row ][ $ref_col ], $ref_row, $ref_col, $ref_parents );
199 $this->table_data[ $ref_row ][ $ref_col ] = $result;
200 // Bail if there was an error already.
201 if ( str_contains( $result, '!ERROR!' ) ) {
202 return $result;
203 }
204 // Remove all whitespace characters.
205 $result = str_replace( array( "\n", "\r", "\t", ' ' ), '', $result );
206 // Treat empty cells as 0.
207 if ( '' === $result ) {
208 $result = '0';
209 }
210 // Bail if the cell does not result in a number (meaning it was a number or expression before being evaluated).
211 if ( ! is_numeric( $result ) ) {
212 return "!ERROR! {$cell_reference[0]} does not contain a number or expression";
213 }
214
215 $expression = (string) preg_replace( '#(?<![A-Z])' . $cell_reference[0] . '(?![0-9])#', $result, $expression );
216 }
217 }
218
219 $result = $this->_evaluate_math_expression( $expression, $row_idx, $col_idx );
220 // Support putting formulas in strings, like =Total: {A3+A4}.
221 if ( $formula_in_string ) {
222 $content = str_replace( $orig_expression, $result, $content );
223 } else {
224 $content = $result;
225 }
226 }
227
228 return $content;
229 }
230
231 /**
232 * Evaluate a math expression.
233 *
234 * @since 1.0.0
235 *
236 * @param string $expression Math expression without leading = sign.
237 * @param int $row_idx Row index of the cell with the expression.
238 * @param int $col_idx Column index of the cell with the expression.
239 * @return string Result of the evaluation.
240 */
241 protected function _evaluate_math_expression( string $expression, int $row_idx, int $col_idx ): string {
242 // Make current cell's name and row and column number available as variables in formulas.
243 $this->evalmath->variables['row'] = $row_idx + 1;
244 $this->evalmath->variables['column'] = $col_idx + 1;
245 $this->evalmath->variables['cell'] = TablePress::number_to_letter( $this->evalmath->variables['column'] ) . $this->evalmath->variables['row'];
246
247 // Straight up evaluation, without parsing of variable or function assignments (which is why we only need one instance of the object).
248 $result = $this->evalmath->evaluate( $expression );
249 if ( false === $result ) {
250 $result = '!ERROR! ' . $this->evalmath->last_error;
251 }
252
253 return (string) $result;
254 }
255
256 } // class TablePress_Evaluate_Legacy
257