@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/entry.d.ts
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
* the user typed names both a value and how to show it, and the cell keeps
|
|
7
7
|
* both, so `=A1*2` over a `12%` cell is 0.24 rather than `#VALUE!`. The
|
|
8
8
|
* format is the one Excel would pick, with the decimals the user typed.
|
|
9
|
-
*
|
|
9
|
+
* `3/4/2026` is the sheet's `2026-03-04` under a date format and `10:30`
|
|
10
|
+
* is 0.4375 of a day under a time format, for the same reason. Anything
|
|
11
|
+
* else, formulas included, is stored as typed.
|
|
10
12
|
*/
|
|
13
|
+
import { type SheetCulture } from './culture.js';
|
|
11
14
|
export type ParsedEntry = {
|
|
12
15
|
/** The number, as text the workbook stores. */
|
|
13
16
|
value: string;
|
|
14
|
-
/** The Excel format string the entry implies
|
|
15
|
-
|
|
17
|
+
/** The Excel format string the entry implies; none for a bare number
|
|
18
|
+
* read out of an accountant's parentheses, which Excel shows as General. */
|
|
19
|
+
numFmt?: string;
|
|
16
20
|
};
|
|
17
21
|
/**
|
|
18
22
|
* Excel's AutoComplete for a column of text.
|
|
@@ -28,4 +32,4 @@ export type ParsedEntry = {
|
|
|
28
32
|
* `entries` are the distinct text entries of the run, any order.
|
|
29
33
|
*/
|
|
30
34
|
export declare function completeEntry(typed: string, entries: Iterable<string>): string | null;
|
|
31
|
-
export declare function parseEntry(text: string): ParsedEntry | null;
|
|
35
|
+
export declare function parseEntry(text: string, culture?: SheetCulture): ParsedEntry | null;
|
package/dist/sheet/entry.js
CHANGED
|
@@ -1,18 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Excel makes of a typed entry before it is stored.
|
|
3
|
+
*
|
|
4
|
+
* `12%` is the number 0.12 shown as a percentage, `$1,200` is 1200 shown
|
|
5
|
+
* as currency, `1,234.5` is 1234.5 with a thousands separator: the text
|
|
6
|
+
* the user typed names both a value and how to show it, and the cell keeps
|
|
7
|
+
* both, so `=A1*2` over a `12%` cell is 0.24 rather than `#VALUE!`. The
|
|
8
|
+
* format is the one Excel would pick, with the decimals the user typed.
|
|
9
|
+
* `3/4/2026` is the sheet's `2026-03-04` under a date format and `10:30`
|
|
10
|
+
* is 0.4375 of a day under a time format, for the same reason. Anything
|
|
11
|
+
* else, formulas included, is stored as typed.
|
|
12
|
+
*/
|
|
13
|
+
import { INVARIANT_CULTURE, isInvariant, parseNumberInCulture, slashDateParts, SPACE_GROUP_MARKS } from './culture.js';
|
|
1
14
|
const zeros = (n) => (n > 0 ? '.' + '0'.repeat(n) : '');
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
15
|
+
/** For putting a separator that may be `.` or a space inside a pattern. */
|
|
16
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
const PATTERN_CACHE = new Map();
|
|
18
|
+
function patternsFor(culture) {
|
|
19
|
+
const key = `${culture.decimal}\u0000${culture.group}\u0000${culture.dateOrder}`;
|
|
20
|
+
const hit = PATTERN_CACHE.get(key);
|
|
21
|
+
if (hit)
|
|
22
|
+
return hit;
|
|
23
|
+
const d = escapeRe(culture.decimal);
|
|
24
|
+
// A space-like group mark is accepted in any of its shapes, since a
|
|
25
|
+
// narrow no-break space is what fr-FR formats with and a plain space is
|
|
26
|
+
// what anyone types.
|
|
27
|
+
const g = /\s/.test(culture.group) ? '[\\s\\u00a0\\u202f]' : escapeRe(culture.group);
|
|
28
|
+
const grouped = String.raw `(?:\d{1,3}(?:${g}\d{3})+|\d+)(?:${d}\d+)?`;
|
|
29
|
+
const built = {
|
|
30
|
+
culture,
|
|
31
|
+
percent: new RegExp(String.raw `^(-?)(${grouped})\s?%$`),
|
|
32
|
+
currency: new RegExp(String.raw `^(-?)\$\s?(${grouped})$`),
|
|
33
|
+
currencyParen: new RegExp(String.raw `^\(\$\s?(${grouped})\)$`),
|
|
34
|
+
thousands: new RegExp(String.raw `^(-?)(\d{1,3}(?:${g}\d{3})+(?:${d}\d+)?)$`),
|
|
35
|
+
/** (5): a negative the way a statement writes one, read as -5. */
|
|
36
|
+
paren: new RegExp(String.raw `^\((${grouped})\)$`),
|
|
37
|
+
slashDate: /^(\d{1,2})\/(\d{1,2})(?:\/(\d{2}|\d{4}))?$/,
|
|
38
|
+
};
|
|
39
|
+
PATTERN_CACHE.set(key, built);
|
|
40
|
+
return built;
|
|
41
|
+
}
|
|
42
|
+
/** Digits with optional group marks and decimals, as a number. */
|
|
43
|
+
function numberOf(digits, culture) {
|
|
44
|
+
let plain = digits;
|
|
45
|
+
for (const g of [culture.group, ...SPACE_GROUP_MARKS]) {
|
|
46
|
+
if (g !== '' && g !== culture.decimal)
|
|
47
|
+
plain = plain.split(g).join('');
|
|
48
|
+
}
|
|
49
|
+
if (culture.decimal !== '.')
|
|
50
|
+
plain = plain.split(culture.decimal).join('.');
|
|
5
51
|
const n = Number(plain);
|
|
6
52
|
return Number.isFinite(n) ? n : null;
|
|
7
53
|
}
|
|
8
|
-
const decimalsOf = (digits) => digits.split(
|
|
54
|
+
const decimalsOf = (digits, culture) => digits.split(culture.decimal)[1]?.length ?? 0;
|
|
9
55
|
/** Round away the binary noise of a division: 33.3% is 0.333, not 0.33299999. */
|
|
10
56
|
const trim = (n, places) => String(Number(n.toFixed(places)));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
57
|
+
// A fraction typed with a whole part, as Excel takes one: `3 1/2` is 3.5 and
|
|
58
|
+
// `0 1/2` is the way to enter a bare half without `1/2` becoming a date. The
|
|
59
|
+
// whole part is required - that space is what tells it apart from a date.
|
|
60
|
+
const FRACTION = /^(-?)(\d+)\s+(\d+)\/(\d+)$/;
|
|
61
|
+
const CLOCK = /^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AaPp])\.?[Mm]?\.?)?$/;
|
|
62
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
63
|
+
/** English month names to a 1-12 number: `jan`/`january` alike. */
|
|
64
|
+
const MONTH_NUMBER = (() => {
|
|
65
|
+
const full = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
|
|
66
|
+
const out = {};
|
|
67
|
+
full.forEach((name, i) => { out[name] = i + 1; out[name.slice(0, 3)] = i + 1; });
|
|
68
|
+
out.sept = 9;
|
|
69
|
+
return out;
|
|
70
|
+
})();
|
|
71
|
+
// A day and a month name in either order, with an optional year:
|
|
72
|
+
// 4-Mar-2026, 4 Mar 26, Mar 4, March 4, 2026. The separator is a space,
|
|
73
|
+
// a hyphen or (for the day-first form) a slash; a comma may follow the day.
|
|
74
|
+
const DAY_MONTH = /^(\d{1,2})[\s\-/]+([A-Za-z]{3,9})(?:,?[\s\-/]+(\d{2}|\d{4}))?$/;
|
|
75
|
+
const MONTH_DAY = /^([A-Za-z]{3,9})[\s\-/]+(\d{1,2})(?:,?[\s\-/]+(\d{2}|\d{4}))?$/;
|
|
76
|
+
const yearOf = (raw) => raw === undefined ? new Date().getFullYear() : raw.length === 2 ? (Number(raw) < 30 ? 2000 : 1900) + Number(raw) : Number(raw);
|
|
16
77
|
/**
|
|
17
78
|
* Excel's AutoComplete for a column of text.
|
|
18
79
|
*
|
|
@@ -40,37 +101,126 @@ export function completeEntry(typed, entries) {
|
|
|
40
101
|
}
|
|
41
102
|
return found;
|
|
42
103
|
}
|
|
43
|
-
export function parseEntry(text) {
|
|
104
|
+
export function parseEntry(text, culture = INVARIANT_CULTURE) {
|
|
44
105
|
const t = text.trim();
|
|
45
|
-
|
|
106
|
+
// An apostrophe says the rest is text, so nothing is read out of it and no
|
|
107
|
+
// format is implied: '12% is the text 12%, not twelve percent.
|
|
108
|
+
if (t === '' || t.startsWith('=') || t.startsWith("'"))
|
|
46
109
|
return null;
|
|
47
|
-
|
|
110
|
+
const p = patternsFor(culture);
|
|
111
|
+
let m = p.percent.exec(t);
|
|
48
112
|
if (m) {
|
|
49
|
-
const n = numberOf(m[2]);
|
|
113
|
+
const n = numberOf(m[2], culture);
|
|
50
114
|
if (n === null)
|
|
51
115
|
return null;
|
|
52
|
-
const places = decimalsOf(m[2]);
|
|
116
|
+
const places = decimalsOf(m[2], culture);
|
|
53
117
|
return { value: trim((m[1] ? -n : n) / 100, places + 2), numFmt: `0${zeros(places)}%` };
|
|
54
118
|
}
|
|
55
|
-
m =
|
|
56
|
-
const paren = m ? null :
|
|
119
|
+
m = p.currency.exec(t);
|
|
120
|
+
const paren = m ? null : p.currencyParen.exec(t);
|
|
57
121
|
if (m || paren) {
|
|
58
122
|
const digits = (m ? m[2] : paren[1]);
|
|
59
|
-
const n = numberOf(digits);
|
|
123
|
+
const n = numberOf(digits, culture);
|
|
60
124
|
if (n === null)
|
|
61
125
|
return null;
|
|
62
126
|
const negative = m ? m[1] === '-' : true;
|
|
63
|
-
const places = decimalsOf(digits) > 0 ? 2 : 0;
|
|
127
|
+
const places = decimalsOf(digits, culture) > 0 ? 2 : 0;
|
|
64
128
|
const body = `$#,##0${zeros(places)}`;
|
|
65
129
|
return { value: String(negative ? -n : n), numFmt: `${body};(${body})` };
|
|
66
130
|
}
|
|
67
|
-
m =
|
|
131
|
+
m = p.thousands.exec(t);
|
|
68
132
|
if (m) {
|
|
69
|
-
const n = numberOf(m[2]);
|
|
133
|
+
const n = numberOf(m[2], culture);
|
|
70
134
|
if (n === null)
|
|
71
135
|
return null;
|
|
72
|
-
const places = decimalsOf(m[2]);
|
|
136
|
+
const places = decimalsOf(m[2], culture);
|
|
73
137
|
return { value: String(m[1] ? -n : n), numFmt: `#,##0${zeros(places)}` };
|
|
74
138
|
}
|
|
139
|
+
m = p.paren.exec(t);
|
|
140
|
+
if (m) {
|
|
141
|
+
const n = numberOf(m[1], culture);
|
|
142
|
+
return n === null ? null : { value: String(-n) };
|
|
143
|
+
}
|
|
144
|
+
// A plain number written the culture's way, with no format implied.
|
|
145
|
+
//
|
|
146
|
+
// The invariant culture never reaches here with anything to do, since
|
|
147
|
+
// `1.5` is already what the document stores, and returning null lets the
|
|
148
|
+
// caller keep the text exactly as typed. A culture that spells it `1,5`
|
|
149
|
+
// does have something to do: the stored value has to be turned back into
|
|
150
|
+
// the invariant spelling, or the engine reads a comma it cannot parse.
|
|
151
|
+
if (!isInvariant(culture)) {
|
|
152
|
+
const n = parseNumberInCulture(t, culture);
|
|
153
|
+
if (n !== null)
|
|
154
|
+
return { value: String(n) };
|
|
155
|
+
}
|
|
156
|
+
// A mixed fraction: 3 1/2 is 3.5, shown back as the fraction under Excel's
|
|
157
|
+
// `# ?/?` format, so it adds up and =A1*2 over it is 7 rather than #VALUE!.
|
|
158
|
+
// The whole part is required, which is what keeps 1/2 a date and 0 1/2 a
|
|
159
|
+
// half. The format widens to the digits typed, so 3 11/16 keeps two.
|
|
160
|
+
m = FRACTION.exec(t);
|
|
161
|
+
if (m) {
|
|
162
|
+
const den = Number(m[4]);
|
|
163
|
+
if (den === 0)
|
|
164
|
+
return null;
|
|
165
|
+
const value = (m[1] ? -1 : 1) * (Number(m[2]) + Number(m[3]) / den);
|
|
166
|
+
return { value: trim(value, 10), numFmt: `# ${'?'.repeat(m[3].length)}/${'?'.repeat(m[4].length)}` };
|
|
167
|
+
}
|
|
168
|
+
// A slash date, in the order the culture writes one: 3/4/2026 is the
|
|
169
|
+
// fourth of March where the locale puts the month first and the third of
|
|
170
|
+
// April where it puts the day first. A two-digit year 00-29 is this
|
|
171
|
+
// century, 30-99 the last. Stored as the sheet's `yyyy-mm-dd` text,
|
|
172
|
+
// which is what the date functions read and what the file writer turns
|
|
173
|
+
// into a serial, under a date format so the ribbon says Date and a
|
|
174
|
+
// formula over it inherits one.
|
|
175
|
+
m = p.slashDate.exec(t);
|
|
176
|
+
if (m) {
|
|
177
|
+
const parts = slashDateParts(Number(m[1]), Number(m[2]), culture);
|
|
178
|
+
if (!parts)
|
|
179
|
+
return null;
|
|
180
|
+
const y = yearOf(m[3]);
|
|
181
|
+
const { month, day } = parts;
|
|
182
|
+
if (day <= new Date(Date.UTC(y, month, 0)).getUTCDate()) {
|
|
183
|
+
return { value: `${y}-${pad2(month)}-${pad2(day)}`, numFmt: 'yyyy-mm-dd' };
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
// A date written with the month spelled out, as Excel reads one:
|
|
188
|
+
// 4-Mar-2026, 4 Mar 26, March 4, 2026, Mar 4. English month names only,
|
|
189
|
+
// the language the shell speaks; stored the same yyyy-mm-dd as a slash
|
|
190
|
+
// date. Either order, so both 4-Mar and Mar-4 are the fourth of March.
|
|
191
|
+
const monthDate = (dayText, monthName, yearText) => {
|
|
192
|
+
const month = MONTH_NUMBER[monthName.toLowerCase()];
|
|
193
|
+
if (month === undefined)
|
|
194
|
+
return null;
|
|
195
|
+
const day = Number(dayText);
|
|
196
|
+
const y = yearOf(yearText);
|
|
197
|
+
if (day < 1 || day > new Date(Date.UTC(y, month, 0)).getUTCDate())
|
|
198
|
+
return null;
|
|
199
|
+
return { value: `${y}-${pad2(month)}-${pad2(day)}`, numFmt: 'd-mmm-yyyy' };
|
|
200
|
+
};
|
|
201
|
+
m = DAY_MONTH.exec(t);
|
|
202
|
+
if (m)
|
|
203
|
+
return monthDate(m[1], m[2], m[3]);
|
|
204
|
+
m = MONTH_DAY.exec(t);
|
|
205
|
+
if (m)
|
|
206
|
+
return monthDate(m[2], m[1], m[3]);
|
|
207
|
+
// A clock time is the fraction of the day it is, 10:30 being 0.4375, under
|
|
208
|
+
// the time format that shows it back, so times add up and =A1*2 over one
|
|
209
|
+
// is 21:00 rather than #VALUE!. 10:30 PM is the same fraction past noon.
|
|
210
|
+
m = CLOCK.exec(t);
|
|
211
|
+
if (m) {
|
|
212
|
+
let hours = Number(m[1]);
|
|
213
|
+
const minutes = Number(m[2]), seconds = Number(m[3] ?? 0);
|
|
214
|
+
const meridiem = m[4]?.toLowerCase();
|
|
215
|
+
if (meridiem) {
|
|
216
|
+
if (hours < 1 || hours > 12)
|
|
217
|
+
return null;
|
|
218
|
+
hours = (hours % 12) + (meridiem === 'p' ? 12 : 0);
|
|
219
|
+
}
|
|
220
|
+
if (hours > 23 || minutes > 59 || seconds > 59)
|
|
221
|
+
return null;
|
|
222
|
+
const fraction = (hours * 3600 + minutes * 60 + seconds) / 86400;
|
|
223
|
+
return { value: String(Number(fraction.toFixed(10))), numFmt: `h:mm${m[3] ? ':ss' : ''}${meridiem ? ' AM/PM' : ''}` };
|
|
224
|
+
}
|
|
75
225
|
return null;
|
|
76
226
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel's Error Checking: the sheet read once, the cells worth a second
|
|
3
|
+
* look listed in reading order.
|
|
4
|
+
*
|
|
5
|
+
* Two findings, both Excel's own. A cell whose formula works out to an
|
|
6
|
+
* error is the obvious one, and the dialog says what the error means rather
|
|
7
|
+
* than leaving `#VALUE!` to be looked up. A formula that breaks the pattern
|
|
8
|
+
* of the ones above and below it is the useful one: it is how a total that
|
|
9
|
+
* was dragged one row short, or a cell overtyped with a constant, is found
|
|
10
|
+
* in a column of two hundred that all look alike.
|
|
11
|
+
*
|
|
12
|
+
* Kept out of the shell so it can be tested on a workbook and run by
|
|
13
|
+
* anything else that wants the list: a save-time check, a report, an agent.
|
|
14
|
+
*/
|
|
15
|
+
import { type CellValue, type SheetError } from './ast.js';
|
|
16
|
+
export type ErrorFindingKind = 'error' | 'inconsistent';
|
|
17
|
+
export type ErrorFinding = {
|
|
18
|
+
sheet: string;
|
|
19
|
+
row: number;
|
|
20
|
+
col: number;
|
|
21
|
+
kind: ErrorFindingKind;
|
|
22
|
+
/** The cell's text, which is the formula for both kinds. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** The error in the cell, for `kind: 'error'`. */
|
|
25
|
+
error?: SheetError;
|
|
26
|
+
/** What the column's own pattern says the formula would be, for
|
|
27
|
+
* `kind: 'inconsistent'`. */
|
|
28
|
+
expected?: string;
|
|
29
|
+
};
|
|
30
|
+
/** What each error means, in a sentence, the way Excel's dialog explains it. */
|
|
31
|
+
export declare const ERROR_MEANINGS: Record<SheetError, string>;
|
|
32
|
+
export type SheetReader = {
|
|
33
|
+
rowCount(): number;
|
|
34
|
+
colCount(): number;
|
|
35
|
+
getRaw(row: number, col: number): string;
|
|
36
|
+
getValue(row: number, col: number): CellValue;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Every cell worth a look on one sheet, in reading order.
|
|
40
|
+
*
|
|
41
|
+
* `inconsistent` is deliberately quiet: a formula is only called the odd one
|
|
42
|
+
* out when the cells above and below it both hold formulas, those two agree
|
|
43
|
+
* with each other once translated, and this one does not. That is the shape
|
|
44
|
+
* of a column filled down and then broken in the middle, and it is the only
|
|
45
|
+
* shape reported, because an auditing tool that cries wolf is turned off.
|
|
46
|
+
*/
|
|
47
|
+
export declare function checkSheet(sheet: string, read: SheetReader): ErrorFinding[];
|
|
48
|
+
/** One line saying what is wrong with a cell, for a dialog or a log. */
|
|
49
|
+
export declare function describeFinding(finding: ErrorFinding): string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel's Error Checking: the sheet read once, the cells worth a second
|
|
3
|
+
* look listed in reading order.
|
|
4
|
+
*
|
|
5
|
+
* Two findings, both Excel's own. A cell whose formula works out to an
|
|
6
|
+
* error is the obvious one, and the dialog says what the error means rather
|
|
7
|
+
* than leaving `#VALUE!` to be looked up. A formula that breaks the pattern
|
|
8
|
+
* of the ones above and below it is the useful one: it is how a total that
|
|
9
|
+
* was dragged one row short, or a cell overtyped with a constant, is found
|
|
10
|
+
* in a column of two hundred that all look alike.
|
|
11
|
+
*
|
|
12
|
+
* Kept out of the shell so it can be tested on a workbook and run by
|
|
13
|
+
* anything else that wants the list: a save-time check, a report, an agent.
|
|
14
|
+
*/
|
|
15
|
+
import { isError } from './ast.js';
|
|
16
|
+
import { translateFormula } from './refs.js';
|
|
17
|
+
/** What each error means, in a sentence, the way Excel's dialog explains it. */
|
|
18
|
+
export const ERROR_MEANINGS = {
|
|
19
|
+
'#REF!': 'The formula points at a cell that is no longer there, usually because a row, a column or a sheet was deleted.',
|
|
20
|
+
'#CYCLE!': 'The formula depends on its own cell, directly or through others. Turn on iterative calculation if the loop is meant.',
|
|
21
|
+
'#DIV/0!': 'Something is divided by zero, or by a blank cell, which counts as zero.',
|
|
22
|
+
'#VALUE!': 'An argument is the wrong kind: text where a number is needed, most often.',
|
|
23
|
+
'#NAME?': 'A name in the formula is not known: a misspelled function, a defined name that does not exist, or text left without quotes.',
|
|
24
|
+
'#NUM!': 'The numbers are out of range for what was asked: a root of a negative, or a value too large to hold.',
|
|
25
|
+
'#N/A': 'A lookup found nothing. This is the answer, not a fault, when the value really is not there.',
|
|
26
|
+
'#PARSE!': 'The formula could not be read. A bracket or a quote is usually missing.',
|
|
27
|
+
'#SPILL!': 'The answer is a block, and something is in the way of it. Clear the cells the outline covers.',
|
|
28
|
+
'#CALC!': 'The answer is a block the sheet cannot hold: an empty array, or an array inside an array.',
|
|
29
|
+
'#NULL!': 'Two ranges were asked to overlap and do not. Excel writes it for an intersection with nothing in it.',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Every cell worth a look on one sheet, in reading order.
|
|
33
|
+
*
|
|
34
|
+
* `inconsistent` is deliberately quiet: a formula is only called the odd one
|
|
35
|
+
* out when the cells above and below it both hold formulas, those two agree
|
|
36
|
+
* with each other once translated, and this one does not. That is the shape
|
|
37
|
+
* of a column filled down and then broken in the middle, and it is the only
|
|
38
|
+
* shape reported, because an auditing tool that cries wolf is turned off.
|
|
39
|
+
*/
|
|
40
|
+
export function checkSheet(sheet, read) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const rows = read.rowCount();
|
|
43
|
+
const cols = read.colCount();
|
|
44
|
+
for (let row = 0; row < rows; row += 1) {
|
|
45
|
+
for (let col = 0; col < cols; col += 1) {
|
|
46
|
+
const text = read.getRaw(row, col).trim();
|
|
47
|
+
if (!text.startsWith('='))
|
|
48
|
+
continue;
|
|
49
|
+
const value = read.getValue(row, col);
|
|
50
|
+
if (isError(value)) {
|
|
51
|
+
out.push({ sheet, row, col, kind: 'error', text, error: value.error });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const expected = inconsistentWith(read, row, col, text);
|
|
55
|
+
if (expected !== null)
|
|
56
|
+
out.push({ sheet, row, col, kind: 'inconsistent', text, expected });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** The formula this cell would hold if it followed its neighbours, or null
|
|
62
|
+
* when it already does or there is no pattern to follow. */
|
|
63
|
+
function inconsistentWith(read, row, col, text) {
|
|
64
|
+
if (row < 1)
|
|
65
|
+
return null;
|
|
66
|
+
const above = read.getRaw(row - 1, col).trim();
|
|
67
|
+
const below = read.getRaw(row + 1, col).trim();
|
|
68
|
+
if (!above.startsWith('=') || !below.startsWith('='))
|
|
69
|
+
return null;
|
|
70
|
+
const expected = translateFormula(above, 1, 0);
|
|
71
|
+
if (typeof expected !== 'string')
|
|
72
|
+
return null;
|
|
73
|
+
// The neighbours have to agree with each other, or there is no pattern.
|
|
74
|
+
if (translateFormula(above, 2, 0) !== below)
|
|
75
|
+
return null;
|
|
76
|
+
return expected === text ? null : expected;
|
|
77
|
+
}
|
|
78
|
+
/** One line saying what is wrong with a cell, for a dialog or a log. */
|
|
79
|
+
export function describeFinding(finding) {
|
|
80
|
+
if (finding.kind === 'error')
|
|
81
|
+
return ERROR_MEANINGS[finding.error ?? '#VALUE!'];
|
|
82
|
+
return `This formula is not the one the cells above and below it use. Theirs would read ${finding.expected} here.`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel's Evaluate Formula, as a list of steps.
|
|
3
|
+
*
|
|
4
|
+
* The dialog shows a formula with one part underlined, and every click
|
|
5
|
+
* replaces that part with what it is worth, until the whole thing is the
|
|
6
|
+
* cell's answer. It is the fastest way to find which half of a long formula
|
|
7
|
+
* is the wrong half, which is why it is the auditing tool people reach for
|
|
8
|
+
* after the tracing arrows.
|
|
9
|
+
*
|
|
10
|
+
* The steps are worked out here rather than in the dialog, so they can be
|
|
11
|
+
* tested without a browser and reused by anything that wants to explain a
|
|
12
|
+
* formula: a tooltip, a report, an agent.
|
|
13
|
+
*
|
|
14
|
+
* How a part is evaluated: it is printed back to a formula and handed to the
|
|
15
|
+
* caller, which passes it to the workbook. That keeps this module out of the
|
|
16
|
+
* evaluator entirely, so names, tables, other sheets and custom functions all
|
|
17
|
+
* mean exactly what they mean in the cell, with no second implementation to
|
|
18
|
+
* disagree with the first.
|
|
19
|
+
*/
|
|
20
|
+
import { type CellValue, type Node } from './ast.js';
|
|
21
|
+
/**
|
|
22
|
+
* A formula printed back from its tree, without the leading `=`.
|
|
23
|
+
*
|
|
24
|
+
* The printer a fill-down already uses, so a step reads the way the sheet
|
|
25
|
+
* itself would write the formula, brackets and all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function printNode(node: Node): string;
|
|
28
|
+
/** What a value looks like inside a formula being stepped through. */
|
|
29
|
+
export declare function printValue(value: CellValue): string;
|
|
30
|
+
export type EvaluationStep = {
|
|
31
|
+
/** The formula as it stands before this step, `=` included. */
|
|
32
|
+
formula: string;
|
|
33
|
+
/** The part about to be worked out, as offsets into `formula`. */
|
|
34
|
+
from: number;
|
|
35
|
+
to: number;
|
|
36
|
+
/** That part on its own, as it reads at this point in the walk. */
|
|
37
|
+
expression: string;
|
|
38
|
+
/** What it is worth. */
|
|
39
|
+
value: CellValue;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Walk a formula the way Excel's Evaluate Formula does.
|
|
43
|
+
*
|
|
44
|
+
* `evaluate` is given one sub-formula at a time, `=` included, and answers
|
|
45
|
+
* with its value; the workbook's `evaluateText` is exactly that. The last
|
|
46
|
+
* step's value is the cell's own.
|
|
47
|
+
*/
|
|
48
|
+
export declare function evaluationSteps(ast: Node, evaluate: (formula: string) => CellValue, limit?: number): EvaluationStep[];
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel's Evaluate Formula, as a list of steps.
|
|
3
|
+
*
|
|
4
|
+
* The dialog shows a formula with one part underlined, and every click
|
|
5
|
+
* replaces that part with what it is worth, until the whole thing is the
|
|
6
|
+
* cell's answer. It is the fastest way to find which half of a long formula
|
|
7
|
+
* is the wrong half, which is why it is the auditing tool people reach for
|
|
8
|
+
* after the tracing arrows.
|
|
9
|
+
*
|
|
10
|
+
* The steps are worked out here rather than in the dialog, so they can be
|
|
11
|
+
* tested without a browser and reused by anything that wants to explain a
|
|
12
|
+
* formula: a tooltip, a report, an agent.
|
|
13
|
+
*
|
|
14
|
+
* How a part is evaluated: it is printed back to a formula and handed to the
|
|
15
|
+
* caller, which passes it to the workbook. That keeps this module out of the
|
|
16
|
+
* evaluator entirely, so names, tables, other sheets and custom functions all
|
|
17
|
+
* mean exactly what they mean in the cell, with no second implementation to
|
|
18
|
+
* disagree with the first.
|
|
19
|
+
*/
|
|
20
|
+
import { isError, PRECEDENCE } from './ast.js';
|
|
21
|
+
import { formatFormula } from './refs.js';
|
|
22
|
+
/** A reference that stands for a block rather than one value: printed, never
|
|
23
|
+
* replaced, because `A1:A9` on its own is not a number. */
|
|
24
|
+
function isBlock(node) {
|
|
25
|
+
return node.k === 'range' || node.k === 'name' || node.k === 'table';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A formula printed back from its tree, without the leading `=`.
|
|
29
|
+
*
|
|
30
|
+
* The printer a fill-down already uses, so a step reads the way the sheet
|
|
31
|
+
* itself would write the formula, brackets and all.
|
|
32
|
+
*/
|
|
33
|
+
export function printNode(node) {
|
|
34
|
+
return formatFormula(node).slice(1);
|
|
35
|
+
}
|
|
36
|
+
/** What a value looks like inside a formula being stepped through. */
|
|
37
|
+
export function printValue(value) {
|
|
38
|
+
if (isError(value))
|
|
39
|
+
return value.error;
|
|
40
|
+
if (typeof value === 'string')
|
|
41
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
42
|
+
if (typeof value === 'boolean')
|
|
43
|
+
return value ? 'TRUE' : 'FALSE';
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
function toWork(node) {
|
|
47
|
+
switch (node.k) {
|
|
48
|
+
case 'unary': return { kind: 'node', node, children: [toWork(node.arg)] };
|
|
49
|
+
case 'binary': return { kind: 'node', node, children: [toWork(node.left), toWork(node.right)] };
|
|
50
|
+
case 'fn': return { kind: 'node', node, children: node.args.map(toWork) };
|
|
51
|
+
default: return { kind: 'node', node, children: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The formula a piece of the working tree stands for. */
|
|
55
|
+
function originOf(work) {
|
|
56
|
+
if (work.kind === 'value')
|
|
57
|
+
return work.origin;
|
|
58
|
+
const node = work.node;
|
|
59
|
+
switch (node.k) {
|
|
60
|
+
case 'unary': return { ...node, arg: originOf(work.children[0]) };
|
|
61
|
+
case 'binary': return { ...node, left: originOf(work.children[0]), right: originOf(work.children[1]) };
|
|
62
|
+
case 'fn': return { ...node, args: work.children.map(originOf) };
|
|
63
|
+
default: return node;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Print the working tree, and record where each piece landed. */
|
|
67
|
+
function printWork(work, spans, at = 0) {
|
|
68
|
+
const record = (text) => {
|
|
69
|
+
spans.set(work, [at, at + text.length]);
|
|
70
|
+
return text;
|
|
71
|
+
};
|
|
72
|
+
if (work.kind === 'value')
|
|
73
|
+
return record(printValue(work.value));
|
|
74
|
+
const node = work.node;
|
|
75
|
+
if (work.children.length === 0)
|
|
76
|
+
return record(printNode(node));
|
|
77
|
+
if (node.k === 'unary') {
|
|
78
|
+
const arg = work.children[0];
|
|
79
|
+
if (node.op === '%') {
|
|
80
|
+
const inner = printWorkWrapped(arg, spans, at, 6);
|
|
81
|
+
return record(`${inner}%`);
|
|
82
|
+
}
|
|
83
|
+
const inner = printWorkWrapped(arg, spans, at + node.op.length, 5);
|
|
84
|
+
return record(`${node.op}${inner}`);
|
|
85
|
+
}
|
|
86
|
+
if (node.k === 'binary') {
|
|
87
|
+
// The shared printer's rule: at equal precedence the right operand takes
|
|
88
|
+
// the bracket, and `^` is the mirror of that because it binds rightwards.
|
|
89
|
+
const p = PRECEDENCE[node.op];
|
|
90
|
+
const rightAssociative = node.op === '^';
|
|
91
|
+
const left = printWorkWrapped(work.children[0], spans, at, rightAssociative ? p + 1 : p);
|
|
92
|
+
const right = printWorkWrapped(work.children[1], spans, at + left.length + node.op.length, rightAssociative ? p : p + 1);
|
|
93
|
+
return record(`${left}${node.op}${right}`);
|
|
94
|
+
}
|
|
95
|
+
// A function call, including the `(` node a lambda's arguments build.
|
|
96
|
+
const lambda = node.k === 'fn' && node.name === '(';
|
|
97
|
+
const head = lambda ? printWork(work.children[0], spans, at) : `${node.name}`;
|
|
98
|
+
const args = lambda ? work.children.slice(1) : work.children;
|
|
99
|
+
let out = `${head}(`;
|
|
100
|
+
args.forEach((child, i) => {
|
|
101
|
+
if (i > 0)
|
|
102
|
+
out += ',';
|
|
103
|
+
out += printWork(child, spans, at + out.length);
|
|
104
|
+
});
|
|
105
|
+
return record(`${out})`);
|
|
106
|
+
}
|
|
107
|
+
function printWorkWrapped(work, spans, at, need) {
|
|
108
|
+
const bare = work.kind === 'node'
|
|
109
|
+
&& ((work.node.k === 'binary' && PRECEDENCE[work.node.op] < need)
|
|
110
|
+
|| (work.node.k === 'unary' && work.node.op !== '%' && need > 5));
|
|
111
|
+
const inner = printWork(work, spans, at + (bare ? 1 : 0));
|
|
112
|
+
return bare ? `(${inner})` : inner;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The next piece to evaluate: the leftmost, deepest one whose own parts are
|
|
116
|
+
* already values. A block reference (`A1:A9`, a name, a table column) is
|
|
117
|
+
* left where it is, and so is a literal, since neither is a step.
|
|
118
|
+
*/
|
|
119
|
+
function nextPiece(work) {
|
|
120
|
+
if (work.kind === 'value')
|
|
121
|
+
return null;
|
|
122
|
+
const node = work.node;
|
|
123
|
+
// IF picks a branch. Once the condition is a value the whole call is one
|
|
124
|
+
// step, so the branch that is not taken is never evaluated and never
|
|
125
|
+
// reports an error the cell does not have.
|
|
126
|
+
const isIf = node.k === 'fn' && node.name === 'IF';
|
|
127
|
+
if (isIf) {
|
|
128
|
+
const condition = work.children[0];
|
|
129
|
+
if (condition && condition.kind !== 'value') {
|
|
130
|
+
const inner = nextPiece(condition);
|
|
131
|
+
return inner ?? condition;
|
|
132
|
+
}
|
|
133
|
+
return work;
|
|
134
|
+
}
|
|
135
|
+
for (const child of work.children) {
|
|
136
|
+
const inner = nextPiece(child);
|
|
137
|
+
if (inner)
|
|
138
|
+
return inner;
|
|
139
|
+
}
|
|
140
|
+
if (work.children.length === 0) {
|
|
141
|
+
// A leaf: a single cell is a step, a literal and a block are not.
|
|
142
|
+
return node.k === 'ref' ? work : null;
|
|
143
|
+
}
|
|
144
|
+
// Every child is a value or a block, so this call is ready.
|
|
145
|
+
return work.children.every((c) => c.kind === 'value' || (c.kind === 'node' && (isBlock(c.node) || c.node.k === 'empty' || c.children.length === 0)))
|
|
146
|
+
? work
|
|
147
|
+
: null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Walk a formula the way Excel's Evaluate Formula does.
|
|
151
|
+
*
|
|
152
|
+
* `evaluate` is given one sub-formula at a time, `=` included, and answers
|
|
153
|
+
* with its value; the workbook's `evaluateText` is exactly that. The last
|
|
154
|
+
* step's value is the cell's own.
|
|
155
|
+
*/
|
|
156
|
+
export function evaluationSteps(ast, evaluate, limit = 100) {
|
|
157
|
+
const steps = [];
|
|
158
|
+
let root = toWork(ast);
|
|
159
|
+
for (let i = 0; i < limit; i += 1) {
|
|
160
|
+
const spans = new Map();
|
|
161
|
+
const formula = printWork(root, spans);
|
|
162
|
+
const piece = nextPiece(root);
|
|
163
|
+
if (!piece)
|
|
164
|
+
break;
|
|
165
|
+
const span = spans.get(piece);
|
|
166
|
+
if (!span)
|
|
167
|
+
break;
|
|
168
|
+
// Evaluated as the formula it really is, with every reference still in
|
|
169
|
+
// place, and shown as it now reads, with the parts already worked out
|
|
170
|
+
// standing as their values. The two agree because a replacement only
|
|
171
|
+
// ever stands for what it was worth.
|
|
172
|
+
const origin = originOf(piece);
|
|
173
|
+
const value = evaluate(`=${printNode(origin)}`);
|
|
174
|
+
steps.push({
|
|
175
|
+
formula: `=${formula}`,
|
|
176
|
+
from: span[0] + 1,
|
|
177
|
+
to: span[1] + 1,
|
|
178
|
+
expression: formula.slice(span[0], span[1]),
|
|
179
|
+
value,
|
|
180
|
+
});
|
|
181
|
+
const replaced = { kind: 'value', value, origin };
|
|
182
|
+
root = replace(root, piece, replaced);
|
|
183
|
+
if (root === replaced)
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
return steps;
|
|
187
|
+
}
|
|
188
|
+
/** The tree with one piece swapped out. */
|
|
189
|
+
function replace(work, target, next) {
|
|
190
|
+
if (work === target)
|
|
191
|
+
return next;
|
|
192
|
+
if (work.kind === 'value')
|
|
193
|
+
return work;
|
|
194
|
+
const children = work.children.map((c) => replace(c, target, next));
|
|
195
|
+
return children.some((c, i) => c !== work.children[i]) ? { ...work, children } : work;
|
|
196
|
+
}
|