<?php

class PHPExcel_Reader_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;

    private $rowCount = null;

    // 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 => 'yyyy/m/d',
        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 => 'yyyy/m/d h:mm',
        31 => 'yyyy年m月d日',
        32 => 'h时mmi分',
        33 => 'h时mmi分ss秒',
        37 => '#,##0 ;(#,##0)',
        38 => '#,##0 ;[Red](#,##0)',
        39 => '#,##0.00;(#,##0.00)',
        40 => '#,##0.00;[Red](#,##0.00)',
        44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
        45 => 'mm:ss',
        46 => '[h]:mm:ss',
        47 => 'mm:ss.0',
        48 => '##0.0E+0',
        49 => '@',
        55 => 'AM/PM h时mmi分',
        56 => 'AM/PM h时mmi分ss秒',
        58 => 'm月d日', // CHT & CHS
        27 => 'yyyy年m月',
        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',
            'e' => 'Y',
            'yyyy' => 'Y',
            'yy' => 'y',
            'mmmmm' => 'M',
            'mmmm' => 'F',
            'mmm' => 'M',
            ':mm' => ':i',
            'mmi' => '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 $Filepath Path to file
     * @param array $Options  Options:
     *                        TempDir => string Temporary directory path
     *                        ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects,
     *                        false => as strings
     * @throws Exception
     */
    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'] = str_replace('"', '', $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];
                        }
                        $Value = abs($Value);
                        break;
                    case 3:
                    case 4:
                        if ($Value < 0) {
                            $Format['Code'] = $Sections[1];
                        } elseif ($Value == 0) {
                            $Format['Code'] = $Sections[2];
                        }
                        $Value = abs($Value);
                        break;
                }
            }

            // Stripping colors
            $Format['Code'] = trim(preg_replace('/^\\[[a-zA-Z]+\\]/', '', $Format['Code']));

            // Percentages
            if (substr($Format['Code'], -1) == '%') {
                $Format['Type'] = 'Percentage';
            } elseif (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $Format['Code'])) {
                $Format['Type'] = 'DateTime';
                $Format['Code'] = trim(preg_replace('/^(\[\$[A-Z]*-[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 = $Decimal/$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);
                            $Value = preg_replace('/(0+)(\.?)(0*)/', $Value, $Format['Code']);
                        } else {
                            if (preg_match('/[0#]E[+-]0/i', $Format['Code'])) {
                                //	Scientific format
                                $Value = sprintf('%5.2E', $Value);
                            } 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->rewind();
            $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() {
        if (! isset($this->rowCount)) {
            $total = 0;
            $this->rewind();

            while ($this->Worksheet->read()) {
                if ($this->Worksheet->name == 'row' && $this->Worksheet->nodeType != XMLReader::END_ELEMENT) {
                    $total++;
                }
            }
            $this->rowCount = $total;
        }

        return $this->rowCount;
    }

    /**
     * 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
     * @return int
     */
    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;
        }
    }
}
