For God so loved the world, that He gave His only begotten Son, that all who believe in Him should not perish but have everlasting life
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1531 lines
64 KiB

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