SpreadsheetReader_XLSX.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. <?php
  2. /**
  3. * Class for parsing XLSX files specifically
  4. *
  5. * @author Martins Pilsetnieks
  6. */
  7. class SpreadsheetReader_XLSX implements Iterator, Countable
  8. {
  9. const CELL_TYPE_BOOL = 'b';
  10. const CELL_TYPE_NUMBER = 'n';
  11. const CELL_TYPE_ERROR = 'e';
  12. const CELL_TYPE_SHARED_STR = 's';
  13. const CELL_TYPE_STR = 'str';
  14. const CELL_TYPE_INLINE_STR = 'inlineStr';
  15. /**
  16. * Number of shared strings that can be reasonably cached, i.e., that aren't read from file but stored in memory.
  17. * If the total number of shared strings is higher than this, caching is not used.
  18. * If this value is null, shared strings are cached regardless of amount.
  19. * With large shared string caches there are huge performance gains, however a lot of memory could be used which
  20. * can be a problem, especially on shared hosting.
  21. */
  22. const SHARED_STRING_CACHE_LIMIT = 50000;
  23. private $Options = array(
  24. 'TempDir' => '',
  25. 'ReturnDateTimeObjects' => false
  26. );
  27. private static $RuntimeInfo = array(
  28. 'GMPSupported' => false
  29. );
  30. private $Valid = false;
  31. /**
  32. * @var SpreadsheetReader_* Handle for the reader object
  33. */
  34. private $Handle = false;
  35. // Worksheet file
  36. /**
  37. * @var string Path to the worksheet XML file
  38. */
  39. private $WorksheetPath = false;
  40. /**
  41. * @var XMLReader XML reader object for the worksheet XML file
  42. */
  43. private $Worksheet = false;
  44. // Shared strings file
  45. /**
  46. * @var string Path to shared strings XML file
  47. */
  48. private $SharedStringsPath = false;
  49. /**
  50. * @var XMLReader XML reader object for the shared strings XML file
  51. */
  52. private $SharedStrings = false;
  53. /**
  54. * @var array Shared strings cache, if the number of shared strings is low enough
  55. */
  56. private $SharedStringCache = array();
  57. // Workbook data
  58. /**
  59. * @var SimpleXMLElement XML object for the workbook XML file
  60. */
  61. private $WorkbookXML = false;
  62. // Style data
  63. /**
  64. * @var SimpleXMLElement XML object for the styles XML file
  65. */
  66. private $StylesXML = false;
  67. /**
  68. * @var array Container for cell value style data
  69. */
  70. private $Styles = array();
  71. private $TempDir = '';
  72. private $TempFiles = array();
  73. private $CurrentRow = false;
  74. // Runtime parsing data
  75. /**
  76. * @var int Current row in the file
  77. */
  78. private $Index = 0;
  79. /**
  80. * @var array Data about separate sheets in the file
  81. */
  82. private $Sheets = false;
  83. private $SharedStringCount = 0;
  84. private $SharedStringIndex = 0;
  85. private $LastSharedStringValue = null;
  86. private $RowOpen = false;
  87. private $SSOpen = false;
  88. private $SSForwarded = false;
  89. private static $BuiltinFormats = array(
  90. 0 => '',
  91. 1 => '0',
  92. 2 => '0.00',
  93. 3 => '#,##0',
  94. 4 => '#,##0.00',
  95. 9 => '0%',
  96. 10 => '0.00%',
  97. 11 => '0.00E+00',
  98. 12 => '# ?/?',
  99. 13 => '# ??/??',
  100. 14 => 'mm-dd-yy',
  101. 15 => 'd-mmm-yy',
  102. 16 => 'd-mmm',
  103. 17 => 'mmm-yy',
  104. 18 => 'h:mm AM/PM',
  105. 19 => 'h:mm:ss AM/PM',
  106. 20 => 'h:mm',
  107. 21 => 'h:mm:ss',
  108. 22 => 'm/d/yy h:mm',
  109. 37 => '#,##0 ;(#,##0)',
  110. 38 => '#,##0 ;[Red](#,##0)',
  111. 39 => '#,##0.00;(#,##0.00)',
  112. 40 => '#,##0.00;[Red](#,##0.00)',
  113. 45 => 'mm:ss',
  114. 46 => '[h]:mm:ss',
  115. 47 => 'mmss.0',
  116. 48 => '##0.0E+0',
  117. 49 => '@',
  118. // CHT & CHS
  119. 27 => '[$-404]e/m/d',
  120. 30 => 'm/d/yy',
  121. 36 => '[$-404]e/m/d',
  122. 50 => '[$-404]e/m/d',
  123. 57 => '[$-404]e/m/d',
  124. // THA
  125. 59 => 't0',
  126. 60 => 't0.00',
  127. 61 =>'t#,##0',
  128. 62 => 't#,##0.00',
  129. 67 => 't0%',
  130. 68 => 't0.00%',
  131. 69 => 't# ?/?',
  132. 70 => 't# ??/??'
  133. );
  134. private $Formats = array();
  135. private static $DateReplacements = array(
  136. 'All' => array(
  137. '\\' => '',
  138. 'am/pm' => 'A',
  139. 'yyyy' => 'Y',
  140. 'yy' => 'y',
  141. 'mmmmm' => 'M',
  142. 'mmmm' => 'F',
  143. 'mmm' => 'M',
  144. ':mm' => ':i',
  145. 'mm' => 'm',
  146. 'm' => 'n',
  147. 'dddd' => 'l',
  148. 'ddd' => 'D',
  149. 'dd' => 'd',
  150. 'd' => 'j',
  151. 'ss' => 's',
  152. '.s' => ''
  153. ),
  154. '24H' => array(
  155. 'hh' => 'H',
  156. 'h' => 'G'
  157. ),
  158. '12H' => array(
  159. 'hh' => 'h',
  160. 'h' => 'G'
  161. )
  162. );
  163. private static $BaseDate = false;
  164. private static $DecimalSeparator = '.';
  165. private static $ThousandSeparator = '';
  166. private static $CurrencyCode = '';
  167. /**
  168. * @var array Cache for already processed format strings
  169. */
  170. private $ParsedFormatCache = array();
  171. /**
  172. * @param string Path to file
  173. * @param array Options:
  174. * TempDir => string Temporary directory path
  175. * ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects, false => as strings
  176. */
  177. public function __construct($Filepath, array $Options = null)
  178. {
  179. if (!is_readable($Filepath))
  180. {
  181. throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.')');
  182. }
  183. $this -> TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ?
  184. $Options['TempDir'] :
  185. sys_get_temp_dir();
  186. $this -> TempDir = rtrim($this -> TempDir, DIRECTORY_SEPARATOR);
  187. $this -> TempDir = $this -> TempDir.DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR;
  188. $Zip = new ZipArchive;
  189. $Status = $Zip -> open($Filepath);
  190. if ($Status !== true)
  191. {
  192. throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.') (Error '.$Status.')');
  193. }
  194. // Getting the general workbook information
  195. if ($Zip -> locateName('xl/workbook.xml') !== false)
  196. {
  197. $this -> WorkbookXML = new SimpleXMLElement($Zip -> getFromName('xl/workbook.xml'));
  198. }
  199. // Extracting the XMLs from the XLSX zip file
  200. if ($Zip -> locateName('xl/sharedStrings.xml') !== false)
  201. {
  202. $this -> SharedStringsPath = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml';
  203. $Zip -> extractTo($this -> TempDir, 'xl/sharedStrings.xml');
  204. $this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml';
  205. if (is_readable($this -> SharedStringsPath))
  206. {
  207. $this -> SharedStrings = new XMLReader;
  208. $this -> SharedStrings -> open($this -> SharedStringsPath);
  209. $this -> PrepareSharedStringCache();
  210. }
  211. }
  212. $Sheets = $this -> Sheets();
  213. foreach ($this -> Sheets as $Index => $Name)
  214. {
  215. if ($Zip -> locateName('xl/worksheets/sheet'.$Index.'.xml') !== false)
  216. {
  217. $Zip -> extractTo($this -> TempDir, 'xl/worksheets/sheet'.$Index.'.xml');
  218. $this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets'.DIRECTORY_SEPARATOR.'sheet'.$Index.'.xml';
  219. }
  220. }
  221. $this -> ChangeSheet(0);
  222. // If worksheet is present and is OK, parse the styles already
  223. if ($Zip -> locateName('xl/styles.xml') !== false)
  224. {
  225. $this -> StylesXML = new SimpleXMLElement($Zip -> getFromName('xl/styles.xml'));
  226. if ($this -> StylesXML && $this -> StylesXML -> cellXfs && $this -> StylesXML -> cellXfs -> xf)
  227. {
  228. foreach ($this -> StylesXML -> cellXfs -> xf as $Index => $XF)
  229. {
  230. // Format #0 is a special case - it is the "General" format that is applied regardless of applyNumberFormat
  231. if ($XF -> attributes() -> applyNumberFormat || (0 == (int)$XF -> attributes() -> numFmtId))
  232. {
  233. $FormatId = (int)$XF -> attributes() -> numFmtId;
  234. // If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts
  235. $this -> Styles[] = $FormatId;
  236. }
  237. else
  238. {
  239. // 0 for "General" format
  240. $this -> Styles[] = 0;
  241. }
  242. }
  243. }
  244. if ($this -> StylesXML -> numFmts && $this -> StylesXML -> numFmts -> numFmt)
  245. {
  246. foreach ($this -> StylesXML -> numFmts -> numFmt as $Index => $NumFmt)
  247. {
  248. $this -> Formats[(int)$NumFmt -> attributes() -> numFmtId] = (string)$NumFmt -> attributes() -> formatCode;
  249. }
  250. }
  251. unset($this -> StylesXML);
  252. }
  253. $Zip -> close();
  254. // Setting base date
  255. if (!self::$BaseDate)
  256. {
  257. self::$BaseDate = new DateTime;
  258. self::$BaseDate -> setTimezone(new DateTimeZone('UTC'));
  259. self::$BaseDate -> setDate(1900, 1, 0);
  260. self::$BaseDate -> setTime(0, 0, 0);
  261. }
  262. // Decimal and thousand separators
  263. if (!self::$DecimalSeparator && !self::$ThousandSeparator && !self::$CurrencyCode)
  264. {
  265. $Locale = localeconv();
  266. self::$DecimalSeparator = $Locale['decimal_point'];
  267. self::$ThousandSeparator = $Locale['thousands_sep'];
  268. self::$CurrencyCode = $Locale['int_curr_symbol'];
  269. }
  270. if (function_exists('gmp_gcd'))
  271. {
  272. self::$RuntimeInfo['GMPSupported'] = true;
  273. }
  274. }
  275. /**
  276. * Destructor, destroys all that remains (closes and deletes temp files)
  277. */
  278. public function __destruct()
  279. {
  280. foreach ($this -> TempFiles as $TempFile)
  281. {
  282. @unlink($TempFile);
  283. }
  284. // Better safe than sorry - shouldn't try deleting '.' or '/', or '..'.
  285. if (strlen($this -> TempDir) > 2)
  286. {
  287. @rmdir($this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets');
  288. @rmdir($this -> TempDir.'xl');
  289. @rmdir($this -> TempDir);
  290. }
  291. if ($this -> Worksheet && $this -> Worksheet instanceof XMLReader)
  292. {
  293. $this -> Worksheet -> close();
  294. unset($this -> Worksheet);
  295. }
  296. unset($this -> WorksheetPath);
  297. if ($this -> SharedStrings && $this -> SharedStrings instanceof XMLReader)
  298. {
  299. $this -> SharedStrings -> close();
  300. unset($this -> SharedStrings);
  301. }
  302. unset($this -> SharedStringsPath);
  303. if (isset($this -> StylesXML))
  304. {
  305. unset($this -> StylesXML);
  306. }
  307. if ($this -> WorkbookXML)
  308. {
  309. unset($this -> WorkbookXML);
  310. }
  311. }
  312. /**
  313. * Retrieves an array with information about sheets in the current file
  314. *
  315. * @return array List of sheets (key is sheet index, value is name)
  316. */
  317. public function Sheets()
  318. {
  319. if ($this -> Sheets === false)
  320. {
  321. $this -> Sheets = array();
  322. foreach ($this -> WorkbookXML -> sheets -> sheet as $Index => $Sheet)
  323. {
  324. $Attributes = $Sheet -> attributes('r', true);
  325. foreach ($Attributes as $Name => $Value)
  326. {
  327. if ($Name == 'id')
  328. {
  329. $SheetID = (int)str_replace('rId', '', (string)$Value);
  330. break;
  331. }
  332. }
  333. $this -> Sheets[$SheetID] = (string)$Sheet['name'];
  334. }
  335. ksort($this -> Sheets);
  336. }
  337. return array_values($this -> Sheets);
  338. }
  339. /**
  340. * Changes the current sheet in the file to another
  341. *
  342. * @param int Sheet index
  343. *
  344. * @return bool True if sheet was successfully changed, false otherwise.
  345. */
  346. public function ChangeSheet($Index)
  347. {
  348. $RealSheetIndex = false;
  349. $Sheets = $this -> Sheets();
  350. if (isset($Sheets[$Index]))
  351. {
  352. $SheetIndexes = array_keys($this -> Sheets);
  353. $RealSheetIndex = $SheetIndexes[$Index];
  354. }
  355. $TempWorksheetPath = $this -> TempDir.'xl/worksheets/sheet'.$RealSheetIndex.'.xml';
  356. if ($RealSheetIndex !== false && is_readable($TempWorksheetPath))
  357. {
  358. $this -> WorksheetPath = $TempWorksheetPath;
  359. $this -> rewind();
  360. return true;
  361. }
  362. return false;
  363. }
  364. /**
  365. * Creating shared string cache if the number of shared strings is acceptably low (or there is no limit on the amount
  366. */
  367. private function PrepareSharedStringCache()
  368. {
  369. while ($this -> SharedStrings -> read())
  370. {
  371. if ($this -> SharedStrings -> name == 'sst')
  372. {
  373. $this -> SharedStringCount = $this -> SharedStrings -> getAttribute('count');
  374. break;
  375. }
  376. }
  377. if (!$this -> SharedStringCount || (self::SHARED_STRING_CACHE_LIMIT < $this -> SharedStringCount && self::SHARED_STRING_CACHE_LIMIT !== null))
  378. {
  379. return false;
  380. }
  381. $CacheIndex = 0;
  382. $CacheValue = '';
  383. while ($this -> SharedStrings -> read())
  384. {
  385. switch ($this -> SharedStrings -> name)
  386. {
  387. case 'si':
  388. if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
  389. {
  390. $this -> SharedStringCache[$CacheIndex] = $CacheValue;
  391. $CacheIndex++;
  392. $CacheValue = '';
  393. }
  394. break;
  395. case 't':
  396. if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
  397. {
  398. continue;
  399. }
  400. $CacheValue .= $this -> SharedStrings -> readString();
  401. break;
  402. }
  403. }
  404. $this -> SharedStrings -> close();
  405. return true;
  406. }
  407. /**
  408. * Retrieves a shared string value by its index
  409. *
  410. * @param int Shared string index
  411. *
  412. * @return string Value
  413. */
  414. private function GetSharedString($Index)
  415. {
  416. if ((self::SHARED_STRING_CACHE_LIMIT === null || self::SHARED_STRING_CACHE_LIMIT > 0) && !empty($this -> SharedStringCache))
  417. {
  418. if (isset($this -> SharedStringCache[$Index]))
  419. {
  420. return $this -> SharedStringCache[$Index];
  421. }
  422. else
  423. {
  424. return '';
  425. }
  426. }
  427. // If the desired index is before the current, rewind the XML
  428. if ($this -> SharedStringIndex > $Index)
  429. {
  430. $this -> SSOpen = false;
  431. $this -> SharedStrings -> close();
  432. $this -> SharedStrings -> open($this -> SharedStringsPath);
  433. $this -> SharedStringIndex = 0;
  434. $this -> LastSharedStringValue = null;
  435. $this -> SSForwarded = false;
  436. }
  437. // Finding the unique string count (if not already read)
  438. if ($this -> SharedStringIndex == 0 && !$this -> SharedStringCount)
  439. {
  440. while ($this -> SharedStrings -> read())
  441. {
  442. if ($this -> SharedStrings -> name == 'sst')
  443. {
  444. $this -> SharedStringCount = $this -> SharedStrings -> getAttribute('uniqueCount');
  445. break;
  446. }
  447. }
  448. }
  449. // If index of the desired string is larger than possible, don't even bother.
  450. if ($this -> SharedStringCount && ($Index >= $this -> SharedStringCount))
  451. {
  452. return '';
  453. }
  454. // If an index with the same value as the last already fetched is requested
  455. // (any further traversing the tree would get us further away from the node)
  456. if (($Index == $this -> SharedStringIndex) && ($this -> LastSharedStringValue !== null))
  457. {
  458. return $this -> LastSharedStringValue;
  459. }
  460. // Find the correct <si> node with the desired index
  461. while ($this -> SharedStringIndex <= $Index)
  462. {
  463. // SSForwarded is set further to avoid double reading in case nodes are skipped.
  464. if ($this -> SSForwarded)
  465. {
  466. $this -> SSForwarded = false;
  467. }
  468. else
  469. {
  470. $ReadStatus = $this -> SharedStrings -> read();
  471. if (!$ReadStatus)
  472. {
  473. break;
  474. }
  475. }
  476. if ($this -> SharedStrings -> name == 'si')
  477. {
  478. if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
  479. {
  480. $this -> SSOpen = false;
  481. $this -> SharedStringIndex++;
  482. }
  483. else
  484. {
  485. $this -> SSOpen = true;
  486. if ($this -> SharedStringIndex < $Index)
  487. {
  488. $this -> SSOpen = false;
  489. $this -> SharedStrings -> next('si');
  490. $this -> SSForwarded = true;
  491. $this -> SharedStringIndex++;
  492. continue;
  493. }
  494. else
  495. {
  496. break;
  497. }
  498. }
  499. }
  500. }
  501. $Value = '';
  502. // Extract the value from the shared string
  503. if ($this -> SSOpen && ($this -> SharedStringIndex == $Index))
  504. {
  505. while ($this -> SharedStrings -> read())
  506. {
  507. switch ($this -> SharedStrings -> name)
  508. {
  509. case 't':
  510. if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
  511. {
  512. continue;
  513. }
  514. $Value .= $this -> SharedStrings -> readString();
  515. break;
  516. case 'si':
  517. if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
  518. {
  519. $this -> SSOpen = false;
  520. $this -> SSForwarded = true;
  521. break 2;
  522. }
  523. break;
  524. }
  525. }
  526. }
  527. if ($Value)
  528. {
  529. $this -> LastSharedStringValue = $Value;
  530. }
  531. return $Value;
  532. }
  533. /**
  534. * Formats the value according to the index
  535. *
  536. * @param string Cell value
  537. * @param int Format index
  538. *
  539. * @return string Formatted cell value
  540. */
  541. private function FormatValue($Value, $Index)
  542. {
  543. if (!is_numeric($Value))
  544. {
  545. return $Value;
  546. }
  547. if (isset($this -> Styles[$Index]) && ($this -> Styles[$Index] !== false))
  548. {
  549. $Index = $this -> Styles[$Index];
  550. }
  551. else
  552. {
  553. return $Value;
  554. }
  555. // A special case for the "General" format
  556. if ($Index == 0)
  557. {
  558. return $this -> GeneralFormat($Value);
  559. }
  560. $Format = array();
  561. if (isset($this -> ParsedFormatCache[$Index]))
  562. {
  563. $Format = $this -> ParsedFormatCache[$Index];
  564. }
  565. if (!$Format)
  566. {
  567. $Format = array(
  568. 'Code' => false,
  569. 'Type' => false,
  570. 'Scale' => 1,
  571. 'Thousands' => false,
  572. 'Currency' => false
  573. );
  574. if (isset(self::$BuiltinFormats[$Index]))
  575. {
  576. $Format['Code'] = self::$BuiltinFormats[$Index];
  577. }
  578. elseif (isset($this -> Formats[$Index]))
  579. {
  580. $Format['Code'] = $this -> Formats[$Index];
  581. }
  582. // Format code found, now parsing the format
  583. if ($Format['Code'])
  584. {
  585. $Sections = explode(';', $Format['Code']);
  586. $Format['Code'] = $Sections[0];
  587. switch (count($Sections))
  588. {
  589. case 2:
  590. if ($Value < 0)
  591. {
  592. $Format['Code'] = $Sections[1];
  593. }
  594. break;
  595. case 3:
  596. case 4:
  597. if ($Value < 0)
  598. {
  599. $Format['Code'] = $Sections[1];
  600. }
  601. elseif ($Value == 0)
  602. {
  603. $Format['Code'] = $Sections[2];
  604. }
  605. break;
  606. }
  607. }
  608. // Stripping colors
  609. $Format['Code'] = trim(preg_replace('{^\[[[:alpha:]]+\]}i', '', $Format['Code']));
  610. // Percentages
  611. if (substr($Format['Code'], -1) == '%')
  612. {
  613. $Format['Type'] = 'Percentage';
  614. }
  615. elseif (preg_match('{^(\[\$[[:alpha:]]*-[0-9A-F]*\])*[hmsdy]}i', $Format['Code']))
  616. {
  617. $Format['Type'] = 'DateTime';
  618. $Format['Code'] = trim(preg_replace('{^(\[\$[[:alpha:]]*-[0-9A-F]*\])}i', '', $Format['Code']));
  619. $Format['Code'] = strtolower($Format['Code']);
  620. $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['All']);
  621. if (strpos($Format['Code'], 'A') === false)
  622. {
  623. $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['24H']);
  624. }
  625. else
  626. {
  627. $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['12H']);
  628. }
  629. }
  630. elseif ($Format['Code'] == '[$EUR ]#,##0.00_-')
  631. {
  632. $Format['Type'] = 'Euro';
  633. }
  634. else
  635. {
  636. // Removing skipped characters
  637. $Format['Code'] = preg_replace('{_.}', '', $Format['Code']);
  638. // Removing unnecessary escaping
  639. $Format['Code'] = preg_replace("{\\\\}", '', $Format['Code']);
  640. // Removing string quotes
  641. $Format['Code'] = str_replace(array('"', '*'), '', $Format['Code']);
  642. // Removing thousands separator
  643. if (strpos($Format['Code'], '0,0') !== false || strpos($Format['Code'], '#,#') !== false)
  644. {
  645. $Format['Thousands'] = true;
  646. }
  647. $Format['Code'] = str_replace(array('0,0', '#,#'), array('00', '##'), $Format['Code']);
  648. // Scaling (Commas indicate the power)
  649. $Scale = 1;
  650. $Matches = array();
  651. if (preg_match('{(0|#)(,+)}', $Format['Code'], $Matches))
  652. {
  653. $Scale = pow(1000, strlen($Matches[2]));
  654. // Removing the commas
  655. $Format['Code'] = preg_replace(array('{0,+}', '{#,+}'), array('0', '#'), $Format['Code']);
  656. }
  657. $Format['Scale'] = $Scale;
  658. if (preg_match('{#?.*\?\/\?}', $Format['Code']))
  659. {
  660. $Format['Type'] = 'Fraction';
  661. }
  662. else
  663. {
  664. $Format['Code'] = str_replace('#', '', $Format['Code']);
  665. $Matches = array();
  666. if (preg_match('{(0+)(\.?)(0*)}', preg_replace('{\[[^\]]+\]}', '', $Format['Code']), $Matches))
  667. {
  668. $Integer = $Matches[1];
  669. $DecimalPoint = $Matches[2];
  670. $Decimals = $Matches[3];
  671. $Format['MinWidth'] = strlen($Integer) + strlen($DecimalPoint) + strlen($Decimals);
  672. $Format['Decimals'] = $Decimals;
  673. $Format['Precision'] = strlen($Format['Decimals']);
  674. $Format['Pattern'] = '%0'.$Format['MinWidth'].'.'.$Format['Precision'].'f';
  675. }
  676. }
  677. $Matches = array();
  678. if (preg_match('{\[\$(.*)\]}u', $Format['Code'], $Matches))
  679. {
  680. $CurrFormat = $Matches[0];
  681. $CurrCode = $Matches[1];
  682. $CurrCode = explode('-', $CurrCode);
  683. if ($CurrCode)
  684. {
  685. $CurrCode = $CurrCode[0];
  686. }
  687. if (!$CurrCode)
  688. {
  689. $CurrCode = self::$CurrencyCode;
  690. }
  691. $Format['Currency'] = $CurrCode;
  692. }
  693. $Format['Code'] = trim($Format['Code']);
  694. }
  695. $this -> ParsedFormatCache[$Index] = $Format;
  696. }
  697. // Applying format to value
  698. if ($Format)
  699. {
  700. if ($Format['Code'] == '@')
  701. {
  702. return (string)$Value;
  703. }
  704. // Percentages
  705. elseif ($Format['Type'] == 'Percentage')
  706. {
  707. if ($Format['Code'] === '0%')
  708. {
  709. $Value = round(100 * $Value, 0).'%';
  710. }
  711. else
  712. {
  713. $Value = sprintf('%.2f%%', round(100 * $Value, 2));
  714. }
  715. }
  716. // Dates and times
  717. elseif ($Format['Type'] == 'DateTime')
  718. {
  719. $Days = (int)$Value;
  720. // Correcting for Feb 29, 1900
  721. if ($Days > 60)
  722. {
  723. $Days--;
  724. }
  725. // At this point time is a fraction of a day
  726. $Time = ($Value - (int)$Value);
  727. $Seconds = 0;
  728. if ($Time)
  729. {
  730. // Here time is converted to seconds
  731. // Some loss of precision will occur
  732. $Seconds = (int)($Time * 86400);
  733. }
  734. $Value = clone self::$BaseDate;
  735. $Value -> add(new DateInterval('P'.$Days.'D'.($Seconds ? 'T'.$Seconds.'S' : '')));
  736. if (!$this -> Options['ReturnDateTimeObjects'])
  737. {
  738. $Value = $Value -> format($Format['Code']);
  739. }
  740. else
  741. {
  742. // A DateTime object is returned
  743. }
  744. }
  745. elseif ($Format['Type'] == 'Euro')
  746. {
  747. $Value = 'EUR '.sprintf('%1.2f', $Value);
  748. }
  749. else
  750. {
  751. // Fractional numbers
  752. if ($Format['Type'] == 'Fraction' && ($Value != (int)$Value))
  753. {
  754. $Integer = floor(abs($Value));
  755. $Decimal = fmod(abs($Value), 1);
  756. // Removing the integer part and decimal point
  757. $Decimal *= pow(10, strlen($Decimal) - 2);
  758. $DecimalDivisor = pow(10, strlen($Decimal));
  759. if (self::$RuntimeInfo['GMPSupported'])
  760. {
  761. $GCD = gmp_strval(gmp_gcd($Decimal, $DecimalDivisor));
  762. }
  763. else
  764. {
  765. $GCD = self::GCD($Decimal, $DecimalDivisor);
  766. }
  767. $AdjDecimal = $DecimalPart/$GCD;
  768. $AdjDecimalDivisor = $DecimalDivisor/$GCD;
  769. if (
  770. strpos($Format['Code'], '0') !== false ||
  771. strpos($Format['Code'], '#') !== false ||
  772. substr($Format['Code'], 0, 3) == '? ?'
  773. )
  774. {
  775. // The integer part is shown separately apart from the fraction
  776. $Value = ($Value < 0 ? '-' : '').
  777. $Integer ? $Integer.' ' : ''.
  778. $AdjDecimal.'/'.
  779. $AdjDecimalDivisor;
  780. }
  781. else
  782. {
  783. // The fraction includes the integer part
  784. $AdjDecimal += $Integer * $AdjDecimalDivisor;
  785. $Value = ($Value < 0 ? '-' : '').
  786. $AdjDecimal.'/'.
  787. $AdjDecimalDivisor;
  788. }
  789. }
  790. else
  791. {
  792. // Scaling
  793. $Value = $Value / $Format['Scale'];
  794. if (!empty($Format['MinWidth']) && $Format['Decimals'])
  795. {
  796. if ($Format['Thousands'])
  797. {
  798. $Value = number_format($Value, $Format['Precision'],
  799. self::$DecimalSeparator, self::$ThousandSeparator);
  800. }
  801. else
  802. {
  803. $Value = sprintf($Format['Pattern'], $Value);
  804. }
  805. $Value = preg_replace('{(0+)(\.?)(0*)}', $Value, $Format['Code']);
  806. }
  807. }
  808. // Currency/Accounting
  809. if ($Format['Currency'])
  810. {
  811. $Value = preg_replace('', $Format['Currency'], $Value);
  812. }
  813. }
  814. }
  815. return $Value;
  816. }
  817. /**
  818. * Attempts to approximate Excel's "general" format.
  819. *
  820. * @param mixed Value
  821. *
  822. * @return mixed Result
  823. */
  824. public function GeneralFormat($Value)
  825. {
  826. // Numeric format
  827. if (is_numeric($Value))
  828. {
  829. $Value = (float)$Value;
  830. }
  831. return $Value;
  832. }
  833. // !Iterator interface methods
  834. /**
  835. * Rewind the Iterator to the first element.
  836. * Similar to the reset() function for arrays in PHP
  837. */
  838. public function rewind()
  839. {
  840. // Removed the check whether $this -> Index == 0 otherwise ChangeSheet doesn't work properly
  841. // If the worksheet was already iterated, XML file is reopened.
  842. // Otherwise it should be at the beginning anyway
  843. if ($this -> Worksheet instanceof XMLReader)
  844. {
  845. $this -> Worksheet -> close();
  846. }
  847. else
  848. {
  849. $this -> Worksheet = new XMLReader;
  850. }
  851. $this -> Worksheet -> open($this -> WorksheetPath);
  852. $this -> Valid = true;
  853. $this -> RowOpen = false;
  854. $this -> CurrentRow = false;
  855. $this -> Index = 0;
  856. }
  857. /**
  858. * Return the current element.
  859. * Similar to the current() function for arrays in PHP
  860. *
  861. * @return mixed current element from the collection
  862. */
  863. public function current()
  864. {
  865. if ($this -> Index == 0 && $this -> CurrentRow === false)
  866. {
  867. $this -> next();
  868. $this -> Index--;
  869. }
  870. return $this -> CurrentRow;
  871. }
  872. /**
  873. * Move forward to next element.
  874. * Similar to the next() function for arrays in PHP
  875. */
  876. public function next()
  877. {
  878. $this -> Index++;
  879. $this -> CurrentRow = array();
  880. if (!$this -> RowOpen)
  881. {
  882. while ($this -> Valid = $this -> Worksheet -> read())
  883. {
  884. if ($this -> Worksheet -> name == 'row')
  885. {
  886. // Getting the row spanning area (stored as e.g., 1:12)
  887. // so that the last cells will be present, even if empty
  888. $RowSpans = $this -> Worksheet -> getAttribute('spans');
  889. if ($RowSpans)
  890. {
  891. $RowSpans = explode(':', $RowSpans);
  892. $CurrentRowColumnCount = $RowSpans[1];
  893. }
  894. else
  895. {
  896. $CurrentRowColumnCount = 0;
  897. }
  898. if ($CurrentRowColumnCount > 0)
  899. {
  900. $this -> CurrentRow = array_fill(0, $CurrentRowColumnCount, '');
  901. }
  902. $this -> RowOpen = true;
  903. break;
  904. }
  905. }
  906. }
  907. // Reading the necessary row, if found
  908. if ($this -> RowOpen)
  909. {
  910. // These two are needed to control for empty cells
  911. $MaxIndex = 0;
  912. $CellCount = 0;
  913. $CellHasSharedString = false;
  914. while ($this -> Valid = $this -> Worksheet -> read())
  915. {
  916. switch ($this -> Worksheet -> name)
  917. {
  918. // End of row
  919. case 'row':
  920. if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
  921. {
  922. $this -> RowOpen = false;
  923. break 2;
  924. }
  925. break;
  926. // Cell
  927. case 'c':
  928. // If it is a closing tag, skip it
  929. if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
  930. {
  931. continue;
  932. }
  933. $StyleId = (int)$this -> Worksheet -> getAttribute('s');
  934. // Get the index of the cell
  935. $Index = $this -> Worksheet -> getAttribute('r');
  936. $Letter = preg_replace('{[^[:alpha:]]}S', '', $Index);
  937. $Index = self::IndexFromColumnLetter($Letter);
  938. // Determine cell type
  939. if ($this -> Worksheet -> getAttribute('t') == self::CELL_TYPE_SHARED_STR)
  940. {
  941. $CellHasSharedString = true;
  942. }
  943. else
  944. {
  945. $CellHasSharedString = false;
  946. }
  947. $this -> CurrentRow[$Index] = '';
  948. $CellCount++;
  949. if ($Index > $MaxIndex)
  950. {
  951. $MaxIndex = $Index;
  952. }
  953. break;
  954. // Cell value
  955. case 'v':
  956. case 'is':
  957. if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
  958. {
  959. continue;
  960. }
  961. $Value = $this -> Worksheet -> readString();
  962. if ($CellHasSharedString)
  963. {
  964. $Value = $this -> GetSharedString($Value);
  965. }
  966. // Format value if necessary
  967. if ($Value !== '' && $StyleId && isset($this -> Styles[$StyleId]))
  968. {
  969. $Value = $this -> FormatValue($Value, $StyleId);
  970. }
  971. elseif ($Value)
  972. {
  973. $Value = $this -> GeneralFormat($Value);
  974. }
  975. $this -> CurrentRow[$Index] = $Value;
  976. break;
  977. }
  978. }
  979. // Adding empty cells, if necessary
  980. // Only empty cells inbetween and on the left side are added
  981. if ($MaxIndex + 1 > $CellCount)
  982. {
  983. $this -> CurrentRow = $this -> CurrentRow + array_fill(0, $MaxIndex + 1, '');
  984. ksort($this -> CurrentRow);
  985. }
  986. }
  987. return $this -> CurrentRow;
  988. }
  989. /**
  990. * Return the identifying key of the current element.
  991. * Similar to the key() function for arrays in PHP
  992. *
  993. * @return mixed either an integer or a string
  994. */
  995. public function key()
  996. {
  997. return $this -> Index;
  998. }
  999. /**
  1000. * Check if there is a current element after calls to rewind() or next().
  1001. * Used to check if we've iterated to the end of the collection
  1002. *
  1003. * @return boolean FALSE if there's nothing more to iterate over
  1004. */
  1005. public function valid()
  1006. {
  1007. return $this -> Valid;
  1008. }
  1009. // !Countable interface method
  1010. /**
  1011. * Ostensibly should return the count of the contained items but this just returns the number
  1012. * of rows read so far. It's not really correct but at least coherent.
  1013. */
  1014. public function count()
  1015. {
  1016. return $this -> Index + 1;
  1017. }
  1018. /**
  1019. * Takes the column letter and converts it to a numerical index (0-based)
  1020. *
  1021. * @param string Letter(s) to convert
  1022. *
  1023. * @return mixed Numeric index (0-based) or boolean false if it cannot be calculated
  1024. */
  1025. public static function IndexFromColumnLetter($Letter)
  1026. {
  1027. $Powers = array();
  1028. $Letter = strtoupper($Letter);
  1029. $Result = 0;
  1030. for ($i = strlen($Letter) - 1, $j = 0; $i >= 0; $i--, $j++)
  1031. {
  1032. $Ord = ord($Letter[$i]) - 64;
  1033. if ($Ord > 26)
  1034. {
  1035. // Something is very, very wrong
  1036. return false;
  1037. }
  1038. $Result += $Ord * pow(26, $j);
  1039. }
  1040. return $Result - 1;
  1041. }
  1042. /**
  1043. * Helper function for greatest common divisor calculation in case GMP extension is
  1044. * not enabled
  1045. *
  1046. * @param int Number #1
  1047. * @param int Number #2
  1048. *
  1049. * @param int Greatest common divisor
  1050. */
  1051. public static function GCD($A, $B)
  1052. {
  1053. $A = abs($A);
  1054. $B = abs($B);
  1055. if ($A + $B == 0)
  1056. {
  1057. return 0;
  1058. }
  1059. else
  1060. {
  1061. $C = 1;
  1062. while ($A > 0)
  1063. {
  1064. $C = $A;
  1065. $A = $B % $A;
  1066. $B = $C;
  1067. }
  1068. return $C;
  1069. }
  1070. }
  1071. }
  1072. ?>