目标求解

目标求解是一个假设分析工具,可根据公式结果反推出达到目标值所需的输入值。它适用于已知期望结果、但需要反向计算关键输入参数的场景。

概述 本示例展示了如何在 SpreadJS 中使用目标求解(Goal Seek)功能,根据公式单元格的目标结果,反推出某个可变单元格应取的值。 页面左侧是工作表示例,内置了“收入测算”和“贷款还款”两个场景;右侧是参数面板,用户可以输入公式单元格、目标值、可变单元格、最大迭代次数和容差,然后执行目标求解并查看结果。 实现思路 页面加载后创建一个单工作表的 Workbook,并调用 initSpread 初始化示例数据。 工作表中预先写入两个业务场景:一个使用乘法公式计算总收入,一个使用 PMT 公式计算每月还款额。 右侧表单负责接收目标求解所需的参数,包括公式单元格、目标值、可变单元格以及求解配置项。 点击“运行目标求解”按钮后,代码会先把输入的单元格地址解析为行列索引,再调用 GC.Spread.Sheets.CalcEngine.goalSeek 执行求解。 根据求解结果,页面会在结果区域显示成功、失败或错误提示。 代码解析 示例入口非常直接:先创建工作簿,再初始化当前工作表内容。 工作表初始化时,代码先铺设“目标求解示例”的基础数据,再加入贷款场景和操作说明。这样用户打开页面后就能立即使用两个现成的例子: 第二个示例使用 PMT 公式计算每月还款额。这个区域适合演示“为了让公式结果达到指定值,需要把利率调到多少”这一类反向求解问题: 目标求解按钮点击后,会先读取右侧面板里的输入值,再调用 goalSeek。这里的关键点是:要同时传入“可变单元格”和“公式单元格”的工作表、行、列位置,以及目标值和可选参数: 为了让用户可以直接输入类似 B5、B11 这样的地址,示例额外封装了一个 parseCellReference 方法,把文本地址转换为目标求解 API 所需的工作表、行和列信息: 运行效果 页面打开后,工作表中会直接展示两个可用于测试的目标求解场景。 右侧面板默认给出一组可直接运行的参数,用户无需手动准备公式即可体验目标求解。 当公式单元格、目标值和可变单元格填写正确时,点击“运行目标求解”会在结果区域显示求解成功信息。 如果当前目标值无法求得可行解,结果区域会提示未找到可行解。 如果输入了无效的单元格地址,例如不是单个单元格或引用格式错误,页面会显示错误信息。 API 参考 GC.Spread.Sheets.CalcEngine.goalSeek(...) 根据公式单元格的目标值,自动调整指定可变单元格的值。 GC.Spread.Sheets.CalcEngine.formulaToRanges(...) 用于把用户输入的单元格地址解析为可操作的单元格范围。 sheet.setFormula(row, col, formula) 用于在示例工作表中写入收入计算公式和 PMT 还款公式。
window.onload = function () { var spread = new GC.Spread.Sheets.Workbook(document.getElementById("ss"), { sheetCount: 1 }); initSpread(spread); }; function initSpread(spread) { var sheet = spread.getSheet(0); // 设置示例数据 sheet.setValue(0, 0, '目标求解示例'); sheet.getRange(0, 0, 1, 2).font('bold 14px Arial'); sheet.setValue(2, 0, '单价:'); sheet.setValue(2, 1, 100); sheet.setFormatter(2, 1, '$#,##0.00'); sheet.setValue(3, 0, '销售数量:'); sheet.setValue(3, 1, 50); sheet.setValue(4, 0, '总收入:'); sheet.setFormula(4, 1, '=B3*B4'); sheet.setFormatter(4, 1, '$#,##0.00'); sheet.getCell(4, 1).backColor('#e3f2fd'); sheet.setColumnWidth(0, 120); sheet.setColumnWidth(1, 100); // 添加 PMT 示例 sheet.setValue(6, 0, '贷款还款示例(PMT)'); sheet.getRange(6, 0, 1, 2).font('bold 14px Arial'); sheet.setValue(8, 0, '贷款金额:'); sheet.setValue(8, 1, 10000); sheet.setFormatter(8, 1, '$#,##0.00'); sheet.setValue(9, 0, '期限(月):'); sheet.setValue(9, 1, 18); sheet.setValue(10, 0, '利率:'); sheet.setValue(10, 1, 0.05); sheet.setFormatter(10, 1, '0.00%'); sheet.setValue(11, 0, '每月还款额:'); sheet.setFormula(11, 1, '=-PMT(B11/12,B10,B9)'); sheet.setFormatter(11, 1, '$#,##0.00'); sheet.getCell(11, 1).backColor('#fff3cd'); // 添加说明 sheet.setValue(13, 0, '操作说明:'); sheet.getCell(13, 0).font('bold 12px Arial'); sheet.setValue(14, 0, '1. 示例 1:将 B5 设为公式单元格,用于求出目标收入'); sheet.setValue(15, 0, '2. 示例 2:将 B12 设为公式单元格,目标值设为 600'); sheet.setValue(16, 0, ' 并将 B11 作为可变单元格,以求出所需利率'); sheet.setValue(17, 0, '3. 输入参数后,点击“运行目标求解”'); // 目标求解实现 document.getElementById("runGoalSeek").addEventListener('click', function() { var formulaCellRef = document.getElementById("formulaCell").value; var targetValue = parseFloat(document.getElementById("targetValue").value); var variableCellRef = document.getElementById("variableCell").value; var maximumIterations = parseInt(document.getElementById("maximumIterations").value) || 100; var tolerance = parseFloat(document.getElementById("tolerance").value) || 0.001; var resultDiv = document.getElementById("result"); try { var formulaCell = parseCellReference(sheet, formulaCellRef); var variableCell = parseCellReference(sheet, variableCellRef); var seekResult = GC.Spread.Sheets.CalcEngine.goalSeek( variableCell.sheet, variableCell.row, variableCell.col, formulaCell.sheet, formulaCell.row, formulaCell.col, targetValue, { maximumIterations: maximumIterations, tolerance: tolerance }); if (seekResult) { resultDiv.innerHTML = '<div class="success">目标求解成功!<br/>求得结果:' + formulaCell.sheet.getValue(formulaCell.row, formulaCell.col).toFixed(2) + '</div>'; } else { resultDiv.innerHTML = '<div class="error">目标求解未找到可行解。<br/>请尝试其他目标值。</div>'; } } catch (e) { resultDiv.innerHTML = '<div class="error">错误:' + e.message + '</div>'; } }); } function parseCellReference(sheet, address) { var ranges = GC.Spread.Sheets.CalcEngine.formulaToRanges(sheet, address, 0, 0); if (ranges && ranges.length === 1 && ranges[0].ranges.length === 1 && ranges[0].ranges[0].rowCount === 1 && ranges[0].ranges[0].colCount === 1) { return { sheet: sheet.getParent().getSheetFromName(ranges[0].sheetName), row: ranges[0].ranges[0].row, col: ranges[0].ranges[0].col }; } else { throw new Error('无效的单元格引用:' + address); } }
<!doctype html> <html style="height:100%;font-size:14px;"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="spreadjs culture" content="zh-cn" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-resources-zh/dist/gc.spread.sheets.resources.zh.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <div class="sample-tutorial"> <div id="ss" class="sample-spreadsheets"></div> <div class="options-container"> <div class="option-row"> <label>公式单元格:</label> <input type="text" id="formulaCell" value="B5" placeholder="例如:B5" /> </div> <div class="option-row"> <label>目标值:</label> <input type="number" id="targetValue" value="10000" placeholder="例如:10000" /> </div> <div class="option-row"> <label>可变单元格:</label> <input type="text" id="variableCell" value="B3" placeholder="例如:B3" /> </div> <div class="option-row"> <label>最大迭代次数:</label> <input type="number" id="maximumIterations" value="100" placeholder="默认值:100" /> </div> <div class="option-row"> <label>容差:</label> <input type="number" id="tolerance" value="0.001" step="0.0001" placeholder="默认值:0.001" /> </div> <div class="option-row"> <input type="button" value="运行目标求解" id="runGoalSeek" /> </div> <div class="option-row"> <label>结果:</label> <div id="result" class="result-container"></div> </div> </div> </div> </body> </html>
.sample-tutorial { position: relative; height: 100%; overflow: hidden; } .sample-spreadsheets { width: calc(100% - 280px); height:100%; overflow: hidden; float: left; } .options-container { float: right; width: 280px; overflow: auto; padding: 12px; height: 100%; box-sizing: border-box; background: #fbfbfb; } .option-row { margin-bottom: 12px; } .option-row label { display: block; margin-bottom: 4px; font-weight: bold; font-size: 12px; } input[type=text], input[type=number] { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 3px; box-sizing: border-box; } input[type=button] { width: 100%; padding: 8px 6px; margin-bottom: 6px; background: #007acc; color: white; border: none; border-radius: 3px; cursor: pointer; font-weight: bold; } input[type=button]:hover { background: #005a9e; } .result-container { padding: 8px; border-radius: 3px; min-height: 40px; } .success { background: #d4edda; color: #155724; padding: 8px; border-radius: 3px; } .error { background: #f8d7da; color: #721c24; padding: 8px; border-radius: 3px; } body { position: absolute; top: 0; bottom: 0; left: 0; right: 0; }