SpreadsheetReader_XLSX.php 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211
  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. ?>