@svgrid/enterprise 3.0.2 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +94 -24
- package/README.md +36 -4
- package/dist/SvAdvancedFilter.svelte +1 -1
- package/dist/SvAlertsManager.svelte +2 -1
- package/dist/SvBoard.svelte +2 -1
- package/dist/SvExportMenu.svelte +4 -1
- package/dist/SvExportMenu.svelte.d.ts +0 -13
- package/dist/SvExpressionEditor.svelte +3 -3
- package/dist/SvFormulaBar.svelte +94 -20
- package/dist/SvFormulaBar.svelte.d.ts +7 -0
- package/dist/SvGridAlerts.svelte +49 -9
- package/dist/SvGridBoard.svelte +13 -8
- package/dist/SvGridBulkEditDrawer.svelte +26 -2
- package/dist/SvGridEditPanel.svelte +3 -0
- package/dist/SvGridScheduler.svelte +69 -40
- package/dist/SvGridSelectionBar.svelte +10 -3
- package/dist/SvPivotDesigner.svelte +198 -40
- package/dist/SvPivotDesigner.svelte.d.ts +27 -20
- package/dist/SvSheet.svelte +4043 -243
- package/dist/SvSheet.svelte.d.ts +76 -3
- package/dist/SvSheetChartSetup.svelte +151 -0
- package/dist/SvSheetChartSetup.svelte.d.ts +18 -0
- package/dist/SvSheetComment.svelte +216 -31
- package/dist/SvSheetComment.svelte.d.ts +11 -11
- package/dist/SvSheetConditionalFormat.svelte +28 -31
- package/dist/SvSheetDataValidation.svelte +66 -42
- package/dist/SvSheetEditRanges.svelte +217 -0
- package/dist/SvSheetEditRanges.svelte.d.ts +15 -0
- package/dist/SvSheetErrors.svelte +125 -0
- package/dist/SvSheetErrors.svelte.d.ts +20 -0
- package/dist/SvSheetEvaluate.svelte +133 -0
- package/dist/SvSheetEvaluate.svelte.d.ts +15 -0
- package/dist/SvSheetFilterMenu.svelte +137 -34
- package/dist/SvSheetFilterMenu.svelte.d.ts +5 -0
- package/dist/SvSheetFindReplace.svelte +19 -19
- package/dist/SvSheetFormatCells.svelte +125 -81
- package/dist/SvSheetGoalSeek.svelte +17 -16
- package/dist/SvSheetInsertFunction.svelte +15 -13
- package/dist/SvSheetIteration.svelte +81 -0
- package/dist/SvSheetIteration.svelte.d.ts +10 -0
- package/dist/SvSheetLink.svelte +99 -0
- package/dist/SvSheetLink.svelte.d.ts +17 -0
- package/dist/SvSheetListPicker.svelte +4 -2
- package/dist/SvSheetListPicker.svelte.d.ts +0 -6
- package/dist/SvSheetManageRules.svelte +20 -18
- package/dist/SvSheetNameManager.svelte +17 -15
- package/dist/SvSheetPageSetup.svelte +159 -0
- package/dist/SvSheetPageSetup.svelte.d.ts +13 -0
- package/dist/SvSheetPasteSpecial.svelte +22 -20
- package/dist/SvSheetPivot.svelte +220 -0
- package/dist/SvSheetPivot.svelte.d.ts +21 -0
- package/dist/SvSheetProtectSheet.svelte +81 -0
- package/dist/SvSheetProtectSheet.svelte.d.ts +11 -0
- package/dist/SvSheetRemoveDuplicates.svelte +13 -10
- package/dist/SvSheetRibbon.svelte +104 -51
- package/dist/SvSheetSizeDialog.svelte +4 -2
- package/dist/SvSheetSort.svelte +218 -0
- package/dist/SvSheetSort.svelte.d.ts +28 -0
- package/dist/SvSheetSparklines.svelte +135 -0
- package/dist/SvSheetSparklines.svelte.d.ts +14 -0
- package/dist/SvSheetTable.svelte +160 -0
- package/dist/SvSheetTable.svelte.d.ts +24 -0
- package/dist/SvSheetTabs.svelte +161 -31
- package/dist/SvSheetTabs.svelte.d.ts +22 -8
- package/dist/SvSheetTextToColumns.svelte +20 -20
- package/dist/SvSheetValidationAlert.svelte +8 -6
- package/dist/advanced-filter-enable.js +1 -1
- package/dist/alerts/alert-engine.d.ts +6 -0
- package/dist/alerts/alert-engine.js +21 -4
- package/dist/alerts/alert-formats.js +2 -0
- package/dist/alerts/alert-types.d.ts +1 -1
- package/dist/alerts.d.ts +1 -1
- package/dist/alerts.js +2 -2
- package/dist/board.js +2 -2
- package/dist/bulk-edit.d.ts +3 -1
- package/dist/bulk-edit.js +1 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +39827 -16429
- package/dist/designer/assets/GridMenus-8gTNssgV.js +8 -0
- package/dist/designer/assets/{SvChartMenu-D9hEeHar.js → SvChartMenu-BDoeqGz6.js} +1 -1
- package/dist/designer/assets/{SvDateTimePicker-DARvKUL2.js → SvDateTimePicker-DCPg7WGN.js} +1 -1
- package/dist/designer/assets/SvGridCellEditor-DMoWJjEI.js +4 -0
- package/dist/designer/assets/{SvGridChart-B8j1SSy3.js → SvGridChart-Cy1ATrWh.js} +2 -2
- package/dist/designer/assets/SvGridChartBuilder-eQ3mn0L9.js +1 -0
- package/dist/designer/assets/{SvGridChartPanel-pT6ITc2P.js → SvGridChartPanel-CATdVc9X.js} +3 -3
- package/dist/designer/assets/{SvGridChartView-bg281UA1.js → SvGridChartView-CmsZtCED.js} +1 -1
- package/dist/designer/assets/SvGridDropdown-Cjz0H-fI.js +1 -0
- package/dist/designer/assets/{SvTabs-UFCMTHXS.js → SvTabs-2mHC6oaz.js} +1 -1
- package/dist/designer/assets/cell-formatting-BbubJTVz.js +1 -0
- package/dist/designer/assets/{chart-cIPUsbM5.js → chart-Cn7MnNqA.js} +1 -1
- package/dist/designer/assets/{chart-panel-messages-DtTxpBUU.js → chart-panel-messages-Dz7PTIqT.js} +1 -1
- package/dist/designer/assets/{chart-summary-Czu_5epb.js → chart-summary-BD1qYB2R.js} +1 -1
- package/dist/designer/assets/{chart-trend-D5I5VEe8.js → chart-trend-B8xr5LuD.js} +1 -1
- package/dist/designer/assets/{dismissable-BO547MH9.js → dismissable-ChWRPkoa.js} +1 -1
- package/dist/designer/assets/{export-format-D-eP4b2l.js → export-format-DGckmgKr.js} +1 -1
- package/dist/designer/assets/{focus-trap-CAreAhlU.js → focus-trap-V2-6Fy9I.js} +1 -1
- package/dist/designer/assets/{index-DwU7CAMm.js → index-D5bVbuzI.js} +317 -315
- package/dist/designer/assets/index-_JVFxt02.css +1 -0
- package/dist/designer/assets/row-resize-BInFWCpr.js +1 -0
- package/dist/designer/assets/server-block-cache-CXJKkJpy.js +1 -0
- package/dist/designer/assets/src-DUQ4xE3x.js +58 -0
- package/dist/designer/assets/validate-C3B8QKsM.js +1 -0
- package/dist/designer/index.html +17 -16
- package/dist/export-pdf.d.ts +75 -0
- package/dist/export-pdf.js +216 -15
- package/dist/export.js +10 -2
- package/dist/gantt/SvGridGantt.svelte +2890 -0
- package/dist/gantt/SvGridGantt.svelte.d.ts +30 -0
- package/dist/gantt/gantt-config.d.ts +73 -0
- package/dist/gantt/gantt-config.js +1 -0
- package/dist/gantt/gantt-critical-path.d.ts +67 -0
- package/dist/gantt/gantt-critical-path.js +207 -0
- package/dist/gantt/gantt-model.d.ts +257 -0
- package/dist/gantt/gantt-model.js +651 -0
- package/dist/gantt/gantt-resources.d.ts +59 -0
- package/dist/gantt/gantt-resources.js +71 -0
- package/dist/gantt/gantt.d.ts +7 -0
- package/dist/gantt/gantt.js +36 -0
- package/dist/gantt/index.d.ts +15 -0
- package/dist/gantt/index.js +22 -0
- package/dist/gantt/timeline-arrows.d.ts +102 -0
- package/dist/gantt/timeline-arrows.js +181 -0
- package/dist/import.d.ts +2 -2
- package/dist/import.js +1 -1
- package/dist/index.d.ts +34 -7
- package/dist/index.js +44 -7
- package/dist/install.js +8 -1
- package/dist/license-core.d.ts +17 -1
- package/dist/license-core.js +16 -5
- package/dist/license.d.ts +18 -2
- package/dist/license.js +71 -6
- package/dist/node/studio.js +140 -8
- package/dist/pivot-enable.js +2 -2
- package/dist/scheduler-dependencies.d.ts +35 -0
- package/dist/scheduler-dependencies.js +36 -1
- package/dist/scheduler.js +2 -2
- package/dist/selection-bar.js +2 -2
- package/dist/server/SvGroupCell.svelte +213 -0
- package/dist/server/SvGroupCell.svelte.d.ts +62 -0
- package/dist/server/SvRowGroupPanel.svelte +322 -0
- package/dist/server/SvRowGroupPanel.svelte.d.ts +37 -0
- package/dist/server/enable.d.ts +17 -0
- package/dist/server/enable.js +49 -0
- package/dist/server/index.d.ts +22 -0
- package/dist/server/index.js +24 -0
- package/dist/server/messages.d.ts +45 -0
- package/dist/server/messages.js +35 -0
- package/dist/server/server-group-model.d.ts +104 -0
- package/dist/server/server-group-model.js +272 -0
- package/dist/server/server-pivot.d.ts +46 -0
- package/dist/server/server-pivot.js +57 -0
- package/dist/server/server-row-model.d.ts +547 -0
- package/dist/server/server-row-model.js +1551 -0
- package/dist/server/server-selection.d.ts +99 -0
- package/dist/server/server-selection.js +215 -0
- package/dist/server/svgrid-adapter.d.ts +155 -0
- package/dist/server/svgrid-adapter.js +202 -0
- package/dist/sheet/ast.d.ts +45 -1
- package/dist/sheet/ast.js +14 -0
- package/dist/sheet/auto-filter.d.ts +58 -4
- package/dist/sheet/auto-filter.js +159 -11
- package/dist/sheet/autocomplete.d.ts +9 -0
- package/dist/sheet/autocomplete.js +90 -4
- package/dist/sheet/cell-images.d.ts +39 -0
- package/dist/sheet/cell-images.js +90 -0
- package/dist/sheet/cell-types.d.ts +58 -0
- package/dist/sheet/cell-types.js +96 -0
- package/dist/sheet/coerce.d.ts +40 -0
- package/dist/sheet/coerce.js +170 -3
- package/dist/sheet/comments.d.ts +43 -9
- package/dist/sheet/comments.js +85 -27
- package/dist/sheet/conditional-formats.d.ts +29 -6
- package/dist/sheet/conditional-formats.js +37 -12
- package/dist/sheet/csv.d.ts +32 -0
- package/dist/sheet/csv.js +158 -0
- package/dist/sheet/culture.d.ts +87 -0
- package/dist/sheet/culture.js +247 -0
- package/dist/sheet/delta.d.ts +102 -0
- package/dist/sheet/delta.js +203 -0
- package/dist/sheet/deps.d.ts +45 -1
- package/dist/sheet/deps.js +77 -2
- package/dist/sheet/document.d.ts +106 -4
- package/dist/sheet/document.js +204 -3
- package/dist/sheet/engine.d.ts +67 -0
- package/dist/sheet/engine.js +19 -0
- package/dist/sheet/entry.d.ts +8 -4
- package/dist/sheet/entry.js +171 -21
- package/dist/sheet/error-check.d.ts +49 -0
- package/dist/sheet/error-check.js +83 -0
- package/dist/sheet/evaluate-steps.d.ts +48 -0
- package/dist/sheet/evaluate-steps.js +196 -0
- package/dist/sheet/evaluate.d.ts +67 -0
- package/dist/sheet/evaluate.js +725 -13
- package/dist/sheet/filter-files.d.ts +37 -0
- package/dist/sheet/filter-files.js +465 -0
- package/dist/sheet/format-store.d.ts +15 -0
- package/dist/sheet/format-store.js +11 -1
- package/dist/sheet/from-rows.d.ts +14 -0
- package/dist/sheet/from-rows.js +50 -0
- package/dist/sheet/function-catalog.d.ts +1 -1
- package/dist/sheet/function-catalog.js +296 -5
- package/dist/sheet/functions.js +363 -91
- package/dist/sheet/index.d.ts +35 -8
- package/dist/sheet/index.js +35 -8
- package/dist/sheet/links.d.ts +57 -0
- package/dist/sheet/links.js +174 -0
- package/dist/sheet/messages.d.ts +778 -0
- package/dist/sheet/messages.js +806 -0
- package/dist/sheet/number-format.d.ts +52 -5
- package/dist/sheet/number-format.js +465 -68
- package/dist/sheet/objects.d.ts +106 -0
- package/dist/sheet/objects.js +149 -0
- package/dist/sheet/ods-document.d.ts +44 -0
- package/dist/sheet/ods-document.js +1275 -0
- package/dist/sheet/open-file.d.ts +26 -0
- package/dist/sheet/open-file.js +56 -0
- package/dist/sheet/outline.d.ts +82 -0
- package/dist/sheet/outline.js +228 -0
- package/dist/sheet/packs/array.d.ts +16 -0
- package/dist/sheet/packs/array.js +172 -0
- package/dist/sheet/packs/database.d.ts +42 -0
- package/dist/sheet/packs/database.js +142 -0
- package/dist/sheet/packs/engineering.d.ts +2 -0
- package/dist/sheet/packs/engineering.js +296 -0
- package/dist/sheet/packs/financial.d.ts +5 -0
- package/dist/sheet/packs/financial.js +220 -0
- package/dist/sheet/packs/math-stats.d.ts +2 -0
- package/dist/sheet/packs/math-stats.js +372 -0
- package/dist/sheet/packs/special.d.ts +44 -0
- package/dist/sheet/packs/special.js +187 -0
- package/dist/sheet/packs/stats-dist.d.ts +2 -0
- package/dist/sheet/packs/stats-dist.js +785 -0
- package/dist/sheet/packs/text-date.d.ts +2 -0
- package/dist/sheet/packs/text-date.js +332 -0
- package/dist/sheet/packs/trig.d.ts +2 -0
- package/dist/sheet/packs/trig.js +73 -0
- package/dist/sheet/page-setup.d.ts +73 -0
- package/dist/sheet/page-setup.js +73 -0
- package/dist/sheet/parse.js +42 -10
- package/dist/sheet/paste-special.d.ts +22 -0
- package/dist/sheet/paste-special.js +14 -5
- package/dist/sheet/pivot-range.d.ts +160 -0
- package/dist/sheet/pivot-range.js +333 -0
- package/dist/sheet/presence.d.ts +73 -0
- package/dist/sheet/presence.js +79 -0
- package/dist/sheet/print.d.ts +104 -0
- package/dist/sheet/print.js +169 -0
- package/dist/sheet/protection.d.ts +55 -1
- package/dist/sheet/protection.js +55 -2
- package/dist/sheet/rects.d.ts +1 -1
- package/dist/sheet/refs.d.ts +48 -0
- package/dist/sheet/refs.js +155 -8
- package/dist/sheet/ribbon-icons.d.ts +1 -1
- package/dist/sheet/ribbon-icons.js +186 -0
- package/dist/sheet/ribbon.d.ts +1 -1
- package/dist/sheet/ribbon.js +324 -23
- package/dist/sheet/sheet-kinds.d.ts +74 -0
- package/dist/sheet/sheet-kinds.js +91 -0
- package/dist/sheet/shortcuts.d.ts +13 -3
- package/dist/sheet/shortcuts.js +63 -12
- package/dist/sheet/sort.d.ts +51 -0
- package/dist/sheet/sort.js +107 -0
- package/dist/sheet/sparklines.d.ts +111 -0
- package/dist/sheet/sparklines.js +204 -0
- package/dist/sheet/table-styles.d.ts +74 -0
- package/dist/sheet/table-styles.js +152 -0
- package/dist/sheet/tables.d.ts +19 -0
- package/dist/sheet/tables.js +31 -0
- package/dist/sheet/tokenize.d.ts +18 -1
- package/dist/sheet/tokenize.js +60 -9
- package/dist/sheet/validation.d.ts +16 -0
- package/dist/sheet/validation.js +37 -0
- package/dist/sheet/workbook.d.ts +148 -3
- package/dist/sheet/workbook.js +742 -31
- package/dist/sheet/xls-cfb.d.ts +32 -0
- package/dist/sheet/xls-cfb.js +290 -0
- package/dist/sheet/xls-document.d.ts +10 -0
- package/dist/sheet/xls-document.js +1243 -0
- package/dist/sheet/xls-formula.d.ts +40 -0
- package/dist/sheet/xls-formula.js +569 -0
- package/dist/sheet/xlsx-document.d.ts +108 -0
- package/dist/sheet/xlsx-document.js +1849 -0
- package/dist/sheet/xlsx-drawing.d.ts +99 -0
- package/dist/sheet/xlsx-drawing.js +420 -0
- package/dist/sheet-enable.js +6 -7
- package/dist/sheet-text.d.ts +7 -0
- package/dist/sheet-text.js +29 -0
- package/dist/sources/auth-supabase.js +2 -2
- package/dist/sources/realtime-supabase.js +2 -2
- package/dist/sources/rest.d.ts +6 -2
- package/dist/sources/rest.js +42 -7
- package/dist/sources/supabase.d.ts +10 -1
- package/dist/sources/supabase.js +73 -40
- package/dist/studio/emit-project.js +14 -1
- package/dist/studio/index.js +1 -1
- package/dist/studio/project.d.ts +11 -2
- package/dist/studio/project.js +5 -2
- package/dist/studio/ui-components.generated.js +107 -2
- package/dist/sveltekit/in-memory.d.ts +2 -2
- package/dist/sveltekit/in-memory.js +125 -4
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/dist/sveltekit/query-plan.d.ts +50 -4
- package/dist/sveltekit/query-plan.js +25 -3
- package/dist/sveltekit/sql-source.d.ts +6 -0
- package/dist/sveltekit/sql-source.js +34 -6
- package/dist/sveltekit/sql.d.ts +31 -0
- package/dist/sveltekit/sql.js +135 -25
- package/dist/sveltekit/transport.d.ts +10 -2
- package/dist/sveltekit/transport.js +30 -6
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watermark.js +35 -2
- package/dist/wc/GridMenus-By9vPuq0.js +1088 -0
- package/dist/wc/SvChartMenu-CQNrQGT2.js +308 -0
- package/dist/wc/SvDateRangeInput-DR6m7bSM.js +199 -0
- package/dist/wc/SvDateTimePicker-D-vRnaMo.js +771 -0
- package/dist/wc/SvField-B_kurx_P.js +149 -0
- package/dist/wc/SvGrid.helpers-BdVDtjUm.js +135 -0
- package/dist/wc/SvGridCellEditor-Bnf1Kr5C.js +560 -0
- package/dist/wc/SvGridChart-BqUJh2pG.js +3572 -0
- package/dist/wc/SvGridChartBuilder-CGycC5cn.js +988 -0
- package/dist/wc/SvGridChartPanel-CRnizsHD.js +1746 -0
- package/dist/wc/SvGridChartView-D1pa_xMy.js +47 -0
- package/dist/wc/SvGridDropdown-45WsObLL.js +259 -0
- package/dist/wc/SvModal-DrfOYYyX.js +189 -0
- package/dist/wc/cell-formatting-CBfgahv1.js +86 -0
- package/dist/wc/chart-6OXlybVT.js +4341 -0
- package/dist/wc/chart-export-CeSADGfi.js +135 -0
- package/dist/wc/chart-export-pdf-BGko7MER.js +104 -0
- package/dist/wc/chart-summary-2m3ia1-n.js +137 -0
- package/dist/wc/chart-trend-CQOQtTMl.js +50 -0
- package/dist/wc/chart-validate-CD6ptiwg.js +180 -0
- package/dist/wc/client-DQIzjDBG.js +3064 -0
- package/dist/wc/column-resize-DUPgynvc.js +101 -0
- package/dist/wc/date-format-DS-z_h9v.js +1130 -0
- package/dist/wc/dismissable-CQ6XFXsk.js +45 -0
- package/dist/wc/editor-contract-peBDxir_.js +22 -0
- package/dist/wc/editor-registry-DxGKqcvx.js +24 -0
- package/dist/wc/focus-trap-Dbb2tJxb.js +65 -0
- package/dist/wc/list-option-DrRyivWp.js +82 -0
- package/dist/wc/popover-WsGlvtQv.js +69 -0
- package/dist/wc/positioning-BiKLfu1E.js +112 -0
- package/dist/wc/react.d.ts +41 -0
- package/dist/wc/react.js +90 -0
- package/dist/wc/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/wc/row-drag-touch-CycRR65n.js +93 -0
- package/dist/wc/row-resize-limG_N1z.js +111 -0
- package/dist/wc/sv-sheet-element.d.ts +97 -0
- package/dist/wc/sv-sheet-element.js +44213 -0
- package/dist/wc/svelte-virtualizer.svelte-CXtQy2TQ.js +530 -0
- package/dist/wc/validate-BqDthkNr.js +88 -0
- package/dist/wc/vue.d.ts +31 -0
- package/dist/wc/vue.js +75 -0
- package/package.json +30 -5
- package/dist/designer/assets/GridMenus-BlsvL_aU.js +0 -8
- package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +0 -4
- package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +0 -1
- package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +0 -1
- package/dist/designer/assets/cell-formatting-wr5wm7US.js +0 -1
- package/dist/designer/assets/index-CMFgAoVV.css +0 -1
- package/dist/designer/assets/row-resize-DLktcqUK.js +0 -1
- package/dist/designer/assets/src-EdI3Xqds.js +0 -58
- package/dist/designer/assets/validate-h-aslRV9.js +0 -1
package/dist/sheet/workbook.js
CHANGED
|
@@ -12,12 +12,21 @@
|
|
|
12
12
|
* disagree with the formula above it.
|
|
13
13
|
*/
|
|
14
14
|
import { parseFormula } from './parse.js';
|
|
15
|
-
import { evaluate, rangeValues } from './evaluate.js';
|
|
15
|
+
import { evaluate, rangeValues, tableRectOf } from './evaluate.js';
|
|
16
16
|
import { withCustomFunctions } from './functions.js';
|
|
17
|
-
import { isError } from './ast.js';
|
|
18
|
-
import { createDependencyGraph, precedentsOf, cellKey } from './deps.js';
|
|
17
|
+
import { isError, typedError } from './ast.js';
|
|
18
|
+
import { createDependencyGraph, precedentsOf, isVolatile, cellKey, parseCellKey } from './deps.js';
|
|
19
|
+
import { createTableRegistry, isValidTableName, shiftTables } from './tables.js';
|
|
19
20
|
import { createNames } from './names.js';
|
|
20
|
-
import { fixupReferences, renameSheetReferences } from './refs.js';
|
|
21
|
+
import { fixupReferences, renameSheetReferences, repointReferences } from './refs.js';
|
|
22
|
+
import { builtinEngine } from './engine.js';
|
|
23
|
+
export const DEFAULT_ITERATION = { enabled: false, maxIterations: 100, maxChange: 0.001 };
|
|
24
|
+
/** Excel's text prefix. A cell typed as `'007` holds the text `007`: the
|
|
25
|
+
* apostrophe says "what follows is text", and it is not part of the value,
|
|
26
|
+
* so it is never displayed, printed, exported or compared. */
|
|
27
|
+
export const TEXT_PREFIX = "'";
|
|
28
|
+
/** The value a typed text stands for once the prefix is taken off. */
|
|
29
|
+
export const withoutTextPrefix = (text) => text.startsWith(TEXT_PREFIX) ? text.slice(1) : text;
|
|
21
30
|
/** A name Excel would accept for a sheet. */
|
|
22
31
|
export function isValidSheetName(name) {
|
|
23
32
|
if (name.trim() === '' || name.length > 31)
|
|
@@ -34,17 +43,106 @@ function nextSheetName(taken) {
|
|
|
34
43
|
return candidate;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
46
|
+
/** Iteration settings with the defaults filled in and the numbers sane. */
|
|
47
|
+
export function cleanIteration(next) {
|
|
48
|
+
const passes = Math.round(Number(next?.maxIterations ?? DEFAULT_ITERATION.maxIterations));
|
|
49
|
+
const change = Number(next?.maxChange ?? DEFAULT_ITERATION.maxChange);
|
|
50
|
+
return {
|
|
51
|
+
enabled: next?.enabled === true,
|
|
52
|
+
maxIterations: Number.isFinite(passes) ? Math.min(Math.max(passes, 1), 32767) : DEFAULT_ITERATION.maxIterations,
|
|
53
|
+
maxChange: Number.isFinite(change) && change >= 0 ? change : DEFAULT_ITERATION.maxChange,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* How far a cell moved between two passes. Two numbers are their distance;
|
|
58
|
+
* anything else has moved either not at all or immeasurably, and an
|
|
59
|
+
* immeasurable move keeps the loop going until the passes run out.
|
|
60
|
+
*/
|
|
61
|
+
function moved(before, after) {
|
|
62
|
+
if (typeof before === 'number' && typeof after === 'number')
|
|
63
|
+
return Math.abs(after - before);
|
|
64
|
+
if (before === undefined)
|
|
65
|
+
return Infinity;
|
|
66
|
+
if (isError(before) || isError(after)) {
|
|
67
|
+
return isError(before) && isError(after) && before.error === after.error ? 0 : Infinity;
|
|
68
|
+
}
|
|
69
|
+
return before === after ? 0 : Infinity;
|
|
70
|
+
}
|
|
37
71
|
export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], options = {}) {
|
|
38
72
|
const order = [];
|
|
73
|
+
/** What the shell is hiding, for SUBTOTAL. Null until it says. */
|
|
74
|
+
let hiddenRows = null;
|
|
75
|
+
/** Whether any cell holds a SUBTOTAL, so a filter click can skip the
|
|
76
|
+
* recalculation when nothing reads what is hidden. Null when unknown. */
|
|
77
|
+
let subtotalsHere = null;
|
|
39
78
|
const byName = new Map();
|
|
40
79
|
const graph = createDependencyGraph();
|
|
80
|
+
/**
|
|
81
|
+
* The workbook's tables. The parser has always read a structured
|
|
82
|
+
* reference (`Orders[Amount]`, `[@Qty]`); this is what makes one resolve,
|
|
83
|
+
* because a reference like that means nothing until something says where
|
|
84
|
+
* the table is.
|
|
85
|
+
*/
|
|
86
|
+
const rawTables = createTableRegistry(options.tables);
|
|
87
|
+
/**
|
|
88
|
+
* The registry as the workbook hands it out: every change drops the
|
|
89
|
+
* cached values and settles again.
|
|
90
|
+
*
|
|
91
|
+
* A table decides what `Orders[Amount]` means, so defining one, removing
|
|
92
|
+
* one or growing one changes the answer of every formula that mentions
|
|
93
|
+
* it. Without this, a table defined after the cells were read leaves each
|
|
94
|
+
* of those formulas showing the `#REF!` it worked out when there was no
|
|
95
|
+
* table, which is the first thing anyone hits.
|
|
96
|
+
*/
|
|
97
|
+
const tables = {
|
|
98
|
+
get: (name) => rawTables.get(name),
|
|
99
|
+
list: () => rawTables.list(),
|
|
100
|
+
at: (sheet, row, col) => rawTables.at(sheet, row, col),
|
|
101
|
+
define(table) { rawTables.define(table); afterTables(); },
|
|
102
|
+
remove(name) { const gone = rawTables.remove(name); if (gone)
|
|
103
|
+
afterTables(); return gone; },
|
|
104
|
+
growToInclude(sheet, row, col) { const grew = rawTables.growToInclude(sheet, row, col); if (grew)
|
|
105
|
+
afterTables(); return grew; },
|
|
106
|
+
clear() { rawTables.clear(); afterTables(); },
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* `Orders2`, `Orders3`, ...: a table name nothing has taken yet.
|
|
110
|
+
*
|
|
111
|
+
* A short name plus a number can read as a cell reference (`T2`), which
|
|
112
|
+
* Excel refuses as a table name and so does the registry, so those fall
|
|
113
|
+
* back to Excel's own `Table2`.
|
|
114
|
+
*/
|
|
115
|
+
function freeTableName(base) {
|
|
116
|
+
const taken = new Set(rawTables.list().map((t) => t.name.toLowerCase()));
|
|
117
|
+
const root = base.replace(/\d+$/, '') || 'Table';
|
|
118
|
+
for (let i = 2;; i += 1) {
|
|
119
|
+
for (const candidate of [`${root}${i}`, `Table${i}`]) {
|
|
120
|
+
if (!taken.has(candidate.toLowerCase()) && isValidTableName(candidate))
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function afterTables() {
|
|
126
|
+
dropAll();
|
|
127
|
+
loadEngine();
|
|
128
|
+
settleAll();
|
|
129
|
+
}
|
|
41
130
|
const functions = withCustomFunctions(options.functions);
|
|
131
|
+
const engine = options.engine ?? builtinEngine();
|
|
42
132
|
/** Computed values, keyed the same way the graph is. Rebuilt lazily and
|
|
43
133
|
* invalidated by every write, so it can never hold a value whose formula
|
|
44
134
|
* has since changed. */
|
|
45
135
|
const values = new Map();
|
|
46
136
|
/** Cells currently being resolved, for cycle detection. */
|
|
47
137
|
const visiting = new Set();
|
|
138
|
+
/** Iterative calculation, and the value each cell had on the pass before:
|
|
139
|
+
* what a cell in a cycle reads for itself while iteration is on, and what
|
|
140
|
+
* the convergence test measures against. */
|
|
141
|
+
let iteration = cleanIteration(options.iteration);
|
|
142
|
+
const previous = new Map();
|
|
143
|
+
/** Cells whose formula is volatile (INDIRECT, OFFSET, RAND, NOW ...):
|
|
144
|
+
* recomputed on every write, since the graph cannot see what they read. */
|
|
145
|
+
const volatile = new Set();
|
|
48
146
|
/** Parsed formulas, keyed by their TEXT rather than their position: a
|
|
49
147
|
* column of =A1*2 filled down is one parse, not one per row. */
|
|
50
148
|
const astCache = new Map();
|
|
@@ -52,9 +150,23 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
52
150
|
// the dependency graph only knows the CELLS a name pointed at when the
|
|
53
151
|
// formula was last evaluated. Dropping the cache is the honest answer:
|
|
54
152
|
// names change rarely, and a full recompute costs less than one stale total.
|
|
153
|
+
/**
|
|
154
|
+
* Dynamic arrays. A formula whose answer is a grid spills it over the
|
|
155
|
+
* cells below and to the right: `spills` keeps each anchor's rectangle,
|
|
156
|
+
* `spilledBy` points every covered cell back at its anchor, and a covered
|
|
157
|
+
* cell reads its value from the anchor's grid while its own text stays
|
|
158
|
+
* blank, as in Excel. An anchor whose rectangle runs into a cell that
|
|
159
|
+
* holds text, or into another spill, shows #SPILL! and sits in `blocked`
|
|
160
|
+
* until a write frees the way.
|
|
161
|
+
*/
|
|
162
|
+
const spills = new Map();
|
|
163
|
+
const spilledBy = new Map();
|
|
164
|
+
const blocked = new Set();
|
|
165
|
+
/** Cells whose value a spill just changed under them, to compute once the write settles. */
|
|
166
|
+
const pending = new Set();
|
|
55
167
|
const names = (() => {
|
|
56
168
|
const inner = createNames();
|
|
57
|
-
const drop = () => {
|
|
169
|
+
const drop = () => { dropAll(); settleAll(); };
|
|
58
170
|
return {
|
|
59
171
|
...inner,
|
|
60
172
|
define: (name, refersTo) => { inner.define(name, refersTo); drop(); },
|
|
@@ -72,6 +184,16 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
72
184
|
function resolveSheetName(sheet, self) {
|
|
73
185
|
return sheet ?? self;
|
|
74
186
|
}
|
|
187
|
+
/** The sheets from `from` to `to` in tab order, inclusive, for a 3D
|
|
188
|
+
* reference; null when either name is not a sheet. */
|
|
189
|
+
function sheetsInRange(from, to) {
|
|
190
|
+
const lower = order.map((n) => n.toLowerCase());
|
|
191
|
+
const i = lower.indexOf(from.toLowerCase());
|
|
192
|
+
const j = lower.indexOf(to.toLowerCase());
|
|
193
|
+
if (i < 0 || j < 0)
|
|
194
|
+
return null;
|
|
195
|
+
return i <= j ? order.slice(i, j + 1) : order.slice(j, i + 1);
|
|
196
|
+
}
|
|
75
197
|
function parseCached(text) {
|
|
76
198
|
if (astCache.has(text))
|
|
77
199
|
return astCache.get(text) ?? null;
|
|
@@ -85,8 +207,9 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
85
207
|
astCache.set(text, ast);
|
|
86
208
|
return ast;
|
|
87
209
|
}
|
|
88
|
-
function contextFor(self, override) {
|
|
210
|
+
function contextFor(self, override, at) {
|
|
89
211
|
return {
|
|
212
|
+
currentCell: at ? { sheet: self, row: at.row, col: at.col } : undefined,
|
|
90
213
|
resolve: (sheet, row, col) => {
|
|
91
214
|
const name = resolveSheetName(sheet, self);
|
|
92
215
|
// A validation rule reads the cell it is checking as the text being
|
|
@@ -96,10 +219,273 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
96
219
|
return compute(name, row, col);
|
|
97
220
|
},
|
|
98
221
|
lastRow: (sheet) => Math.max(rowCount(resolveSheetName(sheet, self)) - 1, 0),
|
|
222
|
+
spillRect: (sheet, row, col) => {
|
|
223
|
+
// The array anchored here, computed first so a formula that reads
|
|
224
|
+
// `A1#` sees the spill even on the pass that establishes it. Only an
|
|
225
|
+
// anchor has an entry; a covered cell or a plain value gives null,
|
|
226
|
+
// which the operator reads as #REF!.
|
|
227
|
+
const name = resolveSheetName(sheet, self);
|
|
228
|
+
compute(name, row, col);
|
|
229
|
+
const rect = spills.get(cellKey(name, row, col));
|
|
230
|
+
return rect ? [rect.r1, rect.c1, rect.r2, rect.c2] : null;
|
|
231
|
+
},
|
|
232
|
+
sheetsBetween: (from, to) => sheetsInRange(from, to),
|
|
99
233
|
resolveNameNode: (name) => names.resolve(name),
|
|
234
|
+
findTable: (name) => rawTables.get(name),
|
|
235
|
+
tableAt: (sheet, row, col) => rawTables.at(resolveSheetName(sheet, self), row, col),
|
|
236
|
+
hiddenRow: hiddenRows ? (sheet, row) => hiddenRows(resolveSheetName(sheet, self), row) : undefined,
|
|
237
|
+
isSubtotal: (sheet, row, col) => {
|
|
238
|
+
const text = sheetCells(resolveSheetName(sheet, self))?.[row]?.[col] ?? '';
|
|
239
|
+
return /^\s*=\s*SUBTOTAL\s*\(/i.test(text);
|
|
240
|
+
},
|
|
100
241
|
functions,
|
|
101
242
|
};
|
|
102
243
|
}
|
|
244
|
+
/** The engine's copy of the cells, for an engine that keeps one. */
|
|
245
|
+
function loadEngine() {
|
|
246
|
+
engine.load?.(order.map((name) => ({ name, cells: sheetCells(name).map((r) => [...r]) })));
|
|
247
|
+
}
|
|
248
|
+
/** Forget every derived thing: values, edges, volatility, spills. */
|
|
249
|
+
function dropAll() {
|
|
250
|
+
// Cells may have been replaced wholesale, so the SUBTOTAL flag is stale.
|
|
251
|
+
subtotalsHere = null;
|
|
252
|
+
values.clear();
|
|
253
|
+
previous.clear();
|
|
254
|
+
graph.clear();
|
|
255
|
+
volatile.clear();
|
|
256
|
+
spills.clear();
|
|
257
|
+
spilledBy.clear();
|
|
258
|
+
blocked.clear();
|
|
259
|
+
pending.clear();
|
|
260
|
+
}
|
|
261
|
+
const SUBTOTAL_TEXT = /^\s*=[\s\S]*SUBTOTAL\s*\(/i;
|
|
262
|
+
/** Whether any cell in the workbook holds a SUBTOTAL. Worked out once and
|
|
263
|
+
* forgotten again whenever a write could change the answer. */
|
|
264
|
+
function hasSubtotal() {
|
|
265
|
+
if (subtotalsHere !== null)
|
|
266
|
+
return subtotalsHere;
|
|
267
|
+
let found = false;
|
|
268
|
+
for (const sheet of order) {
|
|
269
|
+
const cells = sheetCells(sheet);
|
|
270
|
+
for (const line of cells) {
|
|
271
|
+
for (const text of line) {
|
|
272
|
+
if (text && SUBTOTAL_TEXT.test(text)) {
|
|
273
|
+
found = true;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (found)
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (found)
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
subtotalsHere = found;
|
|
284
|
+
return found;
|
|
285
|
+
}
|
|
286
|
+
/** Whether a key holds a formula, for the settling passes. */
|
|
287
|
+
function isFormulaKey(key) {
|
|
288
|
+
const at = parseCellKey(key);
|
|
289
|
+
return (sheetCells(at.sheet ?? '')?.[at.row]?.[at.col] ?? '').trim().startsWith('=');
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Compute every formula in the workbook, so every spill is on record
|
|
293
|
+
* before any cell it covers is read. Lazy evaluation alone would leave a
|
|
294
|
+
* covered cell blank when it is read before its anchor, which a repaint
|
|
295
|
+
* from the top of a scrolled sheet does.
|
|
296
|
+
*/
|
|
297
|
+
function settleAll() {
|
|
298
|
+
for (const other of order) {
|
|
299
|
+
const cells = sheetCells(other) ?? [];
|
|
300
|
+
for (let r = 0; r < cells.length; r += 1) {
|
|
301
|
+
const line = cells[r];
|
|
302
|
+
for (let c = 0; c < line.length; c += 1)
|
|
303
|
+
if (line[c].trim().startsWith('='))
|
|
304
|
+
compute(other, r, c);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
settlePending();
|
|
308
|
+
runIterations();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Run the cycles until they stop moving.
|
|
312
|
+
*
|
|
313
|
+
* Everything in a cycle, and everything downstream of one, is recomputed
|
|
314
|
+
* from the values it last had; a pass that moves no cell by more than
|
|
315
|
+
* `maxChange` is the last one, and so is pass number `maxIterations`. This
|
|
316
|
+
* is Excel's model exactly: it does not promise convergence, it promises a
|
|
317
|
+
* bounded number of tries and whatever they reached.
|
|
318
|
+
*/
|
|
319
|
+
function runIterations() {
|
|
320
|
+
if (!iteration.enabled)
|
|
321
|
+
return [];
|
|
322
|
+
const seeds = graph.cycles();
|
|
323
|
+
if (seeds.length === 0)
|
|
324
|
+
return [];
|
|
325
|
+
// Cycle members are downstream of each other, so `dirtyFrom` already
|
|
326
|
+
// includes them; the union is for a seed whose readers were never
|
|
327
|
+
// evaluated.
|
|
328
|
+
const affected = [...new Set([...seeds, ...graph.dirtyFrom(seeds)])];
|
|
329
|
+
for (let pass = 0; pass < Math.max(1, iteration.maxIterations); pass += 1) {
|
|
330
|
+
const before = new Map();
|
|
331
|
+
for (const key of affected) {
|
|
332
|
+
before.set(key, values.get(key) ?? previous.get(key));
|
|
333
|
+
values.delete(key);
|
|
334
|
+
}
|
|
335
|
+
let worst = 0;
|
|
336
|
+
for (const key of affected) {
|
|
337
|
+
const at = parseCellKey(key);
|
|
338
|
+
const after = compute(at.sheet ?? '', at.row, at.col);
|
|
339
|
+
worst = Math.max(worst, moved(before.get(key), after));
|
|
340
|
+
}
|
|
341
|
+
if (worst <= iteration.maxChange)
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
return affected;
|
|
345
|
+
}
|
|
346
|
+
/** Compute what a spill changed under other formulas, until nothing is left. */
|
|
347
|
+
function settlePending() {
|
|
348
|
+
const done = [];
|
|
349
|
+
while (pending.size) {
|
|
350
|
+
const batch = [...pending];
|
|
351
|
+
pending.clear();
|
|
352
|
+
for (const key of batch) {
|
|
353
|
+
const at = parseCellKey(key);
|
|
354
|
+
compute(at.sheet ?? '', at.row, at.col);
|
|
355
|
+
done.push(key);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return done;
|
|
359
|
+
}
|
|
360
|
+
/** Drop an anchor's spill; the covered cells' values and edges go with it. */
|
|
361
|
+
function clearSpill(anchor) {
|
|
362
|
+
const rect = spills.get(anchor);
|
|
363
|
+
if (!rect)
|
|
364
|
+
return [];
|
|
365
|
+
spills.delete(anchor);
|
|
366
|
+
const at = parseCellKey(anchor);
|
|
367
|
+
const covered = [];
|
|
368
|
+
for (let r = rect.r1; r <= rect.r2; r += 1) {
|
|
369
|
+
for (let c = rect.c1; c <= rect.c2; c += 1) {
|
|
370
|
+
if (r === at.row && c === at.col)
|
|
371
|
+
continue;
|
|
372
|
+
const key = cellKey(at.sheet, r, c);
|
|
373
|
+
if (spilledBy.get(key) !== anchor)
|
|
374
|
+
continue;
|
|
375
|
+
spilledBy.delete(key);
|
|
376
|
+
values.delete(key);
|
|
377
|
+
graph.setPrecedents(key, null);
|
|
378
|
+
covered.push(key);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return covered;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Record an anchor's grid over its rectangle, or refuse it. Returns the
|
|
385
|
+
* anchor's own value: the grid's top-left cell, or #SPILL! when a cell
|
|
386
|
+
* in the way holds text or belongs to another spill.
|
|
387
|
+
*/
|
|
388
|
+
function recordSpill(anchor, sheet, row, col, grid) {
|
|
389
|
+
const cells = sheetCells(sheet);
|
|
390
|
+
const rect = { r1: row, c1: col, r2: row + grid.length - 1, c2: col + (grid[0]?.length ?? 1) - 1 };
|
|
391
|
+
for (let r = rect.r1; r <= rect.r2; r += 1) {
|
|
392
|
+
for (let c = rect.c1; c <= rect.c2; c += 1) {
|
|
393
|
+
if (r === row && c === col)
|
|
394
|
+
continue;
|
|
395
|
+
const key = cellKey(sheet, r, c);
|
|
396
|
+
const owner = spilledBy.get(key);
|
|
397
|
+
if ((cells[r]?.[c] ?? '').trim() !== '' || (owner !== undefined && owner !== anchor)) {
|
|
398
|
+
for (const k of clearSpill(anchor))
|
|
399
|
+
pending.add(k);
|
|
400
|
+
blocked.add(anchor);
|
|
401
|
+
return { error: '#SPILL!' };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
blocked.delete(anchor);
|
|
406
|
+
const before = spills.get(anchor);
|
|
407
|
+
const stillCovered = new Set();
|
|
408
|
+
for (let r = rect.r1; r <= rect.r2; r += 1) {
|
|
409
|
+
for (let c = rect.c1; c <= rect.c2; c += 1) {
|
|
410
|
+
if (r === row && c === col)
|
|
411
|
+
continue;
|
|
412
|
+
const key = cellKey(sheet, r, c);
|
|
413
|
+
stillCovered.add(key);
|
|
414
|
+
const fresh = spilledBy.get(key) !== anchor;
|
|
415
|
+
spilledBy.set(key, anchor);
|
|
416
|
+
values.set(key, grid[r - row]?.[c - col] ?? '');
|
|
417
|
+
graph.setPrecedents(key, [anchor]);
|
|
418
|
+
// Reported as changed, and a formula that read this cell while it
|
|
419
|
+
// was blank has a stale value.
|
|
420
|
+
pending.add(key);
|
|
421
|
+
if (fresh)
|
|
422
|
+
for (const k of graph.dirtyFrom([key]))
|
|
423
|
+
if (k !== key) {
|
|
424
|
+
values.delete(k);
|
|
425
|
+
pending.add(k);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// The cells the old rectangle covered and the new one does not read blank again.
|
|
430
|
+
if (before) {
|
|
431
|
+
for (let r = before.r1; r <= before.r2; r += 1) {
|
|
432
|
+
for (let c = before.c1; c <= before.c2; c += 1) {
|
|
433
|
+
const key = cellKey(sheet, r, c);
|
|
434
|
+
if (stillCovered.has(key) || spilledBy.get(key) !== anchor)
|
|
435
|
+
continue;
|
|
436
|
+
spilledBy.delete(key);
|
|
437
|
+
values.delete(key);
|
|
438
|
+
graph.setPrecedents(key, null);
|
|
439
|
+
pending.add(key);
|
|
440
|
+
for (const k of graph.dirtyFrom([key]))
|
|
441
|
+
if (k !== key) {
|
|
442
|
+
values.delete(k);
|
|
443
|
+
pending.add(k);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
spills.set(anchor, rect);
|
|
449
|
+
return grid[0]?.[0] ?? '';
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* How deep a cell's value may be resolved by recursion before the chain is
|
|
453
|
+
* primed from the far end instead. A running-balance column is a chain as
|
|
454
|
+
* long as the sheet, and evaluating the last cell of one by recursion
|
|
455
|
+
* overflows the JavaScript stack somewhere past a thousand rows, which the
|
|
456
|
+
* evaluator turns into a `#NUM!`. Priming keeps the depth flat, so the
|
|
457
|
+
* only limit left is the sheet's own size.
|
|
458
|
+
*/
|
|
459
|
+
const DEEP = 150;
|
|
460
|
+
let depth = 0;
|
|
461
|
+
/**
|
|
462
|
+
* Compute what a cell reads, furthest first, so the cell itself resolves
|
|
463
|
+
* against cached values rather than by recursing through the chain. The
|
|
464
|
+
* precedents are the ones the graph recorded at the last evaluation, which
|
|
465
|
+
* is exactly the chain a re-evaluation is about to walk again.
|
|
466
|
+
*/
|
|
467
|
+
function primeChain(from) {
|
|
468
|
+
const seen = new Set([from]);
|
|
469
|
+
const chain = [from];
|
|
470
|
+
const stack = [from];
|
|
471
|
+
while (stack.length) {
|
|
472
|
+
const cur = stack.pop();
|
|
473
|
+
for (const p of graph.precedentsOf(cur)) {
|
|
474
|
+
if (seen.has(p) || values.get(p) !== undefined)
|
|
475
|
+
continue;
|
|
476
|
+
seen.add(p);
|
|
477
|
+
chain.push(p);
|
|
478
|
+
stack.push(p);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
for (let i = chain.length - 1; i >= 1; i -= 1) {
|
|
482
|
+
const key = chain[i];
|
|
483
|
+
if (values.get(key) !== undefined)
|
|
484
|
+
continue;
|
|
485
|
+
const at = parseCellKey(key);
|
|
486
|
+
compute(at.sheet ?? '', at.row, at.col);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
103
489
|
function compute(sheet, row, col) {
|
|
104
490
|
const cells = sheetCells(sheet);
|
|
105
491
|
if (!cells)
|
|
@@ -114,39 +500,123 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
114
500
|
// #REF! is reserved for the two cases that really are broken: a sheet
|
|
115
501
|
// that does not exist, and a negative index, which is what a reference
|
|
116
502
|
// shifted off the top by a delete becomes.
|
|
117
|
-
if (row >= cells.length || col >= (cells[row]?.length ?? 0))
|
|
118
|
-
return '';
|
|
119
503
|
const key = cellKey(sheet, row, col);
|
|
120
504
|
const cached = values.get(key);
|
|
121
505
|
if (cached !== undefined)
|
|
122
506
|
return cached;
|
|
123
|
-
if (
|
|
124
|
-
|
|
507
|
+
if (row >= cells.length || col >= (cells[row]?.length ?? 0)) {
|
|
508
|
+
// Past the written area, unless a spill reaches here.
|
|
509
|
+
const anchor = spilledBy.get(key);
|
|
510
|
+
if (anchor === undefined)
|
|
511
|
+
return '';
|
|
512
|
+
const at = parseCellKey(anchor);
|
|
513
|
+
compute(at.sheet ?? sheet, at.row, at.col);
|
|
514
|
+
return values.get(key) ?? '';
|
|
515
|
+
}
|
|
516
|
+
if (visiting.has(key)) {
|
|
517
|
+
// The cell is being asked for itself. Without iteration that is the
|
|
518
|
+
// error; with it, the loop is meant, and the answer is what this cell
|
|
519
|
+
// was worth on the last pass, starting from 0 as Excel does.
|
|
520
|
+
if (!iteration.enabled)
|
|
521
|
+
return { error: '#CYCLE!' };
|
|
522
|
+
return previous.get(key) ?? 0;
|
|
523
|
+
}
|
|
524
|
+
// Deep enough that recursion is a risk: resolve what this cell reads from
|
|
525
|
+
// the far end first, and come back to it with everything cached.
|
|
526
|
+
if (depth >= DEEP)
|
|
527
|
+
primeChain(key);
|
|
125
528
|
visiting.add(key);
|
|
529
|
+
depth += 1;
|
|
126
530
|
const text = (cells[row]?.[col] ?? '').trim();
|
|
127
531
|
let value;
|
|
128
532
|
if (text === '') {
|
|
533
|
+
// A blank cell shows what an array formula spills into it.
|
|
534
|
+
const anchor = spilledBy.get(key);
|
|
535
|
+
if (anchor !== undefined) {
|
|
536
|
+
const at = parseCellKey(anchor);
|
|
537
|
+
compute(at.sheet ?? sheet, at.row, at.col);
|
|
538
|
+
const spilled = values.get(key);
|
|
539
|
+
if (spilled !== undefined) {
|
|
540
|
+
visiting.delete(key);
|
|
541
|
+
depth -= 1;
|
|
542
|
+
return spilled;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
129
545
|
value = '';
|
|
130
546
|
}
|
|
131
547
|
else if (text.startsWith('=')) {
|
|
132
548
|
const ast = parseCached(text);
|
|
133
|
-
|
|
134
|
-
|
|
549
|
+
// The graph, the volatile set and the spill ranges are read off the
|
|
550
|
+
// reference grammar, which is Excel's whatever evaluates it, so they
|
|
551
|
+
// stay right for any engine. Only the value and the grid are asked for.
|
|
552
|
+
const result = engine.evaluate(text, { sheet, row, col }, {
|
|
553
|
+
context: contextFor(sheet, undefined, { row, col }),
|
|
554
|
+
parse: parseCached,
|
|
555
|
+
});
|
|
556
|
+
value = result.value;
|
|
557
|
+
const grid = isError(value) ? null : result.spill ?? null;
|
|
558
|
+
if (grid)
|
|
559
|
+
value = recordSpill(key, sheet, row, col, grid);
|
|
560
|
+
else if (spills.has(key))
|
|
561
|
+
for (const k of clearSpill(key))
|
|
562
|
+
pending.add(k);
|
|
563
|
+
if (ast) {
|
|
564
|
+
if (isVolatile(ast))
|
|
565
|
+
volatile.add(key);
|
|
566
|
+
else
|
|
567
|
+
volatile.delete(key);
|
|
568
|
+
graph.setPrecedents(key, precedentsOf(ast, { sheet }, (s) => Math.max(rowCount(s ?? sheet) - 1, 0), (name) => names.resolve(name),
|
|
569
|
+
// A structured reference's cells, so a total over a table is
|
|
570
|
+
// recalculated when a cell inside the table is typed. Best effort:
|
|
571
|
+
// a reference that cannot be resolved records no precedents rather
|
|
572
|
+
// than taking the cell's value down with it, since the value has
|
|
573
|
+
// already been worked out by the time this runs.
|
|
574
|
+
(node) => {
|
|
575
|
+
try {
|
|
576
|
+
return tableRectOf(node, contextFor(sheet, undefined, { row, col }));
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
// The cells a spilled-range operator covers right now. The anchor
|
|
583
|
+
// has just been computed by this same pass, so its rectangle is on
|
|
584
|
+
// record; reading it here (rather than recomputing) keeps the
|
|
585
|
+
// dependency extraction free of side effects.
|
|
586
|
+
(node) => {
|
|
587
|
+
const name = resolveSheetName(node.ref.sheet, sheet);
|
|
588
|
+
const rect = spills.get(cellKey(name, node.ref.row ?? 0, node.ref.col));
|
|
589
|
+
return rect ? { sheet: name, r1: rect.r1, c1: rect.c1, r2: rect.r2, c2: rect.c2 } : null;
|
|
590
|
+
},
|
|
591
|
+
// The sheets a 3D reference spans, so it recalculates when the cell
|
|
592
|
+
// on any of them is typed into.
|
|
593
|
+
(from, to) => sheetsInRange(from, to)));
|
|
594
|
+
}
|
|
135
595
|
else {
|
|
136
|
-
|
|
137
|
-
|
|
596
|
+
graph.setPrecedents(key, null);
|
|
597
|
+
volatile.delete(key);
|
|
138
598
|
}
|
|
139
599
|
}
|
|
600
|
+
else if (text.startsWith(TEXT_PREFIX)) {
|
|
601
|
+
// Excel's apostrophe: what follows is text, whatever it looks like,
|
|
602
|
+
// and the apostrophe itself is not part of it. It is the only way to
|
|
603
|
+
// keep a leading zero, a part number that reads as a date, or the
|
|
604
|
+
// text of a formula in a cell.
|
|
605
|
+
value = text.slice(1);
|
|
606
|
+
}
|
|
140
607
|
else {
|
|
141
608
|
const n = Number(text);
|
|
142
609
|
const upper = text.toUpperCase();
|
|
143
610
|
value = text !== '' && Number.isFinite(n) ? n
|
|
144
611
|
: upper === 'TRUE' ? true
|
|
145
612
|
: upper === 'FALSE' ? false
|
|
146
|
-
: text;
|
|
613
|
+
: typedError(text) ?? text;
|
|
147
614
|
}
|
|
148
615
|
visiting.delete(key);
|
|
616
|
+
depth -= 1;
|
|
149
617
|
values.set(key, value);
|
|
618
|
+
if (iteration.enabled)
|
|
619
|
+
previous.set(key, value);
|
|
150
620
|
return value;
|
|
151
621
|
}
|
|
152
622
|
/** What typed text is worth when it is not a formula: a number, a boolean or itself. */
|
|
@@ -154,11 +624,16 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
154
624
|
const t = text.trim();
|
|
155
625
|
if (t === '')
|
|
156
626
|
return '';
|
|
627
|
+
if (t.startsWith(TEXT_PREFIX))
|
|
628
|
+
return t.slice(1);
|
|
157
629
|
if (t.startsWith('='))
|
|
158
630
|
return t;
|
|
159
631
|
const n = Number(t);
|
|
160
632
|
const upper = t.toUpperCase();
|
|
161
|
-
return Number.isFinite(n) ? n
|
|
633
|
+
return Number.isFinite(n) ? n
|
|
634
|
+
: upper === 'TRUE' ? true
|
|
635
|
+
: upper === 'FALSE' ? false
|
|
636
|
+
: typedError(t) ?? t;
|
|
162
637
|
}
|
|
163
638
|
function rowCount(sheet) {
|
|
164
639
|
return sheetCells(sheet)?.length ?? 0;
|
|
@@ -169,17 +644,29 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
169
644
|
return 0;
|
|
170
645
|
return cells.reduce((max, row) => Math.max(max, row.length), 0);
|
|
171
646
|
}
|
|
172
|
-
/** Drop cached values for a cell and everything downstream of it
|
|
647
|
+
/** Drop cached values for a cell and everything downstream of it, and
|
|
648
|
+
* for every volatile cell and its readers, which any write may affect. */
|
|
173
649
|
function invalidate(keys) {
|
|
174
650
|
const touched = [...keys];
|
|
175
651
|
for (const key of keys)
|
|
176
652
|
values.delete(key);
|
|
177
|
-
for (const key of
|
|
653
|
+
for (const key of volatile) {
|
|
654
|
+
if (values.delete(key))
|
|
655
|
+
touched.push(key);
|
|
656
|
+
}
|
|
657
|
+
for (const key of graph.dirtyFrom([...keys, ...volatile])) {
|
|
178
658
|
values.delete(key);
|
|
179
659
|
touched.push(key);
|
|
180
660
|
}
|
|
181
661
|
return touched;
|
|
182
662
|
}
|
|
663
|
+
/** Every listener on raw writes, plus the option that is one of them. */
|
|
664
|
+
const writeListeners = new Set();
|
|
665
|
+
function emitWrite(change) {
|
|
666
|
+
options.onWrite?.(change);
|
|
667
|
+
for (const listener of writeListeners)
|
|
668
|
+
listener(change);
|
|
669
|
+
}
|
|
183
670
|
function reportRecalc(keys) {
|
|
184
671
|
const cb = options.onRecalc;
|
|
185
672
|
if (!cb || keys.length === 0)
|
|
@@ -197,6 +684,7 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
197
684
|
get sheets() { return [...order]; },
|
|
198
685
|
get active() { return active; },
|
|
199
686
|
get names() { return names; },
|
|
687
|
+
get tables() { return tables; },
|
|
200
688
|
setActive(name) {
|
|
201
689
|
if (sheetCells(name))
|
|
202
690
|
active = order.find((n) => n.toLowerCase() === name.toLowerCase()) ?? active;
|
|
@@ -212,6 +700,7 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
212
700
|
const index = at === undefined ? order.length : Math.max(0, Math.min(at, order.length));
|
|
213
701
|
order.splice(index, 0, chosen);
|
|
214
702
|
active = chosen;
|
|
703
|
+
loadEngine();
|
|
215
704
|
return chosen;
|
|
216
705
|
},
|
|
217
706
|
removeSheet(name) {
|
|
@@ -222,12 +711,19 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
222
711
|
return false;
|
|
223
712
|
const [removed] = order.splice(index, 1);
|
|
224
713
|
byName.delete(removed.toLowerCase());
|
|
714
|
+
// The tables that lived on it go with it: a table on a sheet that is
|
|
715
|
+
// gone resolves to cells that are gone.
|
|
716
|
+
for (const table of rawTables.list()) {
|
|
717
|
+
if (table.sheet.toLowerCase() === removed.toLowerCase())
|
|
718
|
+
rawTables.remove(table.name);
|
|
719
|
+
}
|
|
225
720
|
// Every cached value may have read the sheet that just went.
|
|
226
|
-
|
|
227
|
-
|
|
721
|
+
dropAll();
|
|
722
|
+
loadEngine();
|
|
228
723
|
if (active.toLowerCase() === removed.toLowerCase()) {
|
|
229
724
|
active = order[Math.min(index, order.length - 1)] ?? order[0];
|
|
230
725
|
}
|
|
726
|
+
settleAll();
|
|
231
727
|
return true;
|
|
232
728
|
},
|
|
233
729
|
renameSheet(from, to) {
|
|
@@ -260,8 +756,16 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
260
756
|
if (typeof next === 'string' && next !== entry.refersTo)
|
|
261
757
|
names.define(entry.name, next);
|
|
262
758
|
}
|
|
263
|
-
|
|
264
|
-
|
|
759
|
+
// A table names the sheet it sits on, and a structured reference
|
|
760
|
+
// resolves through that name. Leaving it behind turned every
|
|
761
|
+
// `=SUM(Orders[Amount])` into #REF! the moment the sheet was renamed.
|
|
762
|
+
for (const table of rawTables.list()) {
|
|
763
|
+
if (table.sheet.toLowerCase() === from.toLowerCase())
|
|
764
|
+
rawTables.define({ ...table, sheet: to });
|
|
765
|
+
}
|
|
766
|
+
dropAll();
|
|
767
|
+
loadEngine();
|
|
768
|
+
settleAll();
|
|
265
769
|
return true;
|
|
266
770
|
},
|
|
267
771
|
moveSheet(name, to) {
|
|
@@ -272,6 +776,46 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
272
776
|
order.splice(Math.max(0, Math.min(to, order.length)), 0, moved);
|
|
273
777
|
return true;
|
|
274
778
|
},
|
|
779
|
+
copySheet(from, to) {
|
|
780
|
+
const index = order.findIndex((n) => n.toLowerCase() === from.toLowerCase());
|
|
781
|
+
const source = sheetCells(from);
|
|
782
|
+
if (index < 0 || !source)
|
|
783
|
+
return null;
|
|
784
|
+
let name = to;
|
|
785
|
+
if (name === undefined) {
|
|
786
|
+
const base = order[index];
|
|
787
|
+
for (let i = 2;; i += 1) {
|
|
788
|
+
const candidate = `${base} (${i})`;
|
|
789
|
+
if (!sheetCells(candidate) && isValidSheetName(candidate)) {
|
|
790
|
+
name = candidate;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
if (i > 1000)
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (!isValidSheetName(name) || sheetCells(name))
|
|
798
|
+
return null;
|
|
799
|
+
byName.set(name.toLowerCase(), source.map((line) => [...line]));
|
|
800
|
+
order.splice(index + 1, 0, name);
|
|
801
|
+
active = name;
|
|
802
|
+
// A table belongs to its sheet, so the copy gets one of its own, as
|
|
803
|
+
// Excel's Move or Copy does. Without it the copied cells keep their
|
|
804
|
+
// `[@Qty]` formulas with no table to resolve them, and every one of
|
|
805
|
+
// them reads #REF!.
|
|
806
|
+
for (const table of rawTables.list()) {
|
|
807
|
+
if (table.sheet.toLowerCase() !== from.toLowerCase())
|
|
808
|
+
continue;
|
|
809
|
+
rawTables.define({ ...table, sheet: name, name: freeTableName(table.name) });
|
|
810
|
+
}
|
|
811
|
+
// Nothing cached read the new sheet yet, but a formula elsewhere
|
|
812
|
+
// that reads a whole column of it by name cannot exist either, so
|
|
813
|
+
// only the graph needs the new cells' precedents, which compute
|
|
814
|
+
// records on first read. An engine with a copy of the cells does
|
|
815
|
+
// need the new sheet.
|
|
816
|
+
loadEngine();
|
|
817
|
+
return name;
|
|
818
|
+
},
|
|
275
819
|
getRaw(sheet, row, col) {
|
|
276
820
|
return sheetCells(sheet)?.[row]?.[col] ?? '';
|
|
277
821
|
},
|
|
@@ -286,17 +830,41 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
286
830
|
line.push('');
|
|
287
831
|
if (line[col] === text)
|
|
288
832
|
return;
|
|
833
|
+
if (SUBTOTAL_TEXT.test(text) || SUBTOTAL_TEXT.test(line[col] ?? ''))
|
|
834
|
+
subtotalsHere = null;
|
|
289
835
|
line[col] = text;
|
|
836
|
+
engine.write?.(sheet, row, col, text);
|
|
837
|
+
emitWrite({ sheet: order.find((n) => n.toLowerCase() === sheet.toLowerCase()) ?? sheet, row, col, text });
|
|
290
838
|
const key = cellKey(sheet, row, col);
|
|
291
|
-
|
|
292
|
-
|
|
839
|
+
const keys = [key];
|
|
840
|
+
// A cell that is no longer a formula reads nothing, and spills nothing.
|
|
841
|
+
if (!text.trim().startsWith('=')) {
|
|
293
842
|
graph.setPrecedents(key, null);
|
|
294
|
-
|
|
843
|
+
volatile.delete(key);
|
|
844
|
+
}
|
|
845
|
+
if (spills.has(key))
|
|
846
|
+
keys.push(...clearSpill(key));
|
|
847
|
+
// Text landing in a spilled cell blocks its anchor; a cell going
|
|
848
|
+
// blank may free one that was blocked.
|
|
849
|
+
const anchor = spilledBy.get(key);
|
|
850
|
+
if (anchor !== undefined)
|
|
851
|
+
keys.push(anchor);
|
|
852
|
+
keys.push(...blocked);
|
|
853
|
+
const touched = invalidate(keys);
|
|
854
|
+
// The written cell and the anchors it touched compute now, so a new
|
|
855
|
+
// or changed spill is on record before anything reads under it;
|
|
856
|
+
// plain dependents wait for their next read, as before.
|
|
857
|
+
for (const k of keys)
|
|
858
|
+
if (isFormulaKey(k)) {
|
|
859
|
+
const at = parseCellKey(k);
|
|
860
|
+
compute(at.sheet ?? sheet, at.row, at.col);
|
|
861
|
+
}
|
|
862
|
+
reportRecalc([...new Set([...touched, ...settlePending(), ...runIterations()])]);
|
|
295
863
|
},
|
|
296
864
|
getValue(sheet, row, col) {
|
|
297
865
|
return compute(sheet, row, col);
|
|
298
866
|
},
|
|
299
|
-
evaluateText(sheet, text, override) {
|
|
867
|
+
evaluateText(sheet, text, override, at) {
|
|
300
868
|
const t = text.trim();
|
|
301
869
|
if (t === '')
|
|
302
870
|
return '';
|
|
@@ -304,7 +872,9 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
304
872
|
if (!sheetCells(sheet))
|
|
305
873
|
return { error: '#REF!' };
|
|
306
874
|
const ast = parseCached(t);
|
|
307
|
-
|
|
875
|
+
// The cell the text is read for, so ROW() and COLUMN() and a
|
|
876
|
+
// [@Column] reference mean something in a rule's formula.
|
|
877
|
+
return ast ? evaluate(ast, contextFor(sheet, override, at ?? (override ? { row: override.row, col: override.col } : undefined))) : { error: '#PARSE!' };
|
|
308
878
|
}
|
|
309
879
|
return literal(t);
|
|
310
880
|
},
|
|
@@ -327,10 +897,55 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
327
897
|
}
|
|
328
898
|
return out;
|
|
329
899
|
},
|
|
900
|
+
repointAfterMove(move) {
|
|
901
|
+
if (!sheetCells(move.sheet) || !sheetCells(move.toSheet))
|
|
902
|
+
return;
|
|
903
|
+
if (move.dRow === 0 && move.dCol === 0 && move.sheet === move.toSheet)
|
|
904
|
+
return;
|
|
905
|
+
let touched = false;
|
|
906
|
+
// The whole workbook: another sheet's =Orders!A5 names a moved cell
|
|
907
|
+
// just as a local reference does.
|
|
908
|
+
for (const other of order) {
|
|
909
|
+
const cells = sheetCells(other);
|
|
910
|
+
for (let r = 0; r < cells.length; r += 1) {
|
|
911
|
+
const line = cells[r];
|
|
912
|
+
for (let c = 0; c < line.length; c += 1) {
|
|
913
|
+
const text = line[c] ?? '';
|
|
914
|
+
if (!text.startsWith('='))
|
|
915
|
+
continue;
|
|
916
|
+
const next = repointReferences(text, move, other);
|
|
917
|
+
if (typeof next === 'string' && next !== text) {
|
|
918
|
+
line[c] = next;
|
|
919
|
+
touched = true;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// A name has no home sheet, so only a qualified reference can match.
|
|
925
|
+
for (const entry of names.list()) {
|
|
926
|
+
const next = repointReferences(entry.refersTo, move, null);
|
|
927
|
+
if (typeof next === 'string' && next !== entry.refersTo) {
|
|
928
|
+
names.define(entry.name, next);
|
|
929
|
+
touched = true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (!touched)
|
|
933
|
+
return;
|
|
934
|
+
dropAll();
|
|
935
|
+
astCache.clear();
|
|
936
|
+
loadEngine();
|
|
937
|
+
settleAll();
|
|
938
|
+
},
|
|
330
939
|
applyStructuralEdit(sheet, edit) {
|
|
331
940
|
const target = sheetCells(sheet);
|
|
332
941
|
if (!target || edit.count <= 0)
|
|
333
942
|
return;
|
|
943
|
+
// The tables move with their cells, so a structured reference keeps
|
|
944
|
+
// meaning the same column after a row is inserted above the table.
|
|
945
|
+
const moved = shiftTables(rawTables.list(), sheet, edit);
|
|
946
|
+
rawTables.clear();
|
|
947
|
+
for (const table of moved)
|
|
948
|
+
rawTables.define(table);
|
|
334
949
|
// Rewrite every formula in the WORKBOOK, not just this sheet: another
|
|
335
950
|
// sheet may hold =Orders!A5, and inserting a row in Orders has to move
|
|
336
951
|
// it exactly as if it were local.
|
|
@@ -381,21 +996,115 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
381
996
|
line.splice(edit.at, edit.count);
|
|
382
997
|
}
|
|
383
998
|
}
|
|
384
|
-
|
|
385
|
-
graph.clear();
|
|
999
|
+
dropAll();
|
|
386
1000
|
astCache.clear();
|
|
1001
|
+
loadEngine();
|
|
1002
|
+
settleAll();
|
|
1003
|
+
},
|
|
1004
|
+
precedents(sheet, row, col) {
|
|
1005
|
+
const name = order.find((n) => n.toLowerCase() === sheet.toLowerCase());
|
|
1006
|
+
if (!name)
|
|
1007
|
+
return [];
|
|
1008
|
+
// Computing the cell records what it reads.
|
|
1009
|
+
compute(name, row, col);
|
|
1010
|
+
return graph.precedentsOf(cellKey(name, row, col)).map((key) => {
|
|
1011
|
+
const at = parseCellKey(key);
|
|
1012
|
+
return { sheet: at.sheet ?? name, row: at.row, col: at.col };
|
|
1013
|
+
});
|
|
1014
|
+
},
|
|
1015
|
+
dependents(sheet, row, col) {
|
|
1016
|
+
const name = order.find((n) => n.toLowerCase() === sheet.toLowerCase());
|
|
1017
|
+
if (!name)
|
|
1018
|
+
return [];
|
|
1019
|
+
// A formula that was never evaluated has no edge yet: evaluate every
|
|
1020
|
+
// formula once so the graph is whole.
|
|
1021
|
+
for (const other of order) {
|
|
1022
|
+
const cells = sheetCells(other) ?? [];
|
|
1023
|
+
for (let r = 0; r < cells.length; r += 1) {
|
|
1024
|
+
const line = cells[r];
|
|
1025
|
+
for (let c = 0; c < line.length; c += 1)
|
|
1026
|
+
if (line[c].trim().startsWith('='))
|
|
1027
|
+
compute(other, r, c);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return graph.dependentsOf(cellKey(name, row, col)).map((key) => {
|
|
1031
|
+
const at = parseCellKey(key);
|
|
1032
|
+
return { sheet: at.sheet ?? name, row: at.row, col: at.col };
|
|
1033
|
+
});
|
|
1034
|
+
},
|
|
1035
|
+
subscribeWrites(listener) {
|
|
1036
|
+
writeListeners.add(listener);
|
|
1037
|
+
return () => { writeListeners.delete(listener); };
|
|
1038
|
+
},
|
|
1039
|
+
setHiddenRows(source) {
|
|
1040
|
+
hiddenRows = source;
|
|
1041
|
+
// Only SUBTOTAL reads what is hidden, and nothing in the dependency
|
|
1042
|
+
// graph can see that it changed - so everything is recomputed, but
|
|
1043
|
+
// only when there is a SUBTOTAL to recompute.
|
|
1044
|
+
if (!hasSubtotal())
|
|
1045
|
+
return;
|
|
1046
|
+
dropAll();
|
|
1047
|
+
settleAll();
|
|
387
1048
|
},
|
|
388
1049
|
recalculate() {
|
|
389
|
-
|
|
390
|
-
|
|
1050
|
+
dropAll();
|
|
1051
|
+
loadEngine();
|
|
1052
|
+
settleAll();
|
|
1053
|
+
},
|
|
1054
|
+
get iteration() { return { ...iteration }; },
|
|
1055
|
+
setIteration(next) {
|
|
1056
|
+
const wanted = cleanIteration({ ...iteration, ...next });
|
|
1057
|
+
if (wanted.enabled === iteration.enabled && wanted.maxIterations === iteration.maxIterations && wanted.maxChange === iteration.maxChange)
|
|
1058
|
+
return;
|
|
1059
|
+
iteration = wanted;
|
|
1060
|
+
// Turning it on turns #CYCLE! into a number, and turning it off turns
|
|
1061
|
+
// the number back into #CYCLE!, so every cached value is suspect.
|
|
1062
|
+
dropAll();
|
|
1063
|
+
settleAll();
|
|
1064
|
+
},
|
|
1065
|
+
spillOf(sheet, row, col) {
|
|
1066
|
+
const name = order.find((n) => n.toLowerCase() === sheet.toLowerCase());
|
|
1067
|
+
if (!name)
|
|
1068
|
+
return null;
|
|
1069
|
+
compute(name, row, col);
|
|
1070
|
+
const key = cellKey(name, row, col);
|
|
1071
|
+
const anchor = spills.has(key) ? key : spilledBy.get(key);
|
|
1072
|
+
if (anchor === undefined)
|
|
1073
|
+
return null;
|
|
1074
|
+
const rect = spills.get(anchor);
|
|
1075
|
+
if (!rect)
|
|
1076
|
+
return null;
|
|
1077
|
+
const at = parseCellKey(anchor);
|
|
1078
|
+
return { anchor: { row: at.row, col: at.col }, rect: [rect.r1, rect.c1, rect.r2, rect.c2] };
|
|
1079
|
+
},
|
|
1080
|
+
spills(sheet) {
|
|
1081
|
+
const name = order.find((n) => n.toLowerCase() === sheet.toLowerCase());
|
|
1082
|
+
if (!name)
|
|
1083
|
+
return [];
|
|
1084
|
+
settleAll();
|
|
1085
|
+
const out = [];
|
|
1086
|
+
for (const [anchor, rect] of spills) {
|
|
1087
|
+
const at = parseCellKey(anchor);
|
|
1088
|
+
if ((at.sheet ?? '').toLowerCase() !== name.toLowerCase())
|
|
1089
|
+
continue;
|
|
1090
|
+
out.push({ row: at.row, col: at.col, rect: [rect.r1, rect.c1, rect.r2, rect.c2] });
|
|
1091
|
+
}
|
|
1092
|
+
return out.sort((a, b) => a.row - b.row || a.col - b.col);
|
|
391
1093
|
},
|
|
392
1094
|
rowCount,
|
|
393
1095
|
colCount,
|
|
394
1096
|
serialize() {
|
|
1097
|
+
const list = rawTables.list();
|
|
395
1098
|
return {
|
|
396
1099
|
sheets: order.map((name) => ({ name, cells: sheetCells(name).map((r) => [...r]) })),
|
|
397
1100
|
active,
|
|
398
1101
|
names: names.serialize(),
|
|
1102
|
+
// Absent rather than empty in a workbook with no tables, so a saved
|
|
1103
|
+
// document from before they existed reads the same either way.
|
|
1104
|
+
...(list.length ? { tables: list.map((t) => ({ ...t })) } : {}),
|
|
1105
|
+
// Written only when it is on, so a document saved by a workbook that
|
|
1106
|
+
// never heard of iteration reads back identically.
|
|
1107
|
+
...(iteration.enabled ? { iteration: { ...iteration } } : {}),
|
|
399
1108
|
};
|
|
400
1109
|
},
|
|
401
1110
|
};
|
|
@@ -404,6 +1113,8 @@ export function createWorkbook(initial = [{ name: 'Sheet1', cells: [] }], option
|
|
|
404
1113
|
order.push(sheet.name);
|
|
405
1114
|
}
|
|
406
1115
|
active = order[0] ?? '';
|
|
1116
|
+
loadEngine();
|
|
1117
|
+
settleAll();
|
|
407
1118
|
return workbook;
|
|
408
1119
|
}
|
|
409
1120
|
export { isError };
|