PHPExcel中的循环引用 - 无限循环或错误结果

时间:2014-09-09 14:12:41

标签: phpexcel

我正在使用PHPExcel 1.8.0

我已阅读有关循环引用的帖子,例如this one,但我仍遇到问题。

  1. 如果电子表格包含一个循环引用。公式,PHPExcel的计算与MS Excel不匹配。
  2. 如果电子表格包含多个循环引用,则PHPExcel将进入无限循环。
  3. 以下是我到目前为止所做的细节。

    假设电子表格中A1 = B1且B1 = A1 + 1,Excel设置为100次迭代。这是我的代码:

    // create reader
    $objReader = PHPExcel_IOFactory::createReader('Excel2007');
    $objReader->setReadDataOnly(true);
    
    // load workbook
    $objPHPExcel = $objReader->load($this->_path);
    
    // set iterative calculations max count
    PHPExcel_Calculation::getInstance($objPHPExcel)->cyclicFormulaCount = 100;
    
    // calculate
    $objWorksheet = $objPHPExcel->getSheetByName('Testing');
    $data = $objWorksheet->rangeToArray('A1:B1');
    
    echo '<pre>';
    print_r ($data);
    echo '</pre>';
    
    // release resources
    $objPHPExcel->disconnectWorksheets();
    unset($objPHPExcel);
    

    MSExcel导致A1 = 99,B1 = 100.我的代码生成:

    Array
    (
        [0] => Array
            (
                [0] => #VALUE!
                [1] => #VALUE!
            )
    )
    

    除此之外,如果我添加A2 = B2和B2 = A2 + 1,并尝试计算(A1:B2),它会进入无限循环并最终崩溃:

    致命错误:第2837行的C:\ xampp \ htdocs \ cgc \ bulldog \ application \ third_party \ PHPExcel \ Calculation.php中允许的内存大小为134217728(已尝试分配24个字节)

    这是我到目前为止所做的。在Calculation.php中的_calculateFormulaValue中:

    第2383行:$cellValue = ''; - 这是#Value的原因!错误。我将其更改为$cellValue = 0;

    第2400行:

    } elseif ($this->_cyclicFormulaCell == '') {
        $this->_cyclicFormulaCell = $wsTitle.'!'.$cellID;
    

    这是无限循环的原因。在第1行中的公式完成后,$ this-&gt; _cyclicFormulaCell不会重新设置为'',因此此条件不适用于第2行中的公式。

    我从第2389行开始修复如下:

        if (($wsTitle{0} !== "\x00") && ($this->_cyclicReferenceStack->onStack($wsTitle.'!'.$cellID))) {
            if ($this->cyclicFormulaCount <= 0) {
                return $this->_raiseFormulaError('Cyclic Reference in Formula');
            } elseif (($this->_cyclicFormulaCount >= $this->cyclicFormulaCount) &&
                      ($this->_cyclicFormulaCell == $wsTitle.'!'.$cellID)) {
                // Olga - reset for next formula
                $this->_cyclicFormulaCell = '';
                return $cellValue;
            } elseif ($this->_cyclicFormulaCell == $wsTitle.'!'.$cellID) {
                ++$this->_cyclicFormulaCount;
                if ($this->_cyclicFormulaCount >= $this->cyclicFormulaCount) {
                    // Olga - reset for next formula
                    $this->_cyclicFormulaCell = '';
                    return $cellValue;
                }
            } elseif ($this->_cyclicFormulaCell == '') {
                $this->_cyclicFormulaCell = $wsTitle.'!'.$cellID;
                if ($this->_cyclicFormulaCount >= $this->cyclicFormulaCount) {
                    // Olga - reset for next formula
                    $this->_cyclicFormulaCell = '';
                    return $cellValue;
                }
            }
    

    完成这些修复后,如果我运行$data = $objWorksheet->rangeToArray('A1:B2');,我会得到以下结果:

    Array
    (
        [0] => Array
            (
                [0] => 100 // should be 99
                [1] => 100
            )
    
        [1] => Array
            (
                [0] => 100 // should be 99
                [1] => 100
            )
    )
    

    如您所见,PHPExcel的结果与MS Excel不一致。为什么会发生这种情况?我该如何解决这个问题?

1 个答案:

答案 0 :(得分:1)

好的,我设法调试了这个。我的电子表格非常复杂,有很多循环引用。最具挑战性的是A取决于B,B取决于A和C,C取决于B的情况。

我还添加了一个maxChange参数,所以这个东西就像Excel一样。否则我的电子表格需要很长时间。

无论如何,这是一个用法示例:

$objPHPExcel = PHPExcel_IOFactory::load($path);
$objCalc = PHPExcel_Calculation::getInstance($objPHPExcel);
$objCalc->cyclicFormulaCount = 100;
$objCalc->maxChange = 0.001;

修改后的两个文件是:Calculation.php和CalcEngine / CyclicReferenceStack.php

这是代码(抱歉Mark,我无法再有足够的时间将其提交给git)。

Calculation.php

将这些添加到类属性中:

private $_precedentsStack = array();
public $maxChange = 0;

将_calculateFormulaValue()函数替换为:

public function _calculateFormulaValue($formula, $cellID=null, PHPExcel_Cell $pCell = null) {
    $this->_debugLog->writeDebugLog("BREAKPOINT: _calculateFormulaValue for $cellID");

    //  Basic validation that this is indeed a formula
    //  We simply return the cell value if not
    $formula = trim($formula);
    if ($formula{0} != '=') return self::_wrapResult($formula);
    $formula = ltrim(substr($formula,1));
    if (!isset($formula{0})) return self::_wrapResult($formula);

    // initialize values
    $pCellParent = ($pCell !== NULL) ? $pCell->getWorksheet() : NULL;
    $wsTitle = ($pCellParent !== NULL) ? $pCellParent->getTitle() : "\x00Wrk";
    $key = $wsTitle.'!'.$cellID;
    $data = array(
        'i' => 0,  // incremented when the entire stack has been calculated
        'j' => 0,  // flags the formula as having been calculated; can only be 0 or 1
        'cellValue' => $pCell->getOldCalculatedValue(), // default value to start with
        'precedents' => array(),
        'holdValue' => FALSE // set to TRUE when change in value is less then maxChange
    );

    // add this as precedent
    $this->_precedentsStack[] = $key;

    // if already been calculated, return cached value
    if (($cellID !== NULL) && ( $this->getValueFromCache($wsTitle, $cellID, $cellValue))) {
        return $cellValue;
    }

    $this->_cyclicReferenceStack->getValueByKey($key, $data);
    extract($data);
    $this->_debugLog->writeDebugLog("iteration # $i");

    // if already calculated in this iteration, return the temp cached value
    if ($i >= $this->cyclicFormulaCount || $j == 1) {
        return $cellValue;
    }

    // on stack, but has not yet been calculated => return default value
    if (($wsTitle{0} !== "\x00") && ($this->_cyclicReferenceStack->onStack($key))) {

        if ($this->cyclicFormulaCount <= 0) {
            return $this->_raiseFormulaError('Cyclic Reference in Formula');
        }

        return $cellValue;
    }

    // calculate value recursively      
    $this->_cyclicReferenceStack->push($key);
    $cellValue = $this->_processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
    $this->_cyclicReferenceStack->pop();

    // everything in precedent stack after the current cell is a precedent
    // and every precedent's precedent is a precedent (aka a mouthfull)
    while ( $this->_precedentsStack[ count($this->_precedentsStack) - 1 ] != $key ){
        $data['precedents'][] = array_pop($this->_precedentsStack);
    }
    $data['precedents'] = array_unique($data['precedents']);

    // check for max change
    $oldValue = $this->_extractResult($data['cellValue']);
    $newValue = $this->_extractResult($cellValue);
    $data['cellValue'] = $cellValue;
    $data['holdValue'] = (abs($oldValue - $newValue) < $this->maxChange);

    // flag as calculated and save to temp storage
    $data['j'] = 1;
    $this->_cyclicReferenceStack->setValueByKey($key, $data);

    // if this cell is a precedent, trigger a re-calculate
    $tempCache = $this->_cyclicReferenceStack->showValues();
    foreach ($tempCache as $tempKey => $tempData) {
        if ( $tempData['holdValue'] == TRUE && ( in_array($key, $tempData['precedents'])) ) {
            $tempData['holdValue'] = FALSE;
        }
        $this->_cyclicReferenceStack->setValueByKey($tempKey, $tempData);
    }

    // at the end of the stack, increment the counter and flag formulas for re-calculation
    if (count($this->_cyclicReferenceStack->showStack()) == 0) {
        $i++;
        $this->_precedentsStack = array();

        $tempCache = $this->_cyclicReferenceStack->showValues();
        foreach ($tempCache as $tempKey => $tempData) {
            $tempData['i'] = $i;
            if ( ! $tempData['holdValue'] ) $tempData['j'] = 0;

            $this->_cyclicReferenceStack->setValueByKey($tempKey, $tempData);
        }

        $this->_debugLog->writeDebugLog("iteration # $i-1 finished");
    }

    if ($i < $this->cyclicFormulaCount) {
        $cellValue = $this->_calculateFormulaValue($pCell->getValue(), $cellID, $pCell);
    } elseif ($cellID !== NULL) {
        // all done: move value from temp storage to cache
        $this->saveValueToCache($wsTitle, $cellID, $cellValue);
        $this->_cyclicReferenceStack->removeValueByKey($key);
    }

    //  Return the calculated value
    return $cellValue;

}   //  function _calculateFormulaValue()

添加此辅助函数:

private function _extractResult($result) {
    if (is_array($result)) {
        while (is_array($result)) {
            $result = array_pop($result);
        }
    }
    return $result;
}

CyclicReferenceStack.php

添加一个属性:

private $_values = array();

添加一堆功能:

public function setValueByKey($key, $value) {
    $this->_values[$key] = $value;
}

public function getValueByKey($key, &$value) {
    if (isset($this->_values[$key])) {
        $value = $this->_values[$key];
        return true;
    }
    return false;
}

public function removeValueByKey($key) {
    if (isset($this->_values[$key])) {
        unset($this->_values[$key]);
    }
}

public function showValues() {
    return $this->_values;
}