shop.balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

spreadsheetreader_xlsx.php (27903B)


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