XLSX.php 29 KB

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