Total Complexity | 187 |
Total Lines | 1407 |
Duplicated Lines | 4.41 % |
Coverage | 74.72% |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | class Parser |
||
32 | { |
||
33 | /** Constants */ |
||
34 | // Sheet title in unquoted form |
||
35 | // Invalid sheet title characters cannot occur in the sheet title: |
||
36 | // *:/\?[] |
||
37 | // Moreover, there are valid sheet title characters that cannot occur in unquoted form (there may be more?) |
||
38 | // +-% '^&<>=,;#()"{} |
||
39 | const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+'; |
||
40 | |||
41 | // Sheet title in quoted form (without surrounding quotes) |
||
42 | // Invalid sheet title characters cannot occur in the sheet title: |
||
43 | // *:/\?[] (usual invalid sheet title characters) |
||
44 | // Single quote is represented as a pair '' |
||
45 | const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+'; |
||
46 | |||
47 | /** |
||
48 | * The index of the character we are currently looking at. |
||
49 | * |
||
50 | * @var int |
||
51 | */ |
||
52 | public $currentCharacter; |
||
53 | |||
54 | /** |
||
55 | * The token we are working on. |
||
56 | * |
||
57 | * @var string |
||
58 | */ |
||
59 | public $currentToken; |
||
60 | |||
61 | /** |
||
62 | * The formula to parse. |
||
63 | * |
||
64 | * @var string |
||
65 | */ |
||
66 | private $formula; |
||
67 | |||
68 | /** |
||
69 | * The character ahead of the current char. |
||
70 | * |
||
71 | * @var string |
||
72 | */ |
||
73 | public $lookAhead; |
||
74 | |||
75 | /** |
||
76 | * The parse tree to be generated. |
||
77 | * |
||
78 | * @var string |
||
79 | */ |
||
80 | private $parseTree; |
||
81 | |||
82 | /** |
||
83 | * Array of external sheets. |
||
84 | * |
||
85 | * @var array |
||
86 | */ |
||
87 | private $externalSheets; |
||
88 | |||
89 | /** |
||
90 | * Array of sheet references in the form of REF structures. |
||
91 | * |
||
92 | * @var array |
||
93 | */ |
||
94 | public $references; |
||
95 | |||
96 | /** |
||
97 | * The Excel ptg indices. |
||
98 | * |
||
99 | * @var array |
||
100 | */ |
||
101 | private $ptg = [ |
||
102 | 'ptgExp' => 0x01, |
||
103 | 'ptgTbl' => 0x02, |
||
104 | 'ptgAdd' => 0x03, |
||
105 | 'ptgSub' => 0x04, |
||
106 | 'ptgMul' => 0x05, |
||
107 | 'ptgDiv' => 0x06, |
||
108 | 'ptgPower' => 0x07, |
||
109 | 'ptgConcat' => 0x08, |
||
110 | 'ptgLT' => 0x09, |
||
111 | 'ptgLE' => 0x0A, |
||
112 | 'ptgEQ' => 0x0B, |
||
113 | 'ptgGE' => 0x0C, |
||
114 | 'ptgGT' => 0x0D, |
||
115 | 'ptgNE' => 0x0E, |
||
116 | 'ptgIsect' => 0x0F, |
||
117 | 'ptgUnion' => 0x10, |
||
118 | 'ptgRange' => 0x11, |
||
119 | 'ptgUplus' => 0x12, |
||
120 | 'ptgUminus' => 0x13, |
||
121 | 'ptgPercent' => 0x14, |
||
122 | 'ptgParen' => 0x15, |
||
123 | 'ptgMissArg' => 0x16, |
||
124 | 'ptgStr' => 0x17, |
||
125 | 'ptgAttr' => 0x19, |
||
126 | 'ptgSheet' => 0x1A, |
||
127 | 'ptgEndSheet' => 0x1B, |
||
128 | 'ptgErr' => 0x1C, |
||
129 | 'ptgBool' => 0x1D, |
||
130 | 'ptgInt' => 0x1E, |
||
131 | 'ptgNum' => 0x1F, |
||
132 | 'ptgArray' => 0x20, |
||
133 | 'ptgFunc' => 0x21, |
||
134 | 'ptgFuncVar' => 0x22, |
||
135 | 'ptgName' => 0x23, |
||
136 | 'ptgRef' => 0x24, |
||
137 | 'ptgArea' => 0x25, |
||
138 | 'ptgMemArea' => 0x26, |
||
139 | 'ptgMemErr' => 0x27, |
||
140 | 'ptgMemNoMem' => 0x28, |
||
141 | 'ptgMemFunc' => 0x29, |
||
142 | 'ptgRefErr' => 0x2A, |
||
143 | 'ptgAreaErr' => 0x2B, |
||
144 | 'ptgRefN' => 0x2C, |
||
145 | 'ptgAreaN' => 0x2D, |
||
146 | 'ptgMemAreaN' => 0x2E, |
||
147 | 'ptgMemNoMemN' => 0x2F, |
||
148 | 'ptgNameX' => 0x39, |
||
149 | 'ptgRef3d' => 0x3A, |
||
150 | 'ptgArea3d' => 0x3B, |
||
151 | 'ptgRefErr3d' => 0x3C, |
||
152 | 'ptgAreaErr3d' => 0x3D, |
||
153 | 'ptgArrayV' => 0x40, |
||
154 | 'ptgFuncV' => 0x41, |
||
155 | 'ptgFuncVarV' => 0x42, |
||
156 | 'ptgNameV' => 0x43, |
||
157 | 'ptgRefV' => 0x44, |
||
158 | 'ptgAreaV' => 0x45, |
||
159 | 'ptgMemAreaV' => 0x46, |
||
160 | 'ptgMemErrV' => 0x47, |
||
161 | 'ptgMemNoMemV' => 0x48, |
||
162 | 'ptgMemFuncV' => 0x49, |
||
163 | 'ptgRefErrV' => 0x4A, |
||
164 | 'ptgAreaErrV' => 0x4B, |
||
165 | 'ptgRefNV' => 0x4C, |
||
166 | 'ptgAreaNV' => 0x4D, |
||
167 | 'ptgMemAreaNV' => 0x4E, |
||
168 | 'ptgMemNoMemNV' => 0x4F, |
||
169 | 'ptgFuncCEV' => 0x58, |
||
170 | 'ptgNameXV' => 0x59, |
||
171 | 'ptgRef3dV' => 0x5A, |
||
172 | 'ptgArea3dV' => 0x5B, |
||
173 | 'ptgRefErr3dV' => 0x5C, |
||
174 | 'ptgAreaErr3dV' => 0x5D, |
||
175 | 'ptgArrayA' => 0x60, |
||
176 | 'ptgFuncA' => 0x61, |
||
177 | 'ptgFuncVarA' => 0x62, |
||
178 | 'ptgNameA' => 0x63, |
||
179 | 'ptgRefA' => 0x64, |
||
180 | 'ptgAreaA' => 0x65, |
||
181 | 'ptgMemAreaA' => 0x66, |
||
182 | 'ptgMemErrA' => 0x67, |
||
183 | 'ptgMemNoMemA' => 0x68, |
||
184 | 'ptgMemFuncA' => 0x69, |
||
185 | 'ptgRefErrA' => 0x6A, |
||
186 | 'ptgAreaErrA' => 0x6B, |
||
187 | 'ptgRefNA' => 0x6C, |
||
188 | 'ptgAreaNA' => 0x6D, |
||
189 | 'ptgMemAreaNA' => 0x6E, |
||
190 | 'ptgMemNoMemNA' => 0x6F, |
||
191 | 'ptgFuncCEA' => 0x78, |
||
192 | 'ptgNameXA' => 0x79, |
||
193 | 'ptgRef3dA' => 0x7A, |
||
194 | 'ptgArea3dA' => 0x7B, |
||
195 | 'ptgRefErr3dA' => 0x7C, |
||
196 | 'ptgAreaErr3dA' => 0x7D, |
||
197 | ]; |
||
198 | |||
199 | /** |
||
200 | * Thanks to Michael Meeks and Gnumeric for the initial arg values. |
||
201 | * |
||
202 | * The following hash was generated by "function_locale.pl" in the distro. |
||
203 | * Refer to function_locale.pl for non-English function names. |
||
204 | * |
||
205 | * The array elements are as follow: |
||
206 | * ptg: The Excel function ptg code. |
||
207 | * args: The number of arguments that the function takes: |
||
208 | * >=0 is a fixed number of arguments. |
||
209 | * -1 is a variable number of arguments. |
||
210 | * class: The reference, value or array class of the function args. |
||
211 | * vol: The function is volatile. |
||
212 | * |
||
213 | * @var array |
||
214 | */ |
||
215 | private $functions = [ |
||
216 | // function ptg args class vol |
||
217 | 'COUNT' => [0, -1, 0, 0], |
||
218 | 'IF' => [1, -1, 1, 0], |
||
219 | 'ISNA' => [2, 1, 1, 0], |
||
220 | 'ISERROR' => [3, 1, 1, 0], |
||
221 | 'SUM' => [4, -1, 0, 0], |
||
222 | 'AVERAGE' => [5, -1, 0, 0], |
||
223 | 'MIN' => [6, -1, 0, 0], |
||
224 | 'MAX' => [7, -1, 0, 0], |
||
225 | 'ROW' => [8, -1, 0, 0], |
||
226 | 'COLUMN' => [9, -1, 0, 0], |
||
227 | 'NA' => [10, 0, 0, 0], |
||
228 | 'NPV' => [11, -1, 1, 0], |
||
229 | 'STDEV' => [12, -1, 0, 0], |
||
230 | 'DOLLAR' => [13, -1, 1, 0], |
||
231 | 'FIXED' => [14, -1, 1, 0], |
||
232 | 'SIN' => [15, 1, 1, 0], |
||
233 | 'COS' => [16, 1, 1, 0], |
||
234 | 'TAN' => [17, 1, 1, 0], |
||
235 | 'ATAN' => [18, 1, 1, 0], |
||
236 | 'PI' => [19, 0, 1, 0], |
||
237 | 'SQRT' => [20, 1, 1, 0], |
||
238 | 'EXP' => [21, 1, 1, 0], |
||
239 | 'LN' => [22, 1, 1, 0], |
||
240 | 'LOG10' => [23, 1, 1, 0], |
||
241 | 'ABS' => [24, 1, 1, 0], |
||
242 | 'INT' => [25, 1, 1, 0], |
||
243 | 'SIGN' => [26, 1, 1, 0], |
||
244 | 'ROUND' => [27, 2, 1, 0], |
||
245 | 'LOOKUP' => [28, -1, 0, 0], |
||
246 | 'INDEX' => [29, -1, 0, 1], |
||
247 | 'REPT' => [30, 2, 1, 0], |
||
248 | 'MID' => [31, 3, 1, 0], |
||
249 | 'LEN' => [32, 1, 1, 0], |
||
250 | 'VALUE' => [33, 1, 1, 0], |
||
251 | 'TRUE' => [34, 0, 1, 0], |
||
252 | 'FALSE' => [35, 0, 1, 0], |
||
253 | 'AND' => [36, -1, 0, 0], |
||
254 | 'OR' => [37, -1, 0, 0], |
||
255 | 'NOT' => [38, 1, 1, 0], |
||
256 | 'MOD' => [39, 2, 1, 0], |
||
257 | 'DCOUNT' => [40, 3, 0, 0], |
||
258 | 'DSUM' => [41, 3, 0, 0], |
||
259 | 'DAVERAGE' => [42, 3, 0, 0], |
||
260 | 'DMIN' => [43, 3, 0, 0], |
||
261 | 'DMAX' => [44, 3, 0, 0], |
||
262 | 'DSTDEV' => [45, 3, 0, 0], |
||
263 | 'VAR' => [46, -1, 0, 0], |
||
264 | 'DVAR' => [47, 3, 0, 0], |
||
265 | 'TEXT' => [48, 2, 1, 0], |
||
266 | 'LINEST' => [49, -1, 0, 0], |
||
267 | 'TREND' => [50, -1, 0, 0], |
||
268 | 'LOGEST' => [51, -1, 0, 0], |
||
269 | 'GROWTH' => [52, -1, 0, 0], |
||
270 | 'PV' => [56, -1, 1, 0], |
||
271 | 'FV' => [57, -1, 1, 0], |
||
272 | 'NPER' => [58, -1, 1, 0], |
||
273 | 'PMT' => [59, -1, 1, 0], |
||
274 | 'RATE' => [60, -1, 1, 0], |
||
275 | 'MIRR' => [61, 3, 0, 0], |
||
276 | 'IRR' => [62, -1, 0, 0], |
||
277 | 'RAND' => [63, 0, 1, 1], |
||
278 | 'MATCH' => [64, -1, 0, 0], |
||
279 | 'DATE' => [65, 3, 1, 0], |
||
280 | 'TIME' => [66, 3, 1, 0], |
||
281 | 'DAY' => [67, 1, 1, 0], |
||
282 | 'MONTH' => [68, 1, 1, 0], |
||
283 | 'YEAR' => [69, 1, 1, 0], |
||
284 | 'WEEKDAY' => [70, -1, 1, 0], |
||
285 | 'HOUR' => [71, 1, 1, 0], |
||
286 | 'MINUTE' => [72, 1, 1, 0], |
||
287 | 'SECOND' => [73, 1, 1, 0], |
||
288 | 'NOW' => [74, 0, 1, 1], |
||
289 | 'AREAS' => [75, 1, 0, 1], |
||
290 | 'ROWS' => [76, 1, 0, 1], |
||
291 | 'COLUMNS' => [77, 1, 0, 1], |
||
292 | 'OFFSET' => [78, -1, 0, 1], |
||
293 | 'SEARCH' => [82, -1, 1, 0], |
||
294 | 'TRANSPOSE' => [83, 1, 1, 0], |
||
295 | 'TYPE' => [86, 1, 1, 0], |
||
296 | 'ATAN2' => [97, 2, 1, 0], |
||
297 | 'ASIN' => [98, 1, 1, 0], |
||
298 | 'ACOS' => [99, 1, 1, 0], |
||
299 | 'CHOOSE' => [100, -1, 1, 0], |
||
300 | 'HLOOKUP' => [101, -1, 0, 0], |
||
301 | 'VLOOKUP' => [102, -1, 0, 0], |
||
302 | 'ISREF' => [105, 1, 0, 0], |
||
303 | 'LOG' => [109, -1, 1, 0], |
||
304 | 'CHAR' => [111, 1, 1, 0], |
||
305 | 'LOWER' => [112, 1, 1, 0], |
||
306 | 'UPPER' => [113, 1, 1, 0], |
||
307 | 'PROPER' => [114, 1, 1, 0], |
||
308 | 'LEFT' => [115, -1, 1, 0], |
||
309 | 'RIGHT' => [116, -1, 1, 0], |
||
310 | 'EXACT' => [117, 2, 1, 0], |
||
311 | 'TRIM' => [118, 1, 1, 0], |
||
312 | 'REPLACE' => [119, 4, 1, 0], |
||
313 | 'SUBSTITUTE' => [120, -1, 1, 0], |
||
314 | 'CODE' => [121, 1, 1, 0], |
||
315 | 'FIND' => [124, -1, 1, 0], |
||
316 | 'CELL' => [125, -1, 0, 1], |
||
317 | 'ISERR' => [126, 1, 1, 0], |
||
318 | 'ISTEXT' => [127, 1, 1, 0], |
||
319 | 'ISNUMBER' => [128, 1, 1, 0], |
||
320 | 'ISBLANK' => [129, 1, 1, 0], |
||
321 | 'T' => [130, 1, 0, 0], |
||
322 | 'N' => [131, 1, 0, 0], |
||
323 | 'DATEVALUE' => [140, 1, 1, 0], |
||
324 | 'TIMEVALUE' => [141, 1, 1, 0], |
||
325 | 'SLN' => [142, 3, 1, 0], |
||
326 | 'SYD' => [143, 4, 1, 0], |
||
327 | 'DDB' => [144, -1, 1, 0], |
||
328 | 'INDIRECT' => [148, -1, 1, 1], |
||
329 | 'CALL' => [150, -1, 1, 0], |
||
330 | 'CLEAN' => [162, 1, 1, 0], |
||
331 | 'MDETERM' => [163, 1, 2, 0], |
||
332 | 'MINVERSE' => [164, 1, 2, 0], |
||
333 | 'MMULT' => [165, 2, 2, 0], |
||
334 | 'IPMT' => [167, -1, 1, 0], |
||
335 | 'PPMT' => [168, -1, 1, 0], |
||
336 | 'COUNTA' => [169, -1, 0, 0], |
||
337 | 'PRODUCT' => [183, -1, 0, 0], |
||
338 | 'FACT' => [184, 1, 1, 0], |
||
339 | 'DPRODUCT' => [189, 3, 0, 0], |
||
340 | 'ISNONTEXT' => [190, 1, 1, 0], |
||
341 | 'STDEVP' => [193, -1, 0, 0], |
||
342 | 'VARP' => [194, -1, 0, 0], |
||
343 | 'DSTDEVP' => [195, 3, 0, 0], |
||
344 | 'DVARP' => [196, 3, 0, 0], |
||
345 | 'TRUNC' => [197, -1, 1, 0], |
||
346 | 'ISLOGICAL' => [198, 1, 1, 0], |
||
347 | 'DCOUNTA' => [199, 3, 0, 0], |
||
348 | 'USDOLLAR' => [204, -1, 1, 0], |
||
349 | 'FINDB' => [205, -1, 1, 0], |
||
350 | 'SEARCHB' => [206, -1, 1, 0], |
||
351 | 'REPLACEB' => [207, 4, 1, 0], |
||
352 | 'LEFTB' => [208, -1, 1, 0], |
||
353 | 'RIGHTB' => [209, -1, 1, 0], |
||
354 | 'MIDB' => [210, 3, 1, 0], |
||
355 | 'LENB' => [211, 1, 1, 0], |
||
356 | 'ROUNDUP' => [212, 2, 1, 0], |
||
357 | 'ROUNDDOWN' => [213, 2, 1, 0], |
||
358 | 'ASC' => [214, 1, 1, 0], |
||
359 | 'DBCS' => [215, 1, 1, 0], |
||
360 | 'RANK' => [216, -1, 0, 0], |
||
361 | 'ADDRESS' => [219, -1, 1, 0], |
||
362 | 'DAYS360' => [220, -1, 1, 0], |
||
363 | 'TODAY' => [221, 0, 1, 1], |
||
364 | 'VDB' => [222, -1, 1, 0], |
||
365 | 'MEDIAN' => [227, -1, 0, 0], |
||
366 | 'SUMPRODUCT' => [228, -1, 2, 0], |
||
367 | 'SINH' => [229, 1, 1, 0], |
||
368 | 'COSH' => [230, 1, 1, 0], |
||
369 | 'TANH' => [231, 1, 1, 0], |
||
370 | 'ASINH' => [232, 1, 1, 0], |
||
371 | 'ACOSH' => [233, 1, 1, 0], |
||
372 | 'ATANH' => [234, 1, 1, 0], |
||
373 | 'DGET' => [235, 3, 0, 0], |
||
374 | 'INFO' => [244, 1, 1, 1], |
||
375 | 'DB' => [247, -1, 1, 0], |
||
376 | 'FREQUENCY' => [252, 2, 0, 0], |
||
377 | 'ERROR.TYPE' => [261, 1, 1, 0], |
||
378 | 'REGISTER.ID' => [267, -1, 1, 0], |
||
379 | 'AVEDEV' => [269, -1, 0, 0], |
||
380 | 'BETADIST' => [270, -1, 1, 0], |
||
381 | 'GAMMALN' => [271, 1, 1, 0], |
||
382 | 'BETAINV' => [272, -1, 1, 0], |
||
383 | 'BINOMDIST' => [273, 4, 1, 0], |
||
384 | 'CHIDIST' => [274, 2, 1, 0], |
||
385 | 'CHIINV' => [275, 2, 1, 0], |
||
386 | 'COMBIN' => [276, 2, 1, 0], |
||
387 | 'CONFIDENCE' => [277, 3, 1, 0], |
||
388 | 'CRITBINOM' => [278, 3, 1, 0], |
||
389 | 'EVEN' => [279, 1, 1, 0], |
||
390 | 'EXPONDIST' => [280, 3, 1, 0], |
||
391 | 'FDIST' => [281, 3, 1, 0], |
||
392 | 'FINV' => [282, 3, 1, 0], |
||
393 | 'FISHER' => [283, 1, 1, 0], |
||
394 | 'FISHERINV' => [284, 1, 1, 0], |
||
395 | 'FLOOR' => [285, 2, 1, 0], |
||
396 | 'GAMMADIST' => [286, 4, 1, 0], |
||
397 | 'GAMMAINV' => [287, 3, 1, 0], |
||
398 | 'CEILING' => [288, 2, 1, 0], |
||
399 | 'HYPGEOMDIST' => [289, 4, 1, 0], |
||
400 | 'LOGNORMDIST' => [290, 3, 1, 0], |
||
401 | 'LOGINV' => [291, 3, 1, 0], |
||
402 | 'NEGBINOMDIST' => [292, 3, 1, 0], |
||
403 | 'NORMDIST' => [293, 4, 1, 0], |
||
404 | 'NORMSDIST' => [294, 1, 1, 0], |
||
405 | 'NORMINV' => [295, 3, 1, 0], |
||
406 | 'NORMSINV' => [296, 1, 1, 0], |
||
407 | 'STANDARDIZE' => [297, 3, 1, 0], |
||
408 | 'ODD' => [298, 1, 1, 0], |
||
409 | 'PERMUT' => [299, 2, 1, 0], |
||
410 | 'POISSON' => [300, 3, 1, 0], |
||
411 | 'TDIST' => [301, 3, 1, 0], |
||
412 | 'WEIBULL' => [302, 4, 1, 0], |
||
413 | 'SUMXMY2' => [303, 2, 2, 0], |
||
414 | 'SUMX2MY2' => [304, 2, 2, 0], |
||
415 | 'SUMX2PY2' => [305, 2, 2, 0], |
||
416 | 'CHITEST' => [306, 2, 2, 0], |
||
417 | 'CORREL' => [307, 2, 2, 0], |
||
418 | 'COVAR' => [308, 2, 2, 0], |
||
419 | 'FORECAST' => [309, 3, 2, 0], |
||
420 | 'FTEST' => [310, 2, 2, 0], |
||
421 | 'INTERCEPT' => [311, 2, 2, 0], |
||
422 | 'PEARSON' => [312, 2, 2, 0], |
||
423 | 'RSQ' => [313, 2, 2, 0], |
||
424 | 'STEYX' => [314, 2, 2, 0], |
||
425 | 'SLOPE' => [315, 2, 2, 0], |
||
426 | 'TTEST' => [316, 4, 2, 0], |
||
427 | 'PROB' => [317, -1, 2, 0], |
||
428 | 'DEVSQ' => [318, -1, 0, 0], |
||
429 | 'GEOMEAN' => [319, -1, 0, 0], |
||
430 | 'HARMEAN' => [320, -1, 0, 0], |
||
431 | 'SUMSQ' => [321, -1, 0, 0], |
||
432 | 'KURT' => [322, -1, 0, 0], |
||
433 | 'SKEW' => [323, -1, 0, 0], |
||
434 | 'ZTEST' => [324, -1, 0, 0], |
||
435 | 'LARGE' => [325, 2, 0, 0], |
||
436 | 'SMALL' => [326, 2, 0, 0], |
||
437 | 'QUARTILE' => [327, 2, 0, 0], |
||
438 | 'PERCENTILE' => [328, 2, 0, 0], |
||
439 | 'PERCENTRANK' => [329, -1, 0, 0], |
||
440 | 'MODE' => [330, -1, 2, 0], |
||
441 | 'TRIMMEAN' => [331, 2, 0, 0], |
||
442 | 'TINV' => [332, 2, 1, 0], |
||
443 | 'CONCATENATE' => [336, -1, 1, 0], |
||
444 | 'POWER' => [337, 2, 1, 0], |
||
445 | 'RADIANS' => [342, 1, 1, 0], |
||
446 | 'DEGREES' => [343, 1, 1, 0], |
||
447 | 'SUBTOTAL' => [344, -1, 0, 0], |
||
448 | 'SUMIF' => [345, -1, 0, 0], |
||
449 | 'COUNTIF' => [346, 2, 0, 0], |
||
450 | 'COUNTBLANK' => [347, 1, 0, 0], |
||
451 | 'ISPMT' => [350, 4, 1, 0], |
||
452 | 'DATEDIF' => [351, 3, 1, 0], |
||
453 | 'DATESTRING' => [352, 1, 1, 0], |
||
454 | 'NUMBERSTRING' => [353, 2, 1, 0], |
||
455 | 'ROMAN' => [354, -1, 1, 0], |
||
456 | 'GETPIVOTDATA' => [358, -1, 0, 0], |
||
457 | 'HYPERLINK' => [359, -1, 1, 0], |
||
458 | 'PHONETIC' => [360, 1, 0, 0], |
||
459 | 'AVERAGEA' => [361, -1, 0, 0], |
||
460 | 'MAXA' => [362, -1, 0, 0], |
||
461 | 'MINA' => [363, -1, 0, 0], |
||
462 | 'STDEVPA' => [364, -1, 0, 0], |
||
463 | 'VARPA' => [365, -1, 0, 0], |
||
464 | 'STDEVA' => [366, -1, 0, 0], |
||
465 | 'VARA' => [367, -1, 0, 0], |
||
466 | 'BAHTTEXT' => [368, 1, 0, 0], |
||
467 | ]; |
||
468 | |||
469 | /** |
||
470 | * The class constructor. |
||
471 | */ |
||
472 | 49 | public function __construct() |
|
473 | { |
||
474 | 49 | $this->currentCharacter = 0; |
|
475 | 49 | $this->currentToken = ''; // The token we are working on. |
|
476 | 49 | $this->formula = ''; // The formula to parse. |
|
477 | 49 | $this->lookAhead = ''; // The character ahead of the current char. |
|
478 | 49 | $this->parseTree = ''; // The parse tree to be generated. |
|
479 | 49 | $this->externalSheets = []; |
|
480 | 49 | $this->references = []; |
|
481 | 49 | } |
|
482 | |||
483 | /** |
||
484 | * Convert a token to the proper ptg value. |
||
485 | * |
||
486 | * @param mixed $token the token to convert |
||
487 | * |
||
488 | * @return mixed the converted token on success |
||
489 | */ |
||
490 | 18 | private function convert($token) |
|
491 | { |
||
492 | 18 | if (preg_match('/"([^"]|""){0,255}"/', $token)) { |
|
493 | 9 | return $this->convertString($token); |
|
494 | 18 | } elseif (is_numeric($token)) { |
|
495 | 15 | return $this->convertNumber($token); |
|
496 | // match references like A1 or $A$1 |
||
497 | 18 | } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { |
|
498 | 12 | return $this->convertRef2d($token); |
|
499 | // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 |
||
500 | 18 | } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\d+)$/u", $token)) { |
|
501 | return $this->convertRef3d($token); |
||
502 | // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 |
||
503 | 18 | } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\d+)$/u", $token)) { |
|
504 | 1 | return $this->convertRef3d($token); |
|
505 | // match ranges like A1:B2 or $A$1:$B$2 |
||
506 | 18 | } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { |
|
507 | 15 | return $this->convertRange2d($token); |
|
508 | // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 |
||
509 | 17 | } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\d+)\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\d+)$/u", $token)) { |
|
510 | return $this->convertRange3d($token); |
||
511 | // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 |
||
512 | 17 | } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\d+)\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\d+)$/u", $token)) { |
|
513 | 3 | return $this->convertRange3d($token); |
|
514 | // operators (including parentheses) |
||
515 | 16 | } elseif (isset($this->ptg[$token])) { |
|
516 | 12 | return pack('C', $this->ptg[$token]); |
|
517 | // match error codes |
||
518 | 15 | } elseif (preg_match("/^#[A-Z0\/]{3,5}[!?]{1}$/", $token) or $token == '#N/A') { |
|
519 | return $this->convertError($token); |
||
520 | // commented so argument number can be processed correctly. See toReversePolish(). |
||
521 | /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) |
||
522 | { |
||
523 | return($this->convertFunction($token, $this->_func_args)); |
||
524 | }*/ |
||
525 | // if it's an argument, ignore the token (the argument remains) |
||
526 | 15 | } elseif ($token == 'arg') { |
|
527 | 15 | return ''; |
|
528 | } |
||
529 | |||
530 | // TODO: use real error codes |
||
531 | throw new WriterException("Unknown token $token"); |
||
532 | } |
||
533 | |||
534 | /** |
||
535 | * Convert a number token to ptgInt or ptgNum. |
||
536 | * |
||
537 | * @param mixed $num an integer or double for conversion to its ptg value |
||
538 | */ |
||
539 | 15 | private function convertNumber($num) |
|
540 | { |
||
541 | // Integer in the range 0..2**16-1 |
||
542 | 15 | if ((preg_match("/^\d+$/", $num)) and ($num <= 65535)) { |
|
543 | 15 | return pack('Cv', $this->ptg['ptgInt'], $num); |
|
544 | } |
||
545 | |||
546 | // A float |
||
547 | 6 | if (BIFFwriter::getByteOrder()) { // if it's Big Endian |
|
548 | $num = strrev($num); |
||
549 | } |
||
550 | |||
551 | 6 | return pack('Cd', $this->ptg['ptgNum'], $num); |
|
552 | } |
||
553 | |||
554 | /** |
||
555 | * Convert a string token to ptgStr. |
||
556 | * |
||
557 | * @param string $string a string for conversion to its ptg value |
||
558 | * |
||
559 | * @return mixed the converted token on success |
||
560 | */ |
||
561 | 9 | private function convertString($string) |
|
562 | { |
||
563 | // chop away beggining and ending quotes |
||
564 | 9 | $string = substr($string, 1, strlen($string) - 2); |
|
565 | 9 | if (strlen($string) > 255) { |
|
1 ignored issue
–
show
|
|||
566 | throw new WriterException('String is too long'); |
||
567 | } |
||
568 | |||
569 | 9 | return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string); |
|
1 ignored issue
–
show
|
|||
570 | } |
||
571 | |||
572 | /** |
||
573 | * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of |
||
574 | * args that it takes. |
||
575 | * |
||
576 | * @param string $token the name of the function for convertion to ptg value |
||
577 | * @param int $num_args the number of arguments the function receives |
||
578 | * |
||
579 | * @return string The packed ptg for the function |
||
580 | */ |
||
581 | 15 | private function convertFunction($token, $num_args) |
|
582 | { |
||
583 | 15 | $args = $this->functions[$token][1]; |
|
584 | |||
585 | // Fixed number of args eg. TIME($i, $j, $k). |
||
586 | 15 | if ($args >= 0) { |
|
587 | 4 | return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]); |
|
588 | } |
||
589 | // Variable number of args eg. SUM($i, $j, $k, ..). |
||
590 | 15 | if ($args == -1) { |
|
591 | 15 | return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); |
|
592 | } |
||
593 | } |
||
594 | |||
595 | /** |
||
596 | * Convert an Excel range such as A1:D4 to a ptgRefV. |
||
597 | * |
||
598 | * @param string $range An Excel range in the A1:A2 |
||
599 | * @param int $class |
||
600 | */ |
||
601 | 15 | private function convertRange2d($range, $class = 0) |
|
602 | { |
||
603 | // TODO: possible class value 0,1,2 check Formula.pm |
||
604 | // Split the range into 2 cell refs |
||
605 | 15 | if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { |
|
606 | 15 | list($cell1, $cell2) = explode(':', $range); |
|
607 | } else { |
||
608 | // TODO: use real error codes |
||
609 | throw new WriterException('Unknown range separator'); |
||
610 | } |
||
611 | |||
612 | // Convert the cell references |
||
613 | 15 | list($row1, $col1) = $this->cellToPackedRowcol($cell1); |
|
614 | 15 | list($row2, $col2) = $this->cellToPackedRowcol($cell2); |
|
615 | |||
616 | // The ptg value depends on the class of the ptg. |
||
617 | 15 | if ($class == 0) { |
|
618 | 15 | $ptgArea = pack('C', $this->ptg['ptgArea']); |
|
619 | } elseif ($class == 1) { |
||
620 | $ptgArea = pack('C', $this->ptg['ptgAreaV']); |
||
621 | } elseif ($class == 2) { |
||
622 | $ptgArea = pack('C', $this->ptg['ptgAreaA']); |
||
623 | } else { |
||
624 | // TODO: use real error codes |
||
625 | throw new WriterException("Unknown class $class"); |
||
626 | } |
||
627 | |||
628 | 15 | return $ptgArea . $row1 . $row2 . $col1 . $col2; |
|
629 | } |
||
630 | |||
631 | /** |
||
632 | * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to |
||
633 | * a ptgArea3d. |
||
634 | * |
||
635 | * @param string $token an Excel range in the Sheet1!A1:A2 format |
||
636 | * |
||
637 | * @return mixed the packed ptgArea3d token on success |
||
638 | */ |
||
639 | 3 | private function convertRange3d($token) |
|
640 | { |
||
641 | // Split the ref at the ! symbol |
||
642 | 3 | list($ext_ref, $range) = explode('!', $token); |
|
643 | |||
644 | // Convert the external reference part (different for BIFF8) |
||
645 | 3 | $ext_ref = $this->getRefIndex($ext_ref); |
|
646 | |||
647 | // Split the range into 2 cell refs |
||
648 | 3 | list($cell1, $cell2) = explode(':', $range); |
|
649 | |||
650 | // Convert the cell references |
||
651 | 3 | if (preg_match("/^(\\$)?[A-Ia-i]?[A-Za-z](\\$)?(\d+)$/", $cell1)) { |
|
652 | 3 | list($row1, $col1) = $this->cellToPackedRowcol($cell1); |
|
653 | 3 | list($row2, $col2) = $this->cellToPackedRowcol($cell2); |
|
654 | } else { // It's a rows range (like 26:27) |
||
655 | list($row1, $col1, $row2, $col2) = $this->rangeToPackedRange($cell1 . ':' . $cell2); |
||
656 | } |
||
657 | |||
658 | // The ptg value depends on the class of the ptg. |
||
659 | 3 | $ptgArea = pack('C', $this->ptg['ptgArea3d']); |
|
660 | |||
661 | 3 | return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2; |
|
662 | } |
||
663 | |||
664 | /** |
||
665 | * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV. |
||
666 | * |
||
667 | * @param string $cell An Excel cell reference |
||
668 | * |
||
669 | * @return string The cell in packed() format with the corresponding ptg |
||
670 | */ |
||
671 | 12 | private function convertRef2d($cell) |
|
672 | { |
||
673 | // Convert the cell reference |
||
674 | 12 | $cell_array = $this->cellToPackedRowcol($cell); |
|
675 | 12 | list($row, $col) = $cell_array; |
|
676 | |||
677 | // The ptg value depends on the class of the ptg. |
||
678 | 12 | $ptgRef = pack('C', $this->ptg['ptgRefA']); |
|
679 | |||
680 | 12 | return $ptgRef . $row . $col; |
|
681 | } |
||
682 | |||
683 | /** |
||
684 | * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a |
||
685 | * ptgRef3d. |
||
686 | * |
||
687 | * @param string $cell An Excel cell reference |
||
688 | * |
||
689 | * @return mixed the packed ptgRef3d token on success |
||
690 | */ |
||
691 | 1 | private function convertRef3d($cell) |
|
692 | { |
||
693 | // Split the ref at the ! symbol |
||
694 | 1 | list($ext_ref, $cell) = explode('!', $cell); |
|
695 | |||
696 | // Convert the external reference part (different for BIFF8) |
||
697 | 1 | $ext_ref = $this->getRefIndex($ext_ref); |
|
698 | |||
699 | // Convert the cell reference part |
||
700 | 1 | list($row, $col) = $this->cellToPackedRowcol($cell); |
|
701 | |||
702 | // The ptg value depends on the class of the ptg. |
||
703 | 1 | $ptgRef = pack('C', $this->ptg['ptgRef3dA']); |
|
704 | |||
705 | 1 | return $ptgRef . $ext_ref . $row . $col; |
|
706 | } |
||
707 | |||
708 | /** |
||
709 | * Convert an error code to a ptgErr. |
||
710 | * |
||
711 | * @param string $errorCode The error code for conversion to its ptg value |
||
712 | * |
||
713 | * @return string The error code ptgErr |
||
714 | */ |
||
715 | private function convertError($errorCode) |
||
716 | { |
||
717 | switch ($errorCode) { |
||
718 | case '#NULL!': |
||
719 | return pack('C', 0x00); |
||
720 | case '#DIV/0!': |
||
721 | return pack('C', 0x07); |
||
722 | case '#VALUE!': |
||
723 | return pack('C', 0x0F); |
||
724 | case '#REF!': |
||
725 | return pack('C', 0x17); |
||
726 | case '#NAME?': |
||
727 | return pack('C', 0x1D); |
||
728 | case '#NUM!': |
||
729 | return pack('C', 0x24); |
||
730 | case '#N/A': |
||
731 | return pack('C', 0x2A); |
||
732 | } |
||
733 | |||
734 | return pack('C', 0xFF); |
||
735 | } |
||
736 | |||
737 | /** |
||
738 | * Look up the REF index that corresponds to an external sheet name |
||
739 | * (or range). If it doesn't exist yet add it to the workbook's references |
||
740 | * array. It assumes all sheet names given must exist. |
||
741 | * |
||
742 | * @param string $ext_ref The name of the external reference |
||
743 | * |
||
744 | * @return mixed The reference index in packed() format on success |
||
745 | */ |
||
746 | 4 | private function getRefIndex($ext_ref) |
|
747 | { |
||
748 | 4 | $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. |
|
749 | 4 | $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. |
|
750 | 4 | $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' |
|
751 | |||
752 | // Check if there is a sheet range eg., Sheet1:Sheet2. |
||
753 | 4 | if (preg_match('/:/', $ext_ref)) { |
|
754 | list($sheet_name1, $sheet_name2) = explode(':', $ext_ref); |
||
755 | |||
756 | $sheet1 = $this->getSheetIndex($sheet_name1); |
||
757 | if ($sheet1 == -1) { |
||
758 | throw new WriterException("Unknown sheet name $sheet_name1 in formula"); |
||
759 | } |
||
760 | $sheet2 = $this->getSheetIndex($sheet_name2); |
||
761 | if ($sheet2 == -1) { |
||
762 | throw new WriterException("Unknown sheet name $sheet_name2 in formula"); |
||
763 | } |
||
764 | |||
765 | // Reverse max and min sheet numbers if necessary |
||
766 | if ($sheet1 > $sheet2) { |
||
767 | list($sheet1, $sheet2) = [$sheet2, $sheet1]; |
||
768 | } |
||
769 | } else { // Single sheet name only. |
||
770 | 4 | $sheet1 = $this->getSheetIndex($ext_ref); |
|
771 | 4 | if ($sheet1 == -1) { |
|
772 | throw new WriterException("Unknown sheet name $ext_ref in formula"); |
||
773 | } |
||
774 | 4 | $sheet2 = $sheet1; |
|
775 | } |
||
776 | |||
777 | // assume all references belong to this document |
||
778 | 4 | $supbook_index = 0x00; |
|
779 | 4 | $ref = pack('vvv', $supbook_index, $sheet1, $sheet2); |
|
780 | 4 | $totalreferences = count($this->references); |
|
781 | 4 | $index = -1; |
|
782 | 4 | for ($i = 0; $i < $totalreferences; ++$i) { |
|
783 | 4 | if ($ref == $this->references[$i]) { |
|
784 | 4 | $index = $i; |
|
785 | |||
786 | 4 | break; |
|
787 | } |
||
788 | } |
||
789 | // if REF was not found add it to references array |
||
790 | 4 | if ($index == -1) { |
|
791 | $this->references[$totalreferences] = $ref; |
||
792 | $index = $totalreferences; |
||
793 | } |
||
794 | |||
795 | 4 | return pack('v', $index); |
|
796 | } |
||
797 | |||
798 | /** |
||
799 | * Look up the index that corresponds to an external sheet name. The hash of |
||
800 | * sheet names is updated by the addworksheet() method of the |
||
801 | * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. |
||
802 | * |
||
803 | * @param string $sheet_name Sheet name |
||
804 | * |
||
805 | * @return int The sheet index, -1 if the sheet was not found |
||
806 | */ |
||
807 | 4 | private function getSheetIndex($sheet_name) |
|
808 | { |
||
809 | 4 | if (!isset($this->externalSheets[$sheet_name])) { |
|
810 | return -1; |
||
811 | } |
||
812 | |||
813 | 4 | return $this->externalSheets[$sheet_name]; |
|
814 | } |
||
815 | |||
816 | /** |
||
817 | * This method is used to update the array of sheet names. It is |
||
818 | * called by the addWorksheet() method of the |
||
819 | * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. |
||
820 | * |
||
821 | * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet() |
||
822 | * |
||
823 | * @param string $name The name of the worksheet being added |
||
824 | * @param int $index The index of the worksheet being added |
||
825 | */ |
||
826 | 48 | public function setExtSheet($name, $index) |
|
827 | { |
||
828 | 48 | $this->externalSheets[$name] = $index; |
|
829 | 48 | } |
|
830 | |||
831 | /** |
||
832 | * pack() row and column into the required 3 or 4 byte format. |
||
833 | * |
||
834 | * @param string $cell The Excel cell reference to be packed |
||
835 | * |
||
836 | * @return array Array containing the row and column in packed() format |
||
837 | */ |
||
838 | 17 | private function cellToPackedRowcol($cell) |
|
839 | { |
||
840 | 17 | $cell = strtoupper($cell); |
|
841 | 17 | list($row, $col, $row_rel, $col_rel) = $this->cellToRowcol($cell); |
|
842 | 17 | if ($col >= 256) { |
|
843 | throw new WriterException("Column in: $cell greater than 255"); |
||
844 | } |
||
845 | 17 | if ($row >= 65536) { |
|
846 | throw new WriterException("Row in: $cell greater than 65536 "); |
||
847 | } |
||
848 | |||
849 | // Set the high bits to indicate if row or col are relative. |
||
850 | 17 | $col |= $col_rel << 14; |
|
851 | 17 | $col |= $row_rel << 15; |
|
852 | 17 | $col = pack('v', $col); |
|
853 | |||
854 | 17 | $row = pack('v', $row); |
|
855 | |||
856 | 17 | return [$row, $col]; |
|
857 | } |
||
858 | |||
859 | /** |
||
860 | * pack() row range into the required 3 or 4 byte format. |
||
861 | * Just using maximum col/rows, which is probably not the correct solution. |
||
862 | * |
||
863 | * @param string $range The Excel range to be packed |
||
864 | * |
||
865 | * @return array Array containing (row1,col1,row2,col2) in packed() format |
||
866 | */ |
||
867 | private function rangeToPackedRange($range) |
||
868 | { |
||
869 | preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match); |
||
870 | // return absolute rows if there is a $ in the ref |
||
871 | $row1_rel = empty($match[1]) ? 1 : 0; |
||
872 | $row1 = $match[2]; |
||
873 | $row2_rel = empty($match[3]) ? 1 : 0; |
||
874 | $row2 = $match[4]; |
||
875 | // Convert 1-index to zero-index |
||
876 | --$row1; |
||
877 | --$row2; |
||
878 | // Trick poor inocent Excel |
||
879 | $col1 = 0; |
||
880 | $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!) |
||
881 | |||
882 | // FIXME: this changes for BIFF8 |
||
883 | if (($row1 >= 65536) or ($row2 >= 65536)) { |
||
884 | throw new WriterException("Row in: $range greater than 65536 "); |
||
885 | } |
||
886 | |||
887 | // Set the high bits to indicate if rows are relative. |
||
888 | $col1 |= $row1_rel << 15; |
||
889 | $col2 |= $row2_rel << 15; |
||
890 | $col1 = pack('v', $col1); |
||
891 | $col2 = pack('v', $col2); |
||
892 | |||
893 | $row1 = pack('v', $row1); |
||
894 | $row2 = pack('v', $row2); |
||
895 | |||
896 | return [$row1, $col1, $row2, $col2]; |
||
897 | } |
||
898 | |||
899 | /** |
||
900 | * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero |
||
901 | * indexed row and column number. Also returns two (0,1) values to indicate |
||
902 | * whether the row or column are relative references. |
||
903 | * |
||
904 | * @param string $cell the Excel cell reference in A1 format |
||
905 | * |
||
906 | * @return array |
||
907 | */ |
||
908 | 17 | private function cellToRowcol($cell) |
|
909 | { |
||
910 | 17 | preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match); |
|
911 | // return absolute column if there is a $ in the ref |
||
912 | 17 | $col_rel = empty($match[1]) ? 1 : 0; |
|
913 | 17 | $col_ref = $match[2]; |
|
914 | 17 | $row_rel = empty($match[3]) ? 1 : 0; |
|
915 | 17 | $row = $match[4]; |
|
916 | |||
917 | // Convert base26 column string to a number. |
||
918 | 17 | $expn = strlen($col_ref) - 1; |
|
919 | 17 | $col = 0; |
|
920 | 17 | $col_ref_length = strlen($col_ref); |
|
921 | 17 | for ($i = 0; $i < $col_ref_length; ++$i) { |
|
922 | 17 | $col += (ord($col_ref[$i]) - 64) * pow(26, $expn); |
|
923 | 17 | --$expn; |
|
924 | } |
||
925 | |||
926 | // Convert 1-index to zero-index |
||
927 | 17 | --$row; |
|
928 | 17 | --$col; |
|
929 | |||
930 | 17 | return [$row, $col, $row_rel, $col_rel]; |
|
931 | } |
||
932 | |||
933 | /** |
||
934 | * Advance to the next valid token. |
||
935 | */ |
||
936 | 18 | private function advance() |
|
937 | { |
||
938 | 18 | $i = $this->currentCharacter; |
|
939 | 18 | $formula_length = strlen($this->formula); |
|
940 | // eat up white spaces |
||
941 | 18 | if ($i < $formula_length) { |
|
942 | 18 | while ($this->formula[$i] == ' ') { |
|
943 | 2 | ++$i; |
|
944 | } |
||
945 | |||
946 | 18 | if ($i < ($formula_length - 1)) { |
|
947 | 18 | $this->lookAhead = $this->formula[$i + 1]; |
|
948 | } |
||
949 | 18 | $token = ''; |
|
950 | } |
||
951 | |||
952 | 18 | while ($i < $formula_length) { |
|
953 | 18 | $token .= $this->formula[$i]; |
|
954 | |||
955 | 18 | View Code Duplication | if ($i < ($formula_length - 1)) { |
956 | 18 | $this->lookAhead = $this->formula[$i + 1]; |
|
957 | } else { |
||
958 | 18 | $this->lookAhead = ''; |
|
959 | } |
||
960 | |||
961 | 18 | if ($this->match($token) != '') { |
|
962 | 18 | $this->currentCharacter = $i + 1; |
|
963 | 18 | $this->currentToken = $token; |
|
964 | |||
965 | 18 | return 1; |
|
966 | } |
||
967 | |||
968 | 18 | View Code Duplication | if ($i < ($formula_length - 2)) { |
969 | 18 | $this->lookAhead = $this->formula[$i + 2]; |
|
970 | } else { // if we run out of characters lookAhead becomes empty |
||
971 | 14 | $this->lookAhead = ''; |
|
972 | } |
||
973 | 18 | ++$i; |
|
974 | } |
||
975 | //die("Lexical error ".$this->currentCharacter); |
||
976 | 18 | } |
|
977 | |||
978 | /** |
||
979 | * Checks if it's a valid token. |
||
980 | * |
||
981 | * @param mixed $token the token to check |
||
982 | * |
||
983 | * @return mixed The checked token or false on failure |
||
984 | */ |
||
985 | 18 | private function match($token) |
|
986 | { |
||
987 | switch ($token) { |
||
988 | 18 | case '+': |
|
989 | 18 | case '-': |
|
990 | 18 | case '*': |
|
991 | 18 | case '/': |
|
992 | 18 | case '(': |
|
993 | 18 | case ')': |
|
994 | 18 | case ',': |
|
995 | 18 | case ';': |
|
996 | 18 | case '>=': |
|
997 | 18 | case '<=': |
|
998 | 18 | case '=': |
|
999 | 18 | case '<>': |
|
1000 | 18 | case '^': |
|
1001 | 18 | case '&': |
|
1002 | 18 | case '%': |
|
1003 | 17 | return $token; |
|
1004 | break; |
||
1005 | 18 | case '>': |
|
1006 | if ($this->lookAhead == '=') { // it's a GE token |
||
1007 | break; |
||
1008 | } |
||
1009 | |||
1010 | return $token; |
||
1011 | break; |
||
1012 | 18 | case '<': |
|
1013 | // it's a LE or a NE token |
||
1014 | 5 | if (($this->lookAhead == '=') or ($this->lookAhead == '>')) { |
|
1015 | 5 | break; |
|
1016 | } |
||
1017 | |||
1018 | return $token; |
||
1019 | break; |
||
1020 | default: |
||
1021 | // if it's a reference A1 or $A$1 or $A1 or A$1 |
||
1022 | 18 | if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/', $token) and !preg_match('/[0-9]/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.') and ($this->lookAhead != '!')) { |
|
1023 | 12 | return $token; |
|
1024 | 18 | } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?[A-Ia-i]?[A-Za-z]\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.')) { |
|
1025 | // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) |
||
1026 | return $token; |
||
1027 | 18 | } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?[A-Ia-i]?[A-Za-z]\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.')) { |
|
1028 | // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) |
||
1029 | 1 | return $token; |
|
1030 | 18 | } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/', $token) && !preg_match('/[0-9]/', $this->lookAhead)) { |
|
1031 | // if it's a range A1:A2 or $A$1:$A$2 |
||
1032 | 15 | return $token; |
|
1033 | 18 | } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+:\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead)) { |
|
1034 | // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 |
||
1035 | return $token; |
||
1036 | 18 | } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+:\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead)) { |
|
1037 | // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 |
||
1038 | 3 | return $token; |
|
1039 | 18 | } elseif (is_numeric($token) and (!is_numeric($token . $this->lookAhead) or ($this->lookAhead == '')) and ($this->lookAhead != '!') and ($this->lookAhead != ':')) { |
|
1040 | // If it's a number (check that it's not a sheet name or range) |
||
1041 | 10 | return $token; |
|
1042 | 18 | } elseif (preg_match('/"([^"]|""){0,255}"/', $token) and $this->lookAhead != '"' and (substr_count($token, '"') % 2 == 0)) { |
|
1043 | // If it's a string (of maximum 255 characters) |
||
1044 | 9 | return $token; |
|
1045 | 18 | } elseif (preg_match("/^#[A-Z0\/]{3,5}[!?]{1}$/", $token) or $token == '#N/A') { |
|
1046 | // If it's an error code |
||
1047 | return $token; |
||
1048 | 18 | } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/i", $token) and ($this->lookAhead == '(')) { |
|
1049 | // if it's a function call |
||
1050 | 15 | return $token; |
|
1051 | 18 | } elseif (substr($token, -1) == ')') { |
|
1052 | // It's an argument of some description (e.g. a named range), |
||
1053 | // precise nature yet to be determined |
||
1054 | 3 | return $token; |
|
1055 | } |
||
1056 | |||
1057 | 18 | return ''; |
|
1058 | } |
||
1059 | 5 | } |
|
1060 | |||
1061 | /** |
||
1062 | * The parsing method. It parses a formula. |
||
1063 | * |
||
1064 | * @param string $formula the formula to parse, without the initial equal |
||
1065 | * sign (=) |
||
1066 | * |
||
1067 | * @return mixed true on success |
||
1068 | */ |
||
1069 | 18 | public function parse($formula) |
|
1070 | { |
||
1071 | 18 | $this->currentCharacter = 0; |
|
1072 | 18 | $this->formula = $formula; |
|
1073 | 18 | $this->lookAhead = isset($formula[1]) ? $formula[1] |
|
1074 | 1 | : ''; |
|
1075 | 18 | $this->advance(); |
|
1076 | 18 | $this->parseTree = $this->condition(); |
|
1077 | |||
1078 | 18 | return true; |
|
1079 | } |
||
1080 | |||
1081 | /** |
||
1082 | * It parses a condition. It assumes the following rule: |
||
1083 | * Cond -> Expr [(">" | "<") Expr]. |
||
1084 | * |
||
1085 | * @return mixed The parsed ptg'd tree on success |
||
1086 | */ |
||
1087 | 18 | private function condition() |
|
1088 | { |
||
1089 | 18 | $result = $this->expression(); |
|
1090 | 18 | if ($this->currentToken == '<') { |
|
1091 | $this->advance(); |
||
1092 | $result2 = $this->expression(); |
||
1093 | $result = $this->createTree('ptgLT', $result, $result2); |
||
1094 | 18 | View Code Duplication | } elseif ($this->currentToken == '>') { |
1095 | $this->advance(); |
||
1096 | $result2 = $this->expression(); |
||
1097 | $result = $this->createTree('ptgGT', $result, $result2); |
||
1098 | 18 | } elseif ($this->currentToken == '<=') { |
|
1099 | $this->advance(); |
||
1100 | $result2 = $this->expression(); |
||
1101 | $result = $this->createTree('ptgLE', $result, $result2); |
||
1102 | 18 | View Code Duplication | } elseif ($this->currentToken == '>=') { |
1103 | $this->advance(); |
||
1104 | $result2 = $this->expression(); |
||
1105 | $result = $this->createTree('ptgGE', $result, $result2); |
||
1106 | 18 | } elseif ($this->currentToken == '=') { |
|
1107 | $this->advance(); |
||
1108 | $result2 = $this->expression(); |
||
1109 | $result = $this->createTree('ptgEQ', $result, $result2); |
||
1110 | 18 | View Code Duplication | } elseif ($this->currentToken == '<>') { |
1111 | 5 | $this->advance(); |
|
1112 | 5 | $result2 = $this->expression(); |
|
1113 | 5 | $result = $this->createTree('ptgNE', $result, $result2); |
|
1114 | 18 | } elseif ($this->currentToken == '&') { |
|
1115 | 6 | $this->advance(); |
|
1116 | 6 | $result2 = $this->expression(); |
|
1117 | 6 | $result = $this->createTree('ptgConcat', $result, $result2); |
|
1118 | } |
||
1119 | |||
1120 | 18 | return $result; |
|
1121 | } |
||
1122 | |||
1123 | /** |
||
1124 | * It parses a expression. It assumes the following rule: |
||
1125 | * Expr -> Term [("+" | "-") Term] |
||
1126 | * -> "string" |
||
1127 | * -> "-" Term : Negative value |
||
1128 | * -> "+" Term : Positive value |
||
1129 | * -> Error code. |
||
1130 | * |
||
1131 | * @return mixed The parsed ptg'd tree on success |
||
1132 | */ |
||
1133 | 18 | private function expression() |
|
1134 | { |
||
1135 | // If it's a string return a string node |
||
1136 | 18 | if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) { |
|
1137 | 9 | $tmp = str_replace('""', '"', $this->currentToken); |
|
1138 | 9 | if (($tmp == '"') || ($tmp == '')) { |
|
1139 | // Trap for "" that has been used for an empty string |
||
1140 | 5 | $tmp = '""'; |
|
1141 | } |
||
1142 | 9 | $result = $this->createTree($tmp, '', ''); |
|
1143 | 9 | $this->advance(); |
|
1144 | |||
1145 | 9 | return $result; |
|
1146 | // If it's an error code |
||
1147 | 18 | } elseif (preg_match("/^#[A-Z0\/]{3,5}[!?]{1}$/", $this->currentToken) or $this->currentToken == '#N/A') { |
|
1148 | $result = $this->createTree($this->currentToken, 'ptgErr', ''); |
||
1149 | $this->advance(); |
||
1150 | |||
1151 | return $result; |
||
1152 | // If it's a negative value |
||
1153 | 18 | } elseif ($this->currentToken == '-') { |
|
1154 | // catch "-" Term |
||
1155 | $this->advance(); |
||
1156 | $result2 = $this->expression(); |
||
1157 | $result = $this->createTree('ptgUminus', $result2, ''); |
||
1158 | |||
1159 | return $result; |
||
1160 | // If it's a positive value |
||
1161 | 18 | View Code Duplication | } elseif ($this->currentToken == '+') { |
1162 | // catch "+" Term |
||
1163 | $this->advance(); |
||
1164 | $result2 = $this->expression(); |
||
1165 | $result = $this->createTree('ptgUplus', $result2, ''); |
||
1166 | |||
1167 | return $result; |
||
1168 | } |
||
1169 | 18 | $result = $this->term(); |
|
1170 | 18 | while (($this->currentToken == '+') or |
|
1171 | 18 | ($this->currentToken == '-') or |
|
1172 | 18 | ($this->currentToken == '^')) { |
|
1173 | 10 | if ($this->currentToken == '+') { |
|
1174 | 10 | $this->advance(); |
|
1175 | 10 | $result2 = $this->term(); |
|
1176 | 10 | $result = $this->createTree('ptgAdd', $result, $result2); |
|
1177 | 3 | } elseif ($this->currentToken == '-') { |
|
1178 | 3 | $this->advance(); |
|
1179 | 3 | $result2 = $this->term(); |
|
1180 | 3 | $result = $this->createTree('ptgSub', $result, $result2); |
|
1181 | } else { |
||
1182 | $this->advance(); |
||
1183 | $result2 = $this->term(); |
||
1184 | $result = $this->createTree('ptgPower', $result, $result2); |
||
1185 | } |
||
1186 | } |
||
1187 | |||
1188 | 18 | return $result; |
|
1189 | } |
||
1190 | |||
1191 | /** |
||
1192 | * This function just introduces a ptgParen element in the tree, so that Excel |
||
1193 | * doesn't get confused when working with a parenthesized formula afterwards. |
||
1194 | * |
||
1195 | * @see fact() |
||
1196 | * |
||
1197 | * @return array The parsed ptg'd tree |
||
1198 | */ |
||
1199 | 1 | private function parenthesizedExpression() |
|
1204 | } |
||
1205 | |||
1206 | /** |
||
1207 | * It parses a term. It assumes the following rule: |
||
1208 | * Term -> Fact [("*" | "/") Fact]. |
||
1209 | * |
||
1210 | * @return mixed The parsed ptg'd tree on success |
||
1211 | */ |
||
1212 | 18 | private function term() |
|
1213 | { |
||
1214 | 18 | $result = $this->fact(); |
|
1215 | 18 | while (($this->currentToken == '*') or |
|
1216 | 18 | ($this->currentToken == '/')) { |
|
1217 | 10 | if ($this->currentToken == '*') { |
|
1218 | 10 | $this->advance(); |
|
1219 | 10 | $result2 = $this->fact(); |
|
1220 | 10 | $result = $this->createTree('ptgMul', $result, $result2); |
|
1221 | } else { |
||
1222 | 3 | $this->advance(); |
|
1223 | 3 | $result2 = $this->fact(); |
|
1224 | 3 | $result = $this->createTree('ptgDiv', $result, $result2); |
|
1225 | } |
||
1226 | } |
||
1227 | |||
1228 | 18 | return $result; |
|
1229 | } |
||
1230 | |||
1231 | /** |
||
1232 | * It parses a factor. It assumes the following rule: |
||
1233 | * Fact -> ( Expr ) |
||
1234 | * | CellRef |
||
1235 | * | CellRange |
||
1236 | * | Number |
||
1237 | * | Function. |
||
1238 | * |
||
1239 | * @return mixed The parsed ptg'd tree on success |
||
1240 | */ |
||
1241 | 18 | private function fact() |
|
1311 | } |
||
1312 | |||
1313 | /** |
||
1314 | * It parses a function call. It assumes the following rule: |
||
1315 | * Func -> ( Expr [,Expr]* ). |
||
1316 | * |
||
1317 | * @return mixed The parsed ptg'd tree on success |
||
1318 | */ |
||
1319 | 15 | private function func() |
|
1320 | { |
||
1321 | 15 | $num_args = 0; // number of arguments received |
|
1322 | 15 | $function = strtoupper($this->currentToken); |
|
1323 | 15 | $result = ''; // initialize result |
|
1324 | 15 | $this->advance(); |
|
1325 | 15 | $this->advance(); // eat the "(" |
|
1326 | 15 | while ($this->currentToken != ')') { |
|
1327 | 15 | if ($num_args > 0) { |
|
1328 | 7 | if ($this->currentToken == ',' || $this->currentToken == ';') { |
|
1329 | 7 | $this->advance(); // eat the "," or ";" |
|
1330 | } else { |
||
1331 | throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}"); |
||
1332 | } |
||
1333 | 7 | $result2 = $this->condition(); |
|
1334 | 7 | $result = $this->createTree('arg', $result, $result2); |
|
1335 | } else { // first argument |
||
1336 | 15 | $result2 = $this->condition(); |
|
1337 | 15 | $result = $this->createTree('arg', '', $result2); |
|
1338 | } |
||
1339 | 15 | ++$num_args; |
|
1340 | } |
||
1341 | 15 | if (!isset($this->functions[$function])) { |
|
1342 | 1 | throw new WriterException("Function $function() doesn't exist"); |
|
1343 | } |
||
1344 | 15 | $args = $this->functions[$function][1]; |
|
1345 | // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid. |
||
1346 | 15 | if (($args >= 0) and ($args != $num_args)) { |
|
1347 | throw new WriterException("Incorrect number of arguments in function $function() "); |
||
1348 | } |
||
1349 | |||
1350 | 15 | $result = $this->createTree($function, $result, $num_args); |
|
1351 | 15 | $this->advance(); // eat the ")" |
|
1352 | 15 | return $result; |
|
1353 | } |
||
1354 | |||
1355 | /** |
||
1356 | * Creates a tree. In fact an array which may have one or two arrays (sub-trees) |
||
1357 | * as elements. |
||
1358 | * |
||
1359 | * @param mixed $value the value of this node |
||
1360 | * @param mixed $left the left array (sub-tree) or a final node |
||
1361 | * @param mixed $right the right array (sub-tree) or a final node |
||
1362 | * |
||
1363 | * @return array A tree |
||
1364 | */ |
||
1365 | 18 | private function createTree($value, $left, $right) |
|
1368 | } |
||
1369 | |||
1370 | /** |
||
1371 | * Builds a string containing the tree in reverse polish notation (What you |
||
1372 | * would use in a HP calculator stack). |
||
1373 | * The following tree:. |
||
1374 | * |
||
1375 | * + |
||
1376 | * / \ |
||
1377 | * 2 3 |
||
1378 | * |
||
1379 | * produces: "23+" |
||
1380 | * |
||
1381 | * The following tree: |
||
1382 | * |
||
1383 | * + |
||
1384 | * / \ |
||
1385 | * 3 * |
||
1386 | * / \ |
||
1387 | * 6 A1 |
||
1388 | * |
||
1389 | * produces: "36A1*+" |
||
1390 | * |
||
1391 | * In fact all operands, functions, references, etc... are written as ptg's |
||
1392 | * |
||
1393 | * @param array $tree the optional tree to convert |
||
1394 | * |
||
1395 | * @return string The tree in reverse polish notation |
||
1396 | */ |
||
1397 | 18 | public function toReversePolish($tree = []) |
|
1438 | } |
||
1439 | } |
||
1440 |