sheet-model
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSheet Model — Headless Spreadsheet for Agents
Sheet Model — 面向Agent的无头电子表格
Build spreadsheet models programmatically using HyperFormula (headless computation engine) + ExcelJS (xlsx export). The spreadsheet IS both the computation and the deliverable.
使用HyperFormula(无头计算引擎)+ ExcelJS(xlsx导出)程序化构建电子表格模型。该电子表格同时兼具计算功能与交付成果属性。
When to Use This vs the xlsx
Skill
xlsx何时使用本Skill vs xlsx
Skill
xlsx| Task | Use |
|---|---|
| Read/write/edit existing .xlsx files | |
| Clean messy CSV data into a spreadsheet | |
| Build a financial model with formulas and scenarios | this skill |
| Produce a .xlsx where formulas are live and editable | this skill |
| Compute ratios, projections, what-if analysis | this skill |
| 任务 | 使用 |
|---|---|
| 读写/编辑现有.xlsx文件 | |
| 将杂乱的CSV数据清理为电子表格 | |
| 构建包含公式和场景的金融模型 | 本Skill |
| 生成带有可编辑实时公式的.xlsx文件 | 本Skill |
| 计算比率、预测、假设分析 | 本Skill |
Setup
安装配置
Install dependencies (run once):
bash
cd {baseDir}
npm install安装依赖(运行一次):
bash
cd {baseDir}
npm installArchitecture
架构
text
Agent code (declarative) SheetModel wrapper Output
──────────────────────── ────────────────────────── ──────────────
addRow('Revenue', 701384) → HyperFormula (compute) → Console table
addRow('EBITDA', formula) → Named ranges auto-tracked → .xlsx with live
addScenarioSheet(config) → {Name} refs resolved → formulas +
getValue('EBITDA') → Dependency graph updates → styling +
exportXlsx('model.xlsx') → ExcelJS (export) → cond. formattext
Agent代码(声明式) SheetModel封装层 输出
──────────────────────── ────────────────────────── ──────────────
addRow('Revenue', 701384) → HyperFormula(计算) → 控制台表格
addRow('EBITDA', formula) → 自动跟踪命名范围 → 带有实时
addScenarioSheet(config) → 解析{Name}引用 → 公式的.xlsx文件 +
getValue('EBITDA') → 更新依赖图 → 样式 +
exportXlsx('model.xlsx') → ExcelJS(导出) → 条件格式Core API
核心API
All code is ESM (). Import the wrapper:
.mjsjavascript
import { SheetModel } from '{baseDir}/lib/sheet-model.mjs';
const M = new SheetModel();所有代码均为ESM(.mjs)格式。导入封装层:
javascript
import { SheetModel } from '{baseDir}/lib/sheet-model.mjs';
const M = new SheetModel();Creating Sheets and Adding Data
创建工作表与添加数据
javascript
M.addSheet('Data');
// Section headers (bold, no value)
M.addSection('Data', 'BALANCE SHEET');
M.addBlank('Data');
// Data rows — addRow returns the A1 row number (use it in SUM ranges)
const r_first = M.addRow('Data', ' Revenue', 701384, { name: 'Revenue' });
const r_costs = M.addRow('Data', ' Costs', -450000, { name: 'Costs' });
const r_other = M.addRow('Data', ' Other', 5000);
// Formula rows — use returned row numbers for SUM ranges
M.addRow('Data', ' EBITDA', `=SUM(B${r_first}:B${r_other})`, { name: 'EBITDA' });
// Formula rows using named references (auto-resolved by HyperFormula)
M.addRow('Data', ' Margin', '=EBITDA/Revenue', { name: 'Margin' });Build top-to-bottom: Names must be defined before any formula that references them. Define data rows first, then formulas.andaddBlank()also return the A1 row number (useful for SUM range boundaries).addSection()
When to use which formula style:
| Need | Use | Why |
|---|---|---|
| Row numbers: | Ranges need cell references; named expressions resolve to single cells |
| Arithmetic between specific cells | Named expressions: | Cleaner, self-documenting |
| Mixed | Both: | Combine as needed |
Never use named expressions as range endpoints: is undefined behavior.
=SUM(Revenue:OtherIncome)javascript
M.addSheet('Data');
// 章节标题(加粗,无值)
M.addSection('Data', '资产负债表');
M.addBlank('Data');
// 数据行 — addRow返回A1格式的行号(可用于SUM范围)
const r_first = M.addRow('Data', ' 营收', 701384, { name: 'Revenue' });
const r_costs = M.addRow('Data', ' 成本', -450000, { name: 'Costs' });
const r_other = M.addRow('Data', ' 其他收入', 5000);
// 公式行 — 使用返回的行号设置SUM范围
M.addRow('Data', ' 息税折旧摊销前利润(EBITDA)', `=SUM(B${r_first}:B${r_other})`, { name: 'EBITDA' });
// 使用命名引用的公式行(由HyperFormula自动解析)
M.addRow('Data', ' 利润率', '=EBITDA/Revenue', { name: 'Margin' });从上到下构建:命名项必须在引用它们的公式之前定义。先定义数据行,再定义公式。和addBlank()也会返回A1格式的行号(对设置SUM范围边界有用)。addSection()
公式风格选择指南:
| 需求 | 使用方式 | 原因 |
|---|---|---|
| 对行范围进行SUM、AVERAGE计算 | 行号: | 范围需要单元格引用;命名表达式仅指向单个单元格 |
| 特定单元格间的算术运算 | 命名表达式: | 更简洁、自文档化 |
| 混合场景 | 两者结合: | 根据需要组合使用 |
切勿将命名表达式用作范围端点:属于未定义行为。
=SUM(Revenue:OtherIncome)Named References
命名引用
The option on :
{ name: 'Revenue' }addRow- Registers a HyperFormula named expression (usable in any formula as )
Revenue - Tracks the A1 row for internal cross-referencing
- Exports as a proper Excel named range in .xlsx (via )
cell.names
⚠️ Names are global across all sheets. Usingin two different sheets overwrites the first. Use unique, prefixed names for multi-sheet models:{ name: 'Revenue' },{ name: 'Rev2024' }.{ name: 'Rev2025' }
Name validation rules:
- Must be valid Excel names: start with a letter or underscore, no spaces
- Cannot collide with Excel cell references: names like ,
AC,PC,R1C1are rejected with a clear errorA1 - Use descriptive names: ,
AdjPC,TotalAC(notCurrentAssets,AC,PC)CA
addRow{ name: 'Revenue' }- 注册一个HyperFormula命名表达式(可在任意公式中以形式使用)
Revenue - 跟踪A1行号用于内部交叉引用
- 在导出的.xlsx中作为标准Excel命名范围导出(通过)
cell.names
⚠️ 命名全局生效:所有工作表共享命名空间。在两个不同工作表中使用会覆盖第一个的定义。对于多工作表模型,请使用唯一的带前缀命名:{ name: 'Revenue' }、{ name: 'Rev2024' }。{ name: 'Rev2025' }
命名验证规则:
- 必须是有效的Excel名称:以字母或下划线开头,无空格
- 不能与Excel单元格引用冲突:类似、
AC、PC、R1C1的命名会被拒绝,并返回清晰的错误信息A1 - 使用描述性命名:、
AdjPC、TotalAC(而非CurrentAssets、AC、PC)CA
Cross-Sheet References (Data Sheets Only)
跨工作表引用(仅数据工作表)
In formulas, reference named cells on other data sheets with dot notation:
addRowjavascript
M.addRow('CashFlow', ' From Operations', '={PnL.NetIncome} + {PnL.Depreciation}');Thissyntax only works in{Sheet.Name}formulas, NOT in scenario output formulas. For scenarios, use named expressions (bare names) — they are global across sheets.addRow
在公式中,使用点语法引用其他数据工作表上的命名单元格:
addRowjavascript
M.addRow('CashFlow', ' 经营活动现金流', '={PnL.NetIncome} + {PnL.Depreciation}');这种语法仅在{Sheet.Name}公式中生效,不支持在场景输出公式中使用。对于场景,请使用命名表达式(裸名)——它们在所有工作表中全局生效。addRow
Scenario Sheets
场景工作表
The core feature — define inputs, scenarios, and output formulas declaratively:
javascript
M.addScenarioSheet('Scenarios', {
inputs: [
{ name: 'GrowthRate', label: 'Revenue Growth %' },
{ name: 'CostCut', label: 'Cost Reduction' },
],
scenarios: [
{ label: 'Base Case', values: {} }, // all inputs = 0
{ label: 'Optimistic', values: { GrowthRate: 0.10, CostCut: 50000 } },
{ label: 'Conservative', values: { GrowthRate: 0.03, CostCut: 20000 } },
],
outputs: [
// {InputName} → column-relative (B2, C2, D2...)
// DataSheetName → named expression from Data sheet (fixed)
// {PriorOutput} → column-relative ref to earlier output in this sheet
{ name: 'AdjRev', label: 'Adj. Revenue', format: 'number',
formula: 'Revenue * (1 + {GrowthRate})' },
{ name: 'AdjCost', label: 'Adj. Costs', format: 'number',
formula: 'Costs + {CostCut}' },
{ name: 'AdjEBITDA', label: 'EBITDA', format: 'number',
formula: '{AdjRev} + {AdjCost}' },
// Section separator
{ section: true, label: 'RATIOS' },
// Ratio with conditional formatting thresholds
{ name: 'EBITDAm', label: 'EBITDA Margin', format: 'percent',
formula: '{AdjEBITDA} / {AdjRev}',
thresholds: { good: 0.15, bad: 0.08 } },
// Inverted threshold (lower = better)
{ name: 'DebtEBITDA', label: 'Debt/EBITDA', format: 'ratio',
formula: 'TotalDebt / {AdjEBITDA}',
thresholds: { good: 2.5, bad: 4.0, invert: true } },
],
});Do NOT callbeforeaddSheet()— it creates the sheet internally. Only useaddScenarioSheet()for data sheets.addSheet()is a one-shot call. You cannot add rows to a scenario sheet after creation. Include all inputs, outputs, and sections in the config object.addScenarioSheetEvery output you want to reference later MUST have aproperty. Without it,namein a subsequent formula will throw an error.{ThatOutput}
核心功能——声明式定义输入、场景和输出公式:
javascript
M.addScenarioSheet('Scenarios', {
inputs: [
{ name: 'GrowthRate', label: '营收增长率 %' },
{ name: 'CostCut', label: '成本削减额' },
],
scenarios: [
{ label: '基准场景', values: {} }, // 所有输入=0
{ label: '乐观场景', values: { GrowthRate: 0.10, CostCut: 50000 } },
{ label: '保守场景', values: { GrowthRate: 0.03, CostCut: 20000 } },
],
outputs: [
// {InputName} → 列相对引用(B2、C2、D2...)
// DataSheetName → 来自Data工作表的命名表达式(固定)
// {PriorOutput} → 本工作表中前序输出的列相对引用
{ name: 'AdjRev', label: '调整后营收', format: 'number',
formula: 'Revenue * (1 + {GrowthRate})' },
{ name: 'AdjCost', label: '调整后成本', format: 'number',
formula: 'Costs + {CostCut}' },
{ name: 'AdjEBITDA', label: '调整后EBITDA', format: 'number',
formula: '{AdjRev} + {AdjCost}' },
// 章节分隔符
{ section: true, label: '比率指标' },
// 带条件格式阈值的比率
{ name: 'EBITDAm', label: 'EBITDA利润率', format: 'percent',
formula: '{AdjEBITDA} / {AdjRev}',
thresholds: { good: 0.15, bad: 0.08 } },
// 反向阈值(值越低越好)
{ name: 'DebtEBITDA', label: '债务/EBITDA', format: 'ratio',
formula: 'TotalDebt / {AdjEBITDA}',
thresholds: { good: 2.5, bad: 4.0, invert: true } },
],
});请勿在前调用addScenarioSheet():场景工作表会由内部自动创建。仅对数据工作表使用addSheet()。addSheet()是一次性调用:创建场景工作表后无法再添加行。请在配置对象中包含所有输入、输出和章节。addScenarioSheet所有需要后续引用的输出必须设置属性:如果未设置,后续公式中的name会抛出错误。{ThatOutput}
Formula Reference Resolution in Scenarios
场景中的公式引用解析规则
Inside , references are resolved as follows:
outputs[].formula| Syntax | Resolves to | Example |
|---|---|---|
| Column-relative cell ref to scenario input row | |
| Column-relative cell ref to prior output in same sheet | |
| HyperFormula named expression (global, from any sheet) | |
Important: Bare names (no ) are HyperFormula named expressions — they resolve to a fixed cell. names resolve to column-relative cells within the Scenarios sheet.
{}{Wrapped}⚠️ Name collision rule: If a scenario outputmatches a Data sheet named expression,{name}will resolve to the Data sheet's fixed cell, not the scenario output's column-relative cell. Always use unique names for scenario outputs. Example: Data sheet has{OutputName}, scenario should use{ name: 'EBITDA' }— never both{ name: 'AdjEBITDA' }.EBITDA
在中,引用的解析规则如下:
outputs[].formula| 语法 | 解析结果 | 示例 |
|---|---|---|
| 场景输入行的列相对单元格引用 | |
| 本工作表中前序输出的列相对单元格引用 | |
| HyperFormula命名表达式(全局,来自任意工作表) | |
重要提示:裸名(无)是HyperFormula命名表达式——它们解析为固定单元格。解析为场景工作表内的列相对单元格。
{}{包裹名}⚠️ 命名冲突规则:如果场景输出的与Data工作表的命名表达式重名,{name}会解析为Data工作表的固定单元格,而非场景输出的列相对单元格。请始终为场景输出使用唯一命名。示例:Data工作表有{OutputName},场景应使用{ name: 'EBITDA' }——切勿两者都用{ name: 'AdjEBITDA' }。EBITDA
Output Formats
输出格式
| Format | Excel numFmt | Display |
|---|---|---|
| | |
| | |
| | |
| | |
| 格式 | Excel数字格式 | 显示效果 |
|---|---|---|
| | |
| | |
| | |
| | |
Thresholds (Conditional Formatting)
阈值(条件格式)
javascript
thresholds: { good: 0.15, bad: 0.08 } // Higher is better (green >= 0.15, red < 0.08)
thresholds: { good: 2.5, bad: 4.0, invert: true } // Lower is better (green <= 2.5, red > 4.0)Colors: green (#E2EFDA), amber (#FFF2CC), red (#FCE4EC).
javascript
thresholds: { good: 0.15, bad: 0.08 } // 值越高越好(绿色 >= 0.15,红色 < 0.08)
thresholds: { good: 2.5, bad: 4.0, invert: true } // 值越低越好(绿色 <= 2.5,红色 > 4.0)颜色:绿色(#E2EFDA)、黄色(#FFF2CC)、红色(#FCE4EC)。
Reading Computed Values
读取计算值
javascript
// From Data sheet (by named ref)
const ebitda = M.getValue('Data', 'EBITDA');
// From Scenarios (by scenario index, 0-based)
const baseEBITDA = M.getScenarioValue('Scenarios', 0, 'AdjEBITDA');
const optEBITDA = M.getScenarioValue('Scenarios', 1, 'AdjEBITDA');
// Raw cell access (sheet, col 0-indexed, a1Row 1-indexed)
const val = M.getCellValue('Data', 1, 5); // col B, row 5Usefor data sheets,getValue()for scenario sheets.getScenarioValue()on a scenario sheet returns the first scenario's value (column B).getValue()
javascript
// 从Data工作表读取(通过命名引用)
const ebitda = M.getValue('Data', 'EBITDA');
// 从Scenarios工作表读取(按场景索引,从0开始)
const baseEBITDA = M.getScenarioValue('Scenarios', 0, 'AdjEBITDA');
const optEBITDA = M.getScenarioValue('Scenarios', 1, 'AdjEBITDA');
// 原始单元格访问(工作表、列0索引、A1行1索引)
const val = M.getCellValue('Data', 1, 5); // B列,第5行对数据工作表使用,对场景工作表使用getValue()。在场景工作表上调用getScenarioValue()会返回第一个场景的值(B列)。getValue()
Error Handling
错误处理
javascript
// Formula errors (division by zero, etc.) return CellError objects, not exceptions
const val = M.getValue('Data', 'Margin');
if (typeof val !== 'number' || !isFinite(val)) {
console.error('Formula error:', val);
// val might be: { type: 'DIV_BY_ZERO' }, { type: 'REF' }, { type: 'NAME' }, etc.
}
// To prevent #DIV/0! in formulas, guard with IF:
M.addRow('Data', ' Margin', '=IF(Revenue=0, 0, EBITDA/Revenue)', { name: 'Margin' });
// Reference errors throw immediately during addRow/addScenarioSheet
// → Always define named rows BEFORE formulas that reference them
// → Error messages include the row label and cell reference for easy debuggingCommon causes of formula errors:
- Division by zero in ratios → guard with
IF(denominator=0, 0, numerator/denominator) - Misspelled named expression → throws immediately with clear error message
- Circular reference → throws immediately with row context
javascript
// 公式错误(除零等)返回CellError对象,而非抛出异常
const val = M.getValue('Data', 'Margin');
if (typeof val !== 'number' || !isFinite(val)) {
console.error('公式错误:', val);
// val可能为:{ type: 'DIV_BY_ZERO' }, { type: 'REF' }, { type: 'NAME' }等
}
// 为避免公式中出现#DIV/0!,使用IF进行防护:
M.addRow('Data', ' 利润率', '=IF(Revenue=0, 0, EBITDA/Revenue)', { name: 'Margin' });
// 引用错误会在addRow/addScenarioSheet调用时立即抛出
// → 务必在引用命名行的公式之前定义命名行
// → 错误信息包含行标签和单元格引用,便于调试公式错误的常见原因:
- 比率计算中除零 → 使用进行防护
IF(denominator=0, 0, numerator/denominator) - 命名表达式拼写错误 → 立即抛出带清晰提示的错误
- 循环引用 → 立即抛出带行上下文的错误
Console Output
控制台输出
javascript
M.printScenarios('Scenarios');Prints a formatted table with emoji flags for threshold-based RAG status (🟢🟡🔴).
javascript
M.printScenarios('Scenarios');打印格式化表格,基于阈值的RAG状态显示 emoji 标识(🟢🟡🔴)。
Export to .xlsx
导出为.xlsx
javascript
await M.exportXlsx('output.xlsx', {
creator: 'Agent Name',
headerColor: '1B3A5C', // Dark blue header background (ARGB hex, no #)
});The exported file contains:
- Live formulas (not static values) — user can change inputs and see results update
- Named ranges on all named cells (visible in Excel's Name Manager)
- Conditional formatting on cells with thresholds (green/amber/red)
- Frozen panes — first column frozen on all sheets; header row also frozen on scenario sheets
- Input cells highlighted in light blue (#DAEEF3)
- Section headers in bold
Named ranges on scenario sheets point to the first scenario column (column B). They exist for cross-sheet references in Excel, not for selecting all scenarios.
javascript
await M.exportXlsx('output.xlsx', {
creator: 'Agent Name',
headerColor: '1B3A5C', // 深蓝色表头背景(ARGB十六进制,无#)
});导出的文件包含:
- 实时公式(而非静态值)——用户可修改输入并查看结果实时更新
- 所有命名单元格的命名范围(在Excel的名称管理器中可见)
- 带阈值单元格的条件格式(绿/黄/红)
- 冻结窗格——所有工作表冻结第一列;场景工作表同时冻结表头行
- 输入单元格高亮(浅蓝#DAEEF3)
- 章节标题加粗
场景工作表的命名范围指向第一个场景列(B列)。它们用于Excel中的跨工作表引用,而非选中所有场景。
Advanced Styling
高级样式定制
For styling beyond what SheetModel provides, modify the file after export with ExcelJS:
javascript
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const ExcelJS = require('exceljs');
await M.exportXlsx('model.xlsx');
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile('model.xlsx');
const ws = wb.getWorksheet('Data');
// Custom column widths, borders, fills, page setup, etc.
ws.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1 };
ws.getColumn(2).numFmt = '$#,##0';
await wb.xlsx.writeFile('model.xlsx');如需SheetModel默认提供之外的样式,可在导出后使用ExcelJS修改文件:
javascript
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const ExcelJS = require('exceljs');
await M.exportXlsx('model.xlsx');
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile('model.xlsx');
const ws = wb.getWorksheet('Data');
// 自定义列宽、边框、填充、页面设置等
ws.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1 };
ws.getColumn(2).numFmt = '$#,##0';
await wb.xlsx.writeFile('model.xlsx');Complete Example: Financial Model with Scenarios
完整示例:带场景的金融模型
javascript
import { SheetModel } from '{baseDir}/lib/sheet-model.mjs';
const M = new SheetModel();
M.addSheet('Data');
// ── Balance Sheet ──
M.addSection('Data', 'BALANCE SHEET');
M.addBlank('Data');
const r1 = M.addRow('Data', ' Cash', 50000, { name: 'Cash' });
const r2 = M.addRow('Data', ' Receivables', 120000, { name: 'Receivables' });
const r3 = M.addRow('Data', ' Inventory', 30000);
M.addRow('Data', ' Current Assets', `=SUM(B${r1}:B${r3})`, { name: 'CurrentAssets' });
M.addBlank('Data');
const r4 = M.addRow('Data', ' Payables', 80000, { name: 'Payables' });
const r5 = M.addRow('Data', ' Short-term Debt', 40000, { name: 'STDebt' });
M.addRow('Data', ' Current Liabilities', `=SUM(B${r4}:B${r5})`, { name: 'CurrentLiab' });
M.addBlank('Data');
M.addRow('Data', ' Equity', 200000, { name: 'Equity' });
M.addRow('Data', ' Long-term Debt', 150000, { name: 'LTDebt' });
// ── P&L ──
M.addBlank('Data');
M.addSection('Data', 'INCOME STATEMENT');
M.addBlank('Data');
const p1 = M.addRow('Data', ' Revenue', 500000, { name: 'Revenue' });
const p2 = M.addRow('Data', ' COGS', -200000);
const p3 = M.addRow('Data', ' Operating Exp', -150000);
const p4 = M.addRow('Data', ' Depreciation', -30000, { name: 'Depreciation' });
M.addRow('Data', ' Operating Income', `=SUM(B${p1}:B${p4})`, { name: 'OpIncome' });
M.addRow('Data', ' Interest Expense', -15000, { name: 'IntExp' });
M.addRow('Data', ' Net Income', '=OpIncome+IntExp', { name: 'NetIncome' });
// ── Scenarios ──
M.addScenarioSheet('Analysis', {
inputs: [
{ name: 'RevGrowth', label: 'Revenue Growth' },
{ name: 'DebtPaydown', label: 'Debt Paydown' },
],
scenarios: [
{ label: 'As-Is', values: {} },
{ label: 'Growth 10%', values: { RevGrowth: 0.10 } },
{ label: 'Deleverage', values: { RevGrowth: 0.05, DebtPaydown: 50000 } },
],
outputs: [
{ name: 'AdjRev', label: 'Adj. Revenue', format: 'number',
formula: 'Revenue * (1 + {RevGrowth})' },
{ name: 'AdjEBITDA', label: 'EBITDA', format: 'number',
formula: '{AdjRev} + (Revenue - OpIncome + Depreciation) / Revenue * {AdjRev} * -1 + ABS(Depreciation)' },
{ name: 'TotalDebt', label: 'Total Debt', format: 'number',
formula: 'LTDebt + STDebt - {DebtPaydown}' },
{ name: 'NetDebt', label: 'Net Debt', format: 'number',
formula: '{TotalDebt} - Cash' },
{ section: true, label: 'KEY RATIOS' },
{ name: 'CurrRatio', label: 'Current Ratio', format: 'ratio',
formula: 'CurrentAssets / CurrentLiab',
thresholds: { good: 1.5, bad: 1.0 } },
{ name: 'DebtEBITDA', label: 'Debt/EBITDA', format: 'ratio',
formula: '{TotalDebt} / {AdjEBITDA}',
thresholds: { good: 2.5, bad: 4.0, invert: true } },
{ name: 'ICR', label: 'Interest Coverage', format: 'ratio',
formula: '{AdjEBITDA} / ABS(IntExp)',
thresholds: { good: 3.0, bad: 1.5 } },
{ name: 'EBITDAm', label: 'EBITDA Margin', format: 'percent',
formula: '{AdjEBITDA} / {AdjRev}',
thresholds: { good: 0.20, bad: 0.10 } },
{ name: 'ROE', label: 'Return on Equity', format: 'percent',
formula: 'NetIncome / Equity',
thresholds: { good: 0.12, bad: 0.05 } },
],
});
// Use
M.printScenarios('Analysis');
console.log('EBITDA (Growth):', M.getScenarioValue('Analysis', 1, 'AdjEBITDA'));
await M.exportXlsx('financial-model.xlsx');javascript
import { SheetModel } from '{baseDir}/lib/sheet-model.mjs';
const M = new SheetModel();
M.addSheet('Data');
// ── 资产负债表 ──
M.addSection('Data', '资产负债表');
M.addBlank('Data');
const r1 = M.addRow('Data', ' 现金', 50000, { name: 'Cash' });
const r2 = M.addRow('Data', ' 应收账款', 120000, { name: 'Receivables' });
const r3 = M.addRow('Data', ' 库存', 30000);
M.addRow('Data', ' 流动资产', `=SUM(B${r1}:B${r3})`, { name: 'CurrentAssets' });
M.addBlank('Data');
const r4 = M.addRow('Data', ' 应付账款', 80000, { name: 'Payables' });
const r5 = M.addRow('Data', ' 短期债务', 40000, { name: 'STDebt' });
M.addRow('Data', ' 流动负债', `=SUM(B${r4}:B${r5})`, { name: 'CurrentLiab' });
M.addBlank('Data');
M.addRow('Data', ' 所有者权益', 200000, { name: 'Equity' });
M.addRow('Data', ' 长期债务', 150000, { name: 'LTDebt' });
// ── 损益表 ──
M.addBlank('Data');
M.addSection('Data', '损益表(P&L)');
M.addBlank('Data');
const p1 = M.addRow('Data', ' 营收', 500000, { name: 'Revenue' });
const p2 = M.addRow('Data', ' 销货成本', -200000);
const p3 = M.addRow('Data', ' 运营费用', -150000);
const p4 = M.addRow('Data', ' 折旧摊销', -30000, { name: 'Depreciation' });
M.addRow('Data', ' 营业利润', `=SUM(B${p1}:B${p4})`, { name: 'OpIncome' });
M.addRow('Data', ' 利息费用', -15000, { name: 'IntExp' });
M.addRow('Data', ' 净利润', '=OpIncome+IntExp', { name: 'NetIncome' });
// ── 场景分析 ──
M.addScenarioSheet('Analysis', {
inputs: [
{ name: 'RevGrowth', label: '营收增长率' },
{ name: 'DebtPaydown', label: '债务偿还额' },
],
scenarios: [
{ label: '当前状态', values: {} },
{ label: '10%增长', values: { RevGrowth: 0.10 } },
{ label: '去杠杆化', values: { RevGrowth: 0.05, DebtPaydown: 50000 } },
],
outputs: [
{ name: 'AdjRev', label: '调整后营收', format: 'number',
formula: 'Revenue * (1 + {RevGrowth})' },
{ name: 'AdjEBITDA', label: '调整后EBITDA', format: 'number',
formula: '{AdjRev} + (Revenue - OpIncome + Depreciation) / Revenue * {AdjRev} * -1 + ABS(Depreciation)' },
{ name: 'TotalDebt', label: '总债务', format: 'number',
formula: 'LTDebt + STDebt - {DebtPaydown}' },
{ name: 'NetDebt', label: '净债务', format: 'number',
formula: '{TotalDebt} - Cash' },
{ section: true, label: '核心比率' },
{ name: 'CurrRatio', label: '流动比率', format: 'ratio',
formula: 'CurrentAssets / CurrentLiab',
thresholds: { good: 1.5, bad: 1.0 } },
{ name: 'DebtEBITDA', label: '债务/EBITDA', format: 'ratio',
formula: '{TotalDebt} / {AdjEBITDA}',
thresholds: { good: 2.5, bad: 4.0, invert: true } },
{ name: 'ICR', label: '利息保障倍数', format: 'ratio',
formula: '{AdjEBITDA} / ABS(IntExp)',
thresholds: { good: 3.0, bad: 1.5 } },
{ name: 'EBITDAm', label: 'EBITDA利润率', format: 'percent',
formula: '{AdjEBITDA} / {AdjRev}',
thresholds: { good: 0.20, bad: 0.10 } },
{ name: 'ROE', label: '净资产收益率', format: 'percent',
formula: 'NetIncome / Equity',
thresholds: { good: 0.12, bad: 0.05 } },
],
});
// 使用
M.printScenarios('Analysis');
console.log('增长场景下的EBITDA:', M.getScenarioValue('Analysis', 1, 'AdjEBITDA'));
await M.exportXlsx('financial-model.xlsx');Recipe: Loan Amortization
实践案例:贷款摊销模型
javascript
const M = new SheetModel();
M.addSheet('Loan');
M.addSection('Loan', 'LOAN PARAMETERS');
M.addBlank('Loan');
M.addRow('Loan', 'Principal', 500000, { name: 'Principal' });
M.addRow('Loan', 'Annual Rate', 0.05, { name: 'AnnualRate' });
M.addRow('Loan', 'Years', 20, { name: 'Years' });
M.addRow('Loan', 'Monthly Rate', '=AnnualRate/12', { name: 'MonthlyRate' });
M.addRow('Loan', 'Periods', '=Years*12', { name: 'Periods' });
M.addBlank('Loan');
// PMT returns negative (cash outflow) — negate for display
M.addRow('Loan', 'Monthly Payment', '=-PMT(MonthlyRate, Periods, Principal)', { name: 'Payment' });
M.addRow('Loan', 'Total Interest', '=Payment*Periods - Principal', { name: 'TotalInterest' });
await M.exportXlsx('loan-model.xlsx');javascript
const M = new SheetModel();
M.addSheet('Loan');
M.addSection('Loan', '贷款参数');
M.addBlank('Loan');
M.addRow('Loan', '本金', 500000, { name: 'Principal' });
M.addRow('Loan', '年利率', 0.05, { name: 'AnnualRate' });
M.addRow('Loan', '贷款年限', 20, { name: 'Years' });
M.addRow('Loan', '月利率', '=AnnualRate/12', { name: 'MonthlyRate' });
M.addRow('Loan', '还款期数', '=Years*12', { name: 'Periods' });
M.addBlank('Loan');
// PMT返回负值(现金流出)——取反用于显示
M.addRow('Loan', '月还款额', '=-PMT(MonthlyRate, Periods, Principal)', { name: 'Payment' });
M.addRow('Loan', '总利息', '=Payment*Periods - Principal', { name: 'TotalInterest' });
await M.exportXlsx('loan-model.xlsx');Available Formulas
支持的公式
HyperFormula supports 395 built-in functions including:
- Math: SUM, AVERAGE, MIN, MAX, ABS, ROUND, CEILING, FLOOR, MOD, POWER, SQRT, LOG
- Financial: PMT, FV, NPER, PV, RATE, NPV, XNPV
- Logical: IF, IFS, AND, OR, NOT, SWITCH, IFERROR
- Lookup: VLOOKUP, HLOOKUP, INDEX, MATCH
- Statistical: COUNT, COUNTA, COUNTIF, SUMIF, SUMIFS, AVERAGEIF
- Text: CONCATENATE, LEFT, RIGHT, MID, LEN, TRIM, UPPER, LOWER, TEXT
- Date: DATE, YEAR, MONTH, DAY, TODAY, DATEDIF, EOMONTH
HyperFormula支持395个内置函数,包括:
- 数学函数:SUM、AVERAGE、MIN、MAX、ABS、ROUND、CEILING、FLOOR、MOD、POWER、SQRT、LOG
- 金融函数:PMT、FV、NPER、PV、RATE、NPV、XNPV
- 逻辑函数:IF、IFS、AND、OR、NOT、SWITCH、IFERROR
- 查找函数:VLOOKUP、HLOOKUP、INDEX、MATCH
- 统计函数:COUNT、COUNTA、COUNTIF、SUMIF、SUMIFS、AVERAGEIF
- 文本函数:CONCATENATE、LEFT、RIGHT、MID、LEN、TRIM、UPPER、LOWER、TEXT
- 日期函数:DATE、YEAR、MONTH、DAY、TODAY、DATEDIF、EOMONTH
ExcelJS Gotchas (Critical)
ExcelJS注意事项(关键)
These bugs were discovered empirically and MUST be followed:
-
Named ranges: Use, NEVER
cell.names = ['Name']ordefinedNames.add().addEx()- tries to parse the name as a cell ref → crashes on names like
add()InvFinCP - silently doesn't persist to the .xlsx file
addEx() - SheetModel handles this automatically — don't use ExcelJS definedNames directly
-
Formula prefix: HyperFormulareturns
getCellFormula()with leading"=SUM(...)". ExcelJS expects=without{ formula: 'SUM(...)' }. Double=causes=errors. SheetModel handles this automatically.#NAME? -
Formula language: .xlsx always stores formulas in English (,
SUM,ABS). Excel translates to locale on display. Always write English function names.IF
以下问题为实际测试发现,必须遵守:
-
命名范围:使用,切勿使用
cell.names = ['Name']或definedNames.add()。addEx()- 会尝试将名称解析为单元格引用→在类似
add()的命名上崩溃InvFinCP - 不会将命名持久化到.xlsx文件中
addEx() - SheetModel会自动处理此问题——请勿直接使用ExcelJS的definedNames
-
公式前缀:HyperFormula的返回带前导
getCellFormula()的公式,如=。 ExcelJS要求公式不带"=SUM(...)",即=。重复的{ formula: 'SUM(...)' }会导致=错误。 SheetModel会自动处理此问题。#NAME? -
公式语言:.xlsx文件始终以英文存储公式(、
SUM、ABS)。 Excel会在显示时转换为本地化语言。请始终使用英文函数名。IF
Limitations
局限性
- Row limit: Practical limit ~5,000 rows. For larger datasets, use pandas/openpyxl via the skill.
xlsx - Single value column on data sheets: writes to columns A (label) and B (value) only. For multi-period models (Year 1, Year 2, Year 3), use a scenario sheet where each "scenario" is a period, or use direct HyperFormula API (
addRow) for additional columns.M.hf.setCellContents(...) - Data sheet formatting: All values in data sheets are formatted as integers (). For percentages, ratios, or decimals, compute them in a scenario sheet output with the appropriate
#,##0option, or post-process with ExcelJS (see Advanced Styling).format - No charts: HyperFormula/ExcelJS can't create Excel charts. Add charts manually or use a separate tool.
- No pivot tables: Use pandas for pivot-style analysis.
- Scenario columns: Maximum 25 scenarios per sheet (columns B–Z). For readability, keep to 10 or fewer.
- Named range naming: Names that match Excel cell/column references (e.g., ,
AC,R1C1) are rejected automatically. Use descriptive names.A1 - Data sheet formulas in .xlsx: The exported Excel formula is the original text, not extracted from HyperFormula. Stick to bare named expressions (e.g., ,
Revenue) and A1 refs via template literals (e.g.,OpIncome).`=SUM(B${r1}:B${r3})`
- 行限制:实际上限约5000行。对于更大数据集,请使用pandas/openpyxl并配合skill。
xlsx - 数据工作表仅单值列:仅写入A列(标签)和B列(值)。对于多期模型(第1年、第2年、第3年),可将每个“期”作为场景使用场景工作表,或使用HyperFormula直接API(
addRow)添加额外列。M.hf.setCellContents(...) - 数据工作表格式:数据工作表中的所有值均格式化为整数()。如需百分比、比率或小数,可在场景工作表输出中使用相应的
#,##0选项计算,或使用ExcelJS进行后处理(见高级样式定制)。format - 无图表支持:HyperFormula/ExcelJS无法创建Excel图表。请手动添加或使用其他工具。
- 无数据透视表:使用pandas进行透视分析。
- 场景列限制:每个工作表最多支持25个场景(B-Z列)。为保证可读性,建议不超过10个。
- 命名范围冲突:与Excel单元格/列引用匹配的命名(如、
AC、R1C1)会被自动拒绝。请使用描述性命名。A1 - 数据工作表公式在.xlsx中的兼容性:导出的Excel公式为原始文本,而非从HyperFormula提取。请仅使用裸命名表达式(如、
Revenue)和通过模板字符串生成的A1引用(如OpIncome)。`=SUM(B${r1}:B${r3})`