【问题标题】:Circular References in PHPExcel - infinite loop or wrong resultPHPExcel 中的循环引用 - 无限循环或错误结果
【发布时间】:2014-09-09 14:12:41
【问题描述】:

我正在使用 PHPExcel 1.8.0

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

  1. 如果电子表格包含一个循环引用。公式,PHPExcel 的计算与 MS Excel 不匹配。
  2. 如果电子表格包含多个循环引用,则 PHPExcel 将进入无限循环。

这里是我到目前为止所做的详细信息。

假设一个电子表格,其中 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;

这就是无限循环的原因。 $this->_cyclicFormulaCell 不会在第 1 行中的公式完成后重新设置为 '',因此此条件不适用于第 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-&gt;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 不一致。为什么会发生这种情况,我该如何解决?

【问题讨论】:

  • 为什么会这样?因为开发人员不是机器,而且会犯错误。你怎么能绕过它?确定确切原因(例如,检查应该是$this-&gt;_cyclicFormulaCount &gt; $this-&gt;cyclicFormulaCount而不是$this-&gt;_cyclicFormulaCount &gt;= $this-&gt;cyclicFormulaCount,测试并证明它,然后向github提交PR
  • 同时,我自己去看看....$cellValue = '' 确实应该依赖于单元格样式,而不是总是设置为 0
  • 附言。感谢您的详细分析......大多数遇到问题的人只是说“它不起作用”(通常不会说到底是什么不起作用......只是希望我为他们解决它
  • 看起来我需要存储循环中每个单独单元格的迭代计数,所以比我最初想象的要复杂一些
  • 马克,非常感谢您的回复。对不起,当我问“为什么会发生这种情况”时,我并没有冒犯的意思——只是想澄清一下算法。不管怎样,我修好了,我在下面发布答案。

标签: phpexcel


【解决方案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

这是代码(抱歉,马克,我没有更多时间将其正确提交给 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;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-24
    • 1970-01-01
    • 2012-09-28
    • 1970-01-01
    • 2012-12-24
    相关资源
    最近更新 更多