PluginProbe
TablePress – Tables in WordPress made easy / 1.14
TablePress – Tables in WordPress made easy v1.14
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 2.4.4 All 44 releases
tablepress / classes / class-evaluate.php

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

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