123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212 |
- <?php
- /**
- * Class for parsing XLSX files specifically
- *
- * @author Martins Pilsetnieks
- */
- class SpreadsheetReader_XLSX implements Iterator, Countable
- {
- const CELL_TYPE_BOOL = 'b';
- const CELL_TYPE_NUMBER = 'n';
- const CELL_TYPE_ERROR = 'e';
- const CELL_TYPE_SHARED_STR = 's';
- const CELL_TYPE_STR = 'str';
- const CELL_TYPE_INLINE_STR = 'inlineStr';
- /**
- * Number of shared strings that can be reasonably cached, i.e., that aren't read from file but stored in memory.
- * If the total number of shared strings is higher than this, caching is not used.
- * If this value is null, shared strings are cached regardless of amount.
- * With large shared string caches there are huge performance gains, however a lot of memory could be used which
- * can be a problem, especially on shared hosting.
- */
- const SHARED_STRING_CACHE_LIMIT = 50000;
- private $Options = array(
- 'TempDir' => '',
- 'ReturnDateTimeObjects' => false
- );
- private static $RuntimeInfo = array(
- 'GMPSupported' => false
- );
- private $Valid = false;
- /**
- * @var SpreadsheetReader_* Handle for the reader object
- */
- private $Handle = false;
- // Worksheet file
- /**
- * @var string Path to the worksheet XML file
- */
- private $WorksheetPath = false;
- /**
- * @var XMLReader XML reader object for the worksheet XML file
- */
- private $Worksheet = false;
- // Shared strings file
- /**
- * @var string Path to shared strings XML file
- */
- private $SharedStringsPath = false;
- /**
- * @var XMLReader XML reader object for the shared strings XML file
- */
- private $SharedStrings = false;
- /**
- * @var array Shared strings cache, if the number of shared strings is low enough
- */
- private $SharedStringCache = array();
- // Workbook data
- /**
- * @var SimpleXMLElement XML object for the workbook XML file
- */
- private $WorkbookXML = false;
- // Style data
- /**
- * @var SimpleXMLElement XML object for the styles XML file
- */
- private $StylesXML = false;
- /**
- * @var array Container for cell value style data
- */
- private $Styles = array();
- private $TempDir = '';
- private $TempFiles = array();
- private $CurrentRow = false;
- // Runtime parsing data
- /**
- * @var int Current row in the file
- */
- private $Index = 0;
- /**
- * @var array Data about separate sheets in the file
- */
- private $Sheets = false;
- private $SharedStringCount = 0;
- private $SharedStringIndex = 0;
- private $LastSharedStringValue = null;
- private $RowOpen = false;
- private $SSOpen = false;
- private $SSForwarded = false;
- private static $BuiltinFormats = array(
- 0 => '',
- 1 => '0',
- 2 => '0.00',
- 3 => '#,##0',
- 4 => '#,##0.00',
- 9 => '0%',
- 10 => '0.00%',
- 11 => '0.00E+00',
- 12 => '# ?/?',
- 13 => '# ??/??',
- 14 => 'mm-dd-yy',
- 15 => 'd-mmm-yy',
- 16 => 'd-mmm',
- 17 => 'mmm-yy',
- 18 => 'h:mm AM/PM',
- 19 => 'h:mm:ss AM/PM',
- 20 => 'h:mm',
- 21 => 'h:mm:ss',
- 22 => 'm/d/yy h:mm',
- 37 => '#,##0 ;(#,##0)',
- 38 => '#,##0 ;[Red](#,##0)',
- 39 => '#,##0.00;(#,##0.00)',
- 40 => '#,##0.00;[Red](#,##0.00)',
- 45 => 'mm:ss',
- 46 => '[h]:mm:ss',
- 47 => 'mmss.0',
- 48 => '##0.0E+0',
- 49 => '@',
- // CHT & CHS
- 27 => '[$-404]e/m/d',
- 30 => 'm/d/yy',
- 36 => '[$-404]e/m/d',
- 50 => '[$-404]e/m/d',
- 57 => '[$-404]e/m/d',
- // THA
- 59 => 't0',
- 60 => 't0.00',
- 61 =>'t#,##0',
- 62 => 't#,##0.00',
- 67 => 't0%',
- 68 => 't0.00%',
- 69 => 't# ?/?',
- 70 => 't# ??/??'
- );
- private $Formats = array();
- private static $DateReplacements = array(
- 'All' => array(
- '\\' => '',
- 'am/pm' => 'A',
- 'yyyy' => 'Y',
- 'yy' => 'y',
- 'mmmmm' => 'M',
- 'mmmm' => 'F',
- 'mmm' => 'M',
- ':mm' => ':i',
- 'mm' => 'm',
- 'm' => 'n',
- 'dddd' => 'l',
- 'ddd' => 'D',
- 'dd' => 'd',
- 'd' => 'j',
- 'ss' => 's',
- '.s' => ''
- ),
- '24H' => array(
- 'hh' => 'H',
- 'h' => 'G'
- ),
- '12H' => array(
- 'hh' => 'h',
- 'h' => 'G'
- )
- );
- private static $BaseDate = false;
- private static $DecimalSeparator = '.';
- private static $ThousandSeparator = '';
- private static $CurrencyCode = '';
- /**
- * @var array Cache for already processed format strings
- */
- private $ParsedFormatCache = array();
- /**
- * @param string Path to file
- * @param array Options:
- * TempDir => string Temporary directory path
- * ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects, false => as strings
- */
- public function __construct($Filepath, array $Options = null)
- {
- if (!is_readable($Filepath))
- {
- throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.')');
- }
- $this -> TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ?
- $Options['TempDir'] :
- sys_get_temp_dir();
- $this -> TempDir = rtrim($this -> TempDir, DIRECTORY_SEPARATOR);
- $this -> TempDir = $this -> TempDir.DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR;
- $Zip = new ZipArchive;
- $Status = $Zip -> open($Filepath);
- if ($Status !== true)
- {
- throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.') (Error '.$Status.')');
- }
- // Getting the general workbook information
- if ($Zip -> locateName('xl/workbook.xml') !== false)
- {
- $this -> WorkbookXML = new SimpleXMLElement($Zip -> getFromName('xl/workbook.xml'));
- }
- // Extracting the XMLs from the XLSX zip file
- if ($Zip -> locateName('xl/sharedStrings.xml') !== false)
- {
- $this -> SharedStringsPath = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml';
- $Zip -> extractTo($this -> TempDir, 'xl/sharedStrings.xml');
- $this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml';
- if (is_readable($this -> SharedStringsPath))
- {
- $this -> SharedStrings = new XMLReader;
- $this -> SharedStrings -> open($this -> SharedStringsPath);
- $this -> PrepareSharedStringCache();
- }
- }
- $Sheets = $this -> Sheets();
- foreach ($this -> Sheets as $Index => $Name)
- {
- if ($Zip -> locateName('xl/worksheets/sheet'.$Index.'.xml') !== false)
- {
- $Zip -> extractTo($this -> TempDir, 'xl/worksheets/sheet'.$Index.'.xml');
- $this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets'.DIRECTORY_SEPARATOR.'sheet'.$Index.'.xml';
- }
- }
- $this -> ChangeSheet(0);
- // If worksheet is present and is OK, parse the styles already
- if ($Zip -> locateName('xl/styles.xml') !== false)
- {
- $this -> StylesXML = new SimpleXMLElement($Zip -> getFromName('xl/styles.xml'));
- if ($this -> StylesXML && $this -> StylesXML -> cellXfs && $this -> StylesXML -> cellXfs -> xf)
- {
- foreach ($this -> StylesXML -> cellXfs -> xf as $Index => $XF)
- {
- // Format #0 is a special case - it is the "General" format that is applied regardless of applyNumberFormat
- if ($XF -> attributes() -> applyNumberFormat || (0 == (int)$XF -> attributes() -> numFmtId))
- {
- $FormatId = (int)$XF -> attributes() -> numFmtId;
- // If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts
- $this -> Styles[] = $FormatId;
- }
- else
- {
- // 0 for "General" format
- $this -> Styles[] = 0;
- }
- }
- }
-
- if ($this -> StylesXML -> numFmts && $this -> StylesXML -> numFmts -> numFmt)
- {
- foreach ($this -> StylesXML -> numFmts -> numFmt as $Index => $NumFmt)
- {
- $this -> Formats[(int)$NumFmt -> attributes() -> numFmtId] = (string)$NumFmt -> attributes() -> formatCode;
- }
- }
- unset($this -> StylesXML);
- }
- $Zip -> close();
- // Setting base date
- if (!self::$BaseDate)
- {
- self::$BaseDate = new DateTime;
- self::$BaseDate -> setTimezone(new DateTimeZone('UTC'));
- self::$BaseDate -> setDate(1900, 1, 0);
- self::$BaseDate -> setTime(0, 0, 0);
- }
- // Decimal and thousand separators
- if (!self::$DecimalSeparator && !self::$ThousandSeparator && !self::$CurrencyCode)
- {
- $Locale = localeconv();
- self::$DecimalSeparator = $Locale['decimal_point'];
- self::$ThousandSeparator = $Locale['thousands_sep'];
- self::$CurrencyCode = $Locale['int_curr_symbol'];
- }
- if (function_exists('gmp_gcd'))
- {
- self::$RuntimeInfo['GMPSupported'] = true;
- }
- }
- /**
- * Destructor, destroys all that remains (closes and deletes temp files)
- */
- public function __destruct()
- {
- foreach ($this -> TempFiles as $TempFile)
- {
- @unlink($TempFile);
- }
- // Better safe than sorry - shouldn't try deleting '.' or '/', or '..'.
- if (strlen($this -> TempDir) > 2)
- {
- @rmdir($this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets');
- @rmdir($this -> TempDir.'xl');
- @rmdir($this -> TempDir);
- }
- if ($this -> Worksheet && $this -> Worksheet instanceof XMLReader)
- {
- $this -> Worksheet -> close();
- unset($this -> Worksheet);
- }
- unset($this -> WorksheetPath);
- if ($this -> SharedStrings && $this -> SharedStrings instanceof XMLReader)
- {
- $this -> SharedStrings -> close();
- unset($this -> SharedStrings);
- }
- unset($this -> SharedStringsPath);
- if (isset($this -> StylesXML))
- {
- unset($this -> StylesXML);
- }
- if ($this -> WorkbookXML)
- {
- unset($this -> WorkbookXML);
- }
- }
- /**
- * Retrieves an array with information about sheets in the current file
- *
- * @return array List of sheets (key is sheet index, value is name)
- */
- public function Sheets()
- {
- if ($this -> Sheets === false)
- {
- $this -> Sheets = array();
- foreach ($this -> WorkbookXML -> sheets -> sheet as $Index => $Sheet)
- {
- $Attributes = $Sheet -> attributes('r', true);
- foreach ($Attributes as $Name => $Value)
- {
- if ($Name == 'id')
- {
- $SheetID = (int)str_replace('rId', '', (string)$Value);
- break;
- }
- }
- $this -> Sheets[$SheetID] = (string)$Sheet['name'];
- }
- ksort($this -> Sheets);
- }
- return array_values($this -> Sheets);
- }
- /**
- * Changes the current sheet in the file to another
- *
- * @param int Sheet index
- *
- * @return bool True if sheet was successfully changed, false otherwise.
- */
- public function ChangeSheet($Index)
- {
- $RealSheetIndex = false;
- $Sheets = $this -> Sheets();
- if (isset($Sheets[$Index]))
- {
- $SheetIndexes = array_keys($this -> Sheets);
- $RealSheetIndex = $SheetIndexes[$Index];
- }
- $TempWorksheetPath = $this -> TempDir.'xl/worksheets/sheet'.$RealSheetIndex.'.xml';
- if ($RealSheetIndex !== false && is_readable($TempWorksheetPath))
- {
- $this -> WorksheetPath = $TempWorksheetPath;
- $this -> rewind();
- return true;
- }
- return false;
- }
- /**
- * Creating shared string cache if the number of shared strings is acceptably low (or there is no limit on the amount
- */
- private function PrepareSharedStringCache()
- {
- while ($this -> SharedStrings -> read())
- {
- if ($this -> SharedStrings -> name == 'sst')
- {
- $this -> SharedStringCount = $this -> SharedStrings -> getAttribute('count');
- break;
- }
- }
- if (!$this -> SharedStringCount || (self::SHARED_STRING_CACHE_LIMIT < $this -> SharedStringCount && self::SHARED_STRING_CACHE_LIMIT !== null))
- {
- return false;
- }
- $CacheIndex = 0;
- $CacheValue = '';
- while ($this -> SharedStrings -> read())
- {
- switch ($this -> SharedStrings -> name)
- {
- case 'si':
- if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
- {
- $this -> SharedStringCache[$CacheIndex] = $CacheValue;
- $CacheIndex++;
- $CacheValue = '';
- }
- break;
- case 't':
- if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
- {
- continue;
- }
- $CacheValue .= $this -> SharedStrings -> readString();
- break;
- }
- }
- $this -> SharedStrings -> close();
- return true;
- }
- /**
- * Retrieves a shared string value by its index
- *
- * @param int Shared string index
- *
- * @return string Value
- */
- private function GetSharedString($Index)
- {
- if ((self::SHARED_STRING_CACHE_LIMIT === null || self::SHARED_STRING_CACHE_LIMIT > 0) && !empty($this -> SharedStringCache))
- {
- if (isset($this -> SharedStringCache[$Index]))
- {
- return $this -> SharedStringCache[$Index];
- }
- else
- {
- return '';
- }
- }
- // If the desired index is before the current, rewind the XML
- if ($this -> SharedStringIndex > $Index)
- {
- $this -> SSOpen = false;
- $this -> SharedStrings -> close();
- $this -> SharedStrings -> open($this -> SharedStringsPath);
- $this -> SharedStringIndex = 0;
- $this -> LastSharedStringValue = null;
- $this -> SSForwarded = false;
- }
- // Finding the unique string count (if not already read)
- if ($this -> SharedStringIndex == 0 && !$this -> SharedStringCount)
- {
- while ($this -> SharedStrings -> read())
- {
- if ($this -> SharedStrings -> name == 'sst')
- {
- $this -> SharedStringCount = $this -> SharedStrings -> getAttribute('uniqueCount');
- break;
- }
- }
- }
- // If index of the desired string is larger than possible, don't even bother.
- if ($this -> SharedStringCount && ($Index >= $this -> SharedStringCount))
- {
- return '';
- }
- // If an index with the same value as the last already fetched is requested
- // (any further traversing the tree would get us further away from the node)
- if (($Index == $this -> SharedStringIndex) && ($this -> LastSharedStringValue !== null))
- {
- return $this -> LastSharedStringValue;
- }
- // Find the correct <si> node with the desired index
- while ($this -> SharedStringIndex <= $Index)
- {
- // SSForwarded is set further to avoid double reading in case nodes are skipped.
- if ($this -> SSForwarded)
- {
- $this -> SSForwarded = false;
- }
- else
- {
- $ReadStatus = $this -> SharedStrings -> read();
- if (!$ReadStatus)
- {
- break;
- }
- }
- if ($this -> SharedStrings -> name == 'si')
- {
- if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
- {
- $this -> SSOpen = false;
- $this -> SharedStringIndex++;
- }
- else
- {
- $this -> SSOpen = true;
-
- if ($this -> SharedStringIndex < $Index)
- {
- $this -> SSOpen = false;
- $this -> SharedStrings -> next('si');
- $this -> SSForwarded = true;
- $this -> SharedStringIndex++;
- continue;
- }
- else
- {
- break;
- }
- }
- }
- }
- $Value = '';
- // Extract the value from the shared string
- if ($this -> SSOpen && ($this -> SharedStringIndex == $Index))
- {
- while ($this -> SharedStrings -> read())
- {
- switch ($this -> SharedStrings -> name)
- {
- case 't':
- if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
- {
- continue;
- }
- $Value .= $this -> SharedStrings -> readString();
- break;
- case 'si':
- if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
- {
- $this -> SSOpen = false;
- $this -> SSForwarded = true;
- break 2;
- }
- break;
- }
- }
- }
- if ($Value)
- {
- $this -> LastSharedStringValue = $Value;
- }
- return $Value;
- }
- /**
- * Formats the value according to the index
- *
- * @param string Cell value
- * @param int Format index
- *
- * @return string Formatted cell value
- */
- private function FormatValue($Value, $Index)
- {
- if (!is_numeric($Value))
- {
- return $Value;
- }
- if (isset($this -> Styles[$Index]) && ($this -> Styles[$Index] !== false))
- {
- $Index = $this -> Styles[$Index];
- }
- else
- {
- return $Value;
- }
- // A special case for the "General" format
- if ($Index == 0)
- {
- return $this -> GeneralFormat($Value);
- }
- $Format = array();
- if (isset($this -> ParsedFormatCache[$Index]))
- {
- $Format = $this -> ParsedFormatCache[$Index];
- }
- if (!$Format)
- {
- $Format = array(
- 'Code' => false,
- 'Type' => false,
- 'Scale' => 1,
- 'Thousands' => false,
- 'Currency' => false
- );
- if (isset(self::$BuiltinFormats[$Index]))
- {
- $Format['Code'] = self::$BuiltinFormats[$Index];
- }
- elseif (isset($this -> Formats[$Index]))
- {
- $Format['Code'] = $this -> Formats[$Index];
- }
- // Format code found, now parsing the format
- if ($Format['Code'])
- {
- $Sections = explode(';', $Format['Code']);
- $Format['Code'] = $Sections[0];
-
- switch (count($Sections))
- {
- case 2:
- if ($Value < 0)
- {
- $Format['Code'] = $Sections[1];
- }
- break;
- case 3:
- case 4:
- if ($Value < 0)
- {
- $Format['Code'] = $Sections[1];
- }
- elseif ($Value == 0)
- {
- $Format['Code'] = $Sections[2];
- }
- break;
- }
- }
- // Stripping colors
- $Format['Code'] = trim(preg_replace('{^\[[[:alpha:]]+\]}i', '', $Format['Code']));
- // Percentages
- if (substr($Format['Code'], -1) == '%')
- {
- $Format['Type'] = 'Percentage';
- }
- elseif (preg_match('{^(\[\$[[:alpha:]]*-[0-9A-F]*\])*[hmsdy]}i', $Format['Code']))
- {
- $Format['Type'] = 'DateTime';
- $Format['Code'] = trim(preg_replace('{^(\[\$[[:alpha:]]*-[0-9A-F]*\])}i', '', $Format['Code']));
- $Format['Code'] = strtolower($Format['Code']);
- $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['All']);
- if (strpos($Format['Code'], 'A') === false)
- {
- $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['24H']);
- }
- else
- {
- $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['12H']);
- }
- }
- elseif ($Format['Code'] == '[$EUR ]#,##0.00_-')
- {
- $Format['Type'] = 'Euro';
- }
- else
- {
- // Removing skipped characters
- $Format['Code'] = preg_replace('{_.}', '', $Format['Code']);
- // Removing unnecessary escaping
- $Format['Code'] = preg_replace("{\\\\}", '', $Format['Code']);
- // Removing string quotes
- $Format['Code'] = str_replace(array('"', '*'), '', $Format['Code']);
- // Removing thousands separator
- if (strpos($Format['Code'], '0,0') !== false || strpos($Format['Code'], '#,#') !== false)
- {
- $Format['Thousands'] = true;
- }
- $Format['Code'] = str_replace(array('0,0', '#,#'), array('00', '##'), $Format['Code']);
- // Scaling (Commas indicate the power)
- $Scale = 1;
- $Matches = array();
- if (preg_match('{(0|#)(,+)}', $Format['Code'], $Matches))
- {
- $Scale = pow(1000, strlen($Matches[2]));
- // Removing the commas
- $Format['Code'] = preg_replace(array('{0,+}', '{#,+}'), array('0', '#'), $Format['Code']);
- }
- $Format['Scale'] = $Scale;
- if (preg_match('{#?.*\?\/\?}', $Format['Code']))
- {
- $Format['Type'] = 'Fraction';
- }
- else
- {
- $Format['Code'] = str_replace('#', '', $Format['Code']);
- $Matches = array();
- if (preg_match('{(0+)(\.?)(0*)}', preg_replace('{\[[^\]]+\]}', '', $Format['Code']), $Matches))
- {
- $Integer = $Matches[1];
- $DecimalPoint = $Matches[2];
- $Decimals = $Matches[3];
- $Format['MinWidth'] = strlen($Integer) + strlen($DecimalPoint) + strlen($Decimals);
- $Format['Decimals'] = $Decimals;
- $Format['Precision'] = strlen($Format['Decimals']);
- $Format['Pattern'] = '%0'.$Format['MinWidth'].'.'.$Format['Precision'].'f';
- }
- }
- $Matches = array();
- if (preg_match('{\[\$(.*)\]}u', $Format['Code'], $Matches))
- {
- $CurrFormat = $Matches[0];
- $CurrCode = $Matches[1];
- $CurrCode = explode('-', $CurrCode);
- if ($CurrCode)
- {
- $CurrCode = $CurrCode[0];
- }
- if (!$CurrCode)
- {
- $CurrCode = self::$CurrencyCode;
- }
- $Format['Currency'] = $CurrCode;
- }
- $Format['Code'] = trim($Format['Code']);
- }
- $this -> ParsedFormatCache[$Index] = $Format;
- }
- // Applying format to value
- if ($Format)
- {
- if ($Format['Code'] == '@')
- {
- return (string)$Value;
- }
- // Percentages
- elseif ($Format['Type'] == 'Percentage')
- {
- if ($Format['Code'] === '0%')
- {
- $Value = round(100 * $Value, 0).'%';
- }
- else
- {
- $Value = sprintf('%.2f%%', round(100 * $Value, 2));
- }
- }
- // Dates and times
- elseif ($Format['Type'] == 'DateTime')
- {
- $Days = (int)$Value;
- // Correcting for Feb 29, 1900
- if ($Days > 60)
- {
- $Days--;
- }
- // At this point time is a fraction of a day
- $Time = ($Value - (int)$Value);
- $Seconds = 0;
- if ($Time)
- {
- // Here time is converted to seconds
- // Some loss of precision will occur
- $Seconds = (int)($Time * 86400);
- }
- $Value = clone self::$BaseDate;
- $Value -> add(new DateInterval('P'.$Days.'D'.($Seconds ? 'T'.$Seconds.'S' : '')));
- if (!$this -> Options['ReturnDateTimeObjects'])
- {
- $Value = $Value -> format($Format['Code']);
- }
- else
- {
- // A DateTime object is returned
- }
- }
- elseif ($Format['Type'] == 'Euro')
- {
- $Value = 'EUR '.sprintf('%1.2f', $Value);
- }
- else
- {
- // Fractional numbers
- if ($Format['Type'] == 'Fraction' && ($Value != (int)$Value))
- {
- $Integer = floor(abs($Value));
- $Decimal = fmod(abs($Value), 1);
- // Removing the integer part and decimal point
- $Decimal *= pow(10, strlen($Decimal) - 2);
- $DecimalDivisor = pow(10, strlen($Decimal));
- if (self::$RuntimeInfo['GMPSupported'])
- {
- $GCD = gmp_strval(gmp_gcd($Decimal, $DecimalDivisor));
- }
- else
- {
- $GCD = self::GCD($Decimal, $DecimalDivisor);
- }
- $AdjDecimal = $DecimalPart/$GCD;
- $AdjDecimalDivisor = $DecimalDivisor/$GCD;
- if (
- strpos($Format['Code'], '0') !== false ||
- strpos($Format['Code'], '#') !== false ||
- substr($Format['Code'], 0, 3) == '? ?'
- )
- {
- // The integer part is shown separately apart from the fraction
- $Value = ($Value < 0 ? '-' : '').
- $Integer ? $Integer.' ' : ''.
- $AdjDecimal.'/'.
- $AdjDecimalDivisor;
- }
- else
- {
- // The fraction includes the integer part
- $AdjDecimal += $Integer * $AdjDecimalDivisor;
- $Value = ($Value < 0 ? '-' : '').
- $AdjDecimal.'/'.
- $AdjDecimalDivisor;
- }
- }
- else
- {
- // Scaling
- $Value = $Value / $Format['Scale'];
- if (!empty($Format['MinWidth']) && $Format['Decimals'])
- {
- if ($Format['Thousands'])
- {
- $Value = number_format($Value, $Format['Precision'],
- self::$DecimalSeparator, self::$ThousandSeparator);
- }
- else
- {
- $Value = sprintf($Format['Pattern'], $Value);
- }
- $Value = preg_replace('{(0+)(\.?)(0*)}', $Value, $Format['Code']);
- }
- }
- // Currency/Accounting
- if ($Format['Currency'])
- {
- $Value = preg_replace('', $Format['Currency'], $Value);
- }
- }
-
- }
- return $Value;
- }
- /**
- * Attempts to approximate Excel's "general" format.
- *
- * @param mixed Value
- *
- * @return mixed Result
- */
- public function GeneralFormat($Value)
- {
- // Numeric format
- if (is_numeric($Value))
- {
- $Value = (float)$Value;
- }
- return $Value;
- }
- // !Iterator interface methods
- /**
- * Rewind the Iterator to the first element.
- * Similar to the reset() function for arrays in PHP
- */
- public function rewind()
- {
- // Removed the check whether $this -> Index == 0 otherwise ChangeSheet doesn't work properly
- // If the worksheet was already iterated, XML file is reopened.
- // Otherwise it should be at the beginning anyway
- if ($this -> Worksheet instanceof XMLReader)
- {
- $this -> Worksheet -> close();
- }
- else
- {
- $this -> Worksheet = new XMLReader;
- }
- $this -> Worksheet -> open($this -> WorksheetPath);
- $this -> Valid = true;
- $this -> RowOpen = false;
- $this -> CurrentRow = false;
- $this -> Index = 0;
- }
- /**
- * Return the current element.
- * Similar to the current() function for arrays in PHP
- *
- * @return mixed current element from the collection
- */
- public function current()
- {
- if ($this -> Index == 0 && $this -> CurrentRow === false)
- {
- $this -> next();
- $this -> Index--;
- }
- return $this -> CurrentRow;
- }
- /**
- * Move forward to next element.
- * Similar to the next() function for arrays in PHP
- */
- public function next()
- {
- $this -> Index++;
- $this -> CurrentRow = array();
- if (!$this -> RowOpen)
- {
- while ($this -> Valid = $this -> Worksheet -> read())
- {
- if ($this -> Worksheet -> name == 'row')
- {
- // Getting the row spanning area (stored as e.g., 1:12)
- // so that the last cells will be present, even if empty
- $RowSpans = $this -> Worksheet -> getAttribute('spans');
- if ($RowSpans)
- {
- $RowSpans = explode(':', $RowSpans);
- $CurrentRowColumnCount = $RowSpans[1];
- }
- else
- {
- $CurrentRowColumnCount = 0;
- }
- if ($CurrentRowColumnCount > 0)
- {
- $this -> CurrentRow = array_fill(0, $CurrentRowColumnCount, '');
- }
- $this -> RowOpen = true;
- break;
- }
- }
- }
- // Reading the necessary row, if found
- if ($this -> RowOpen)
- {
- // These two are needed to control for empty cells
- $MaxIndex = 0;
- $CellCount = 0;
- $CellHasSharedString = false;
- while ($this -> Valid = $this -> Worksheet -> read())
- {
- switch ($this -> Worksheet -> name)
- {
- // End of row
- case 'row':
- if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
- {
- $this -> RowOpen = false;
- break 2;
- }
- break;
- // Cell
- case 'c':
- // If it is a closing tag, skip it
- if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
- {
- continue;
- }
- $StyleId = (int)$this -> Worksheet -> getAttribute('s');
- // Get the index of the cell
- $Index = $this -> Worksheet -> getAttribute('r');
- $Letter = preg_replace('{[^[:alpha:]]}S', '', $Index);
- $Index = self::IndexFromColumnLetter($Letter);
- // Determine cell type
- if ($this -> Worksheet -> getAttribute('t') == self::CELL_TYPE_SHARED_STR)
- {
- $CellHasSharedString = true;
- }
- else
- {
- $CellHasSharedString = false;
- }
- $this -> CurrentRow[$Index] = '';
- $CellCount++;
- if ($Index > $MaxIndex)
- {
- $MaxIndex = $Index;
- }
- break;
- // Cell value
- case 'v':
- case 'is':
- if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
- {
- continue;
- }
- $Value = $this -> Worksheet -> readString();
- if ($CellHasSharedString)
- {
- $Value = $this -> GetSharedString($Value);
- }
- // Format value if necessary
- if ($Value !== '' && $StyleId && isset($this -> Styles[$StyleId]))
- {
- $Value = $this -> FormatValue($Value, $StyleId);
- }
- elseif ($Value)
- {
- $Value = $this -> GeneralFormat($Value);
- }
- $this -> CurrentRow[$Index] = $Value;
- break;
- }
- }
- // Adding empty cells, if necessary
- // Only empty cells inbetween and on the left side are added
- if ($MaxIndex + 1 > $CellCount)
- {
- $this -> CurrentRow = $this -> CurrentRow + array_fill(0, $MaxIndex + 1, '');
- ksort($this -> CurrentRow);
- }
- }
- return $this -> CurrentRow;
- }
- /**
- * Return the identifying key of the current element.
- * Similar to the key() function for arrays in PHP
- *
- * @return mixed either an integer or a string
- */
- public function key()
- {
- return $this -> Index;
- }
- /**
- * Check if there is a current element after calls to rewind() or next().
- * Used to check if we've iterated to the end of the collection
- *
- * @return boolean FALSE if there's nothing more to iterate over
- */
- public function valid()
- {
- return $this -> Valid;
- }
- // !Countable interface method
- /**
- * Ostensibly should return the count of the contained items but this just returns the number
- * of rows read so far. It's not really correct but at least coherent.
- */
- public function count()
- {
- return $this -> Index + 1;
- }
- /**
- * Takes the column letter and converts it to a numerical index (0-based)
- *
- * @param string Letter(s) to convert
- *
- * @return mixed Numeric index (0-based) or boolean false if it cannot be calculated
- */
- public static function IndexFromColumnLetter($Letter)
- {
- $Powers = array();
- $Letter = strtoupper($Letter);
- $Result = 0;
- for ($i = strlen($Letter) - 1, $j = 0; $i >= 0; $i--, $j++)
- {
- $Ord = ord($Letter[$i]) - 64;
- if ($Ord > 26)
- {
- // Something is very, very wrong
- return false;
- }
- $Result += $Ord * pow(26, $j);
- }
- return $Result - 1;
- }
- /**
- * Helper function for greatest common divisor calculation in case GMP extension is
- * not enabled
- *
- * @param int Number #1
- * @param int Number #2
- *
- * @param int Greatest common divisor
- */
- public static function GCD($A, $B)
- {
- $A = abs($A);
- $B = abs($B);
- if ($A + $B == 0)
- {
- return 0;
- }
- else
- {
- $C = 1;
- while ($A > 0)
- {
- $C = $A;
- $A = $B % $A;
- $B = $C;
- }
- return $C;
- }
- }
- }
- ?>
|