12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211 |
- <?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;
- }
- }
- }
- ?>
|