@svgrid/enterprise 3.0.2 → 3.1.0
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-BSjv6Qec.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
|
@@ -69,8 +69,20 @@ function splitSections(pattern) {
|
|
|
69
69
|
out.push(current);
|
|
70
70
|
return out;
|
|
71
71
|
}
|
|
72
|
+
const CONDITION = /^(<=|>=|<>|<|>|=)\s*(-?\d+(?:\.\d+)?)$/;
|
|
73
|
+
function passes(condition, n) {
|
|
74
|
+
switch (condition.op) {
|
|
75
|
+
case '<': return n < condition.value;
|
|
76
|
+
case '<=': return n <= condition.value;
|
|
77
|
+
case '>': return n > condition.value;
|
|
78
|
+
case '>=': return n >= condition.value;
|
|
79
|
+
case '=': return n === condition.value;
|
|
80
|
+
case '<>': return n !== condition.value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
72
83
|
function parseSection(raw) {
|
|
73
84
|
let color;
|
|
85
|
+
let condition;
|
|
74
86
|
let body = '';
|
|
75
87
|
// Walk rather than regex-replace: a pattern like `"[" @ "]"` has brackets
|
|
76
88
|
// INSIDE quoted literals, and a global /\[[^\]]*\]/ happily eats the text
|
|
@@ -96,19 +108,37 @@ function parseSection(raw) {
|
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
98
110
|
const inner = raw.slice(i + 1, end).trim().toUpperCase();
|
|
111
|
+
// An elapsed-time token is part of the pattern rather than a
|
|
112
|
+
// directive about it, so it stays in the body for the renderer.
|
|
113
|
+
if (ELAPSED_TOKEN.test(raw.slice(i))) {
|
|
114
|
+
body += raw.slice(i, end + 1);
|
|
115
|
+
i = end;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
99
118
|
const named = COLORS[inner];
|
|
100
119
|
if (named)
|
|
101
120
|
color = named;
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
const cond = CONDITION.exec(inner);
|
|
122
|
+
if (cond)
|
|
123
|
+
condition = { op: cond[1], value: Number(cond[2]) };
|
|
124
|
+
// Anything else ([$-409] locale tags and the like) is dropped rather
|
|
125
|
+
// than printed.
|
|
104
126
|
i = end;
|
|
105
127
|
continue;
|
|
106
128
|
}
|
|
107
129
|
body += ch;
|
|
108
130
|
}
|
|
109
|
-
return { raw, color, body };
|
|
131
|
+
return condition ? { raw, color, condition, body } : { raw, color, body };
|
|
110
132
|
}
|
|
111
|
-
const DATE_TOKEN = /^(yyyy|yy|mmmm|mmm|mm|m|dddd|ddd|dd|d|hh|h|ss|s|AM\/PM|am\/pm|A\/P)/;
|
|
133
|
+
const DATE_TOKEN = /^(yyyy|yy|mmmmm|mmmm|mmm|mm|m|dddd|ddd|dd|d|hh|h|ss|s|AM\/PM|am\/pm|A\/P|a\/p)/;
|
|
134
|
+
/**
|
|
135
|
+
* Excel's elapsed-time tokens: the hours, minutes or seconds a duration
|
|
136
|
+
* holds in TOTAL rather than the clock's reading of it, which is what a
|
|
137
|
+
* timesheet needs. `[h]:mm` over 1.5 days is 36:00, not 12:00. Only h, m
|
|
138
|
+
* and s go in these brackets, so a colour (`[Red]`) or a condition
|
|
139
|
+
* (`[>100]`) never matches.
|
|
140
|
+
*/
|
|
141
|
+
const ELAPSED_TOKEN = /^\[(hh?|mm?|ss?)\]/;
|
|
112
142
|
/** Does this section describe a date? Scans outside quotes and escapes. */
|
|
113
143
|
function looksLikeDate(body) {
|
|
114
144
|
for (let i = 0; i < body.length; i += 1) {
|
|
@@ -122,6 +152,15 @@ function looksLikeDate(body) {
|
|
|
122
152
|
i = end < 0 ? body.length : end;
|
|
123
153
|
continue;
|
|
124
154
|
}
|
|
155
|
+
if (ELAPSED_TOKEN.test(body.slice(i)))
|
|
156
|
+
return true;
|
|
157
|
+
if (ch === '[') {
|
|
158
|
+
// Any other bracket is a colour or a condition, and says nothing
|
|
159
|
+
// about whether this is a date.
|
|
160
|
+
const end = body.indexOf(']', i + 1);
|
|
161
|
+
i = end < 0 ? body.length : end;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
125
164
|
if (DATE_TOKEN.test(body.slice(i))) {
|
|
126
165
|
// `m` alone is ambiguous with the thousands/number grammar only in
|
|
127
166
|
// theory: a numeric pattern never contains a bare letter outside quotes.
|
|
@@ -150,9 +189,74 @@ function toDate(value) {
|
|
|
150
189
|
}
|
|
151
190
|
return null;
|
|
152
191
|
}
|
|
153
|
-
|
|
192
|
+
/** The meridiem token a pattern carries, which puts `h` on a 12-hour clock. */
|
|
193
|
+
function meridiemOf(body) {
|
|
194
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
195
|
+
if (body[i] === '\\') {
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (body[i] === '"') {
|
|
200
|
+
const end = body.indexOf('"', i + 1);
|
|
201
|
+
i = end < 0 ? body.length : end;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const m = /^(AM\/PM|am\/pm|A\/P|a\/p)/.exec(body.slice(i));
|
|
205
|
+
if (m)
|
|
206
|
+
return m[1];
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The finest unit a time pattern shows, in milliseconds, or null for a
|
|
212
|
+
* pattern that shows no time. Excel ROUNDS to it: 23:59:40 under `hh:mm`
|
|
213
|
+
* reads 00:00, and the date rolls with it where the pattern shows one.
|
|
214
|
+
*/
|
|
215
|
+
function finestUnit(body) {
|
|
216
|
+
let unit = null;
|
|
217
|
+
let sawHour = false;
|
|
218
|
+
const finer = (ms) => { unit = unit === null ? ms : Math.min(unit, ms); };
|
|
219
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
220
|
+
if (body[i] === '\\') {
|
|
221
|
+
i += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (body[i] === '"') {
|
|
225
|
+
const end = body.indexOf('"', i + 1);
|
|
226
|
+
i = end < 0 ? body.length : end;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const fraction = /^\.(0+)/.exec(body.slice(i));
|
|
230
|
+
if (fraction && unit !== null) {
|
|
231
|
+
finer(Math.pow(10, 3 - fraction[1].length));
|
|
232
|
+
i += fraction[0].length - 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const m = DATE_TOKEN.exec(body.slice(i));
|
|
236
|
+
if (!m)
|
|
237
|
+
continue;
|
|
238
|
+
const token = m[1];
|
|
239
|
+
i += token.length - 1;
|
|
240
|
+
if (token === 'h' || token === 'hh') {
|
|
241
|
+
sawHour = true;
|
|
242
|
+
finer(3600000);
|
|
243
|
+
}
|
|
244
|
+
else if (token === 's' || token === 'ss')
|
|
245
|
+
finer(1000);
|
|
246
|
+
else if ((token === 'm' || token === 'mm') && sawHour)
|
|
247
|
+
finer(60000);
|
|
248
|
+
}
|
|
249
|
+
return unit;
|
|
250
|
+
}
|
|
251
|
+
function renderDate(body, date, serial) {
|
|
252
|
+
const unit = finestUnit(body);
|
|
253
|
+
if (unit !== null && unit > 0)
|
|
254
|
+
date = new Date(Math.round(date.getTime() / unit) * unit);
|
|
255
|
+
const meridiem = meridiemOf(body);
|
|
256
|
+
const hourOf = (d) => (meridiem ? ((d.getUTCHours() + 11) % 12) + 1 : d.getUTCHours());
|
|
154
257
|
let out = '';
|
|
155
258
|
let sawHour = false;
|
|
259
|
+
let sawSecond = false;
|
|
156
260
|
for (let i = 0; i < body.length; i += 1) {
|
|
157
261
|
const ch = body[i];
|
|
158
262
|
if (ch === '\\') {
|
|
@@ -160,12 +264,33 @@ function renderDate(body, date) {
|
|
|
160
264
|
i += 1;
|
|
161
265
|
continue;
|
|
162
266
|
}
|
|
267
|
+
const elapsed = ELAPSED_TOKEN.exec(body.slice(i));
|
|
268
|
+
if (elapsed) {
|
|
269
|
+
const token = elapsed[1];
|
|
270
|
+
i += elapsed[0].length - 1;
|
|
271
|
+
// Without a serial there is no duration to total, so the clock's own
|
|
272
|
+
// reading is the best answer; with one, the whole of it counts.
|
|
273
|
+
const days = serial ?? 0;
|
|
274
|
+
const total = token.startsWith('h') ? days * 24 : token.startsWith('m') ? days * 1440 : days * 86400;
|
|
275
|
+
const whole = Math.floor(Math.abs(total) + 1e-9) * (total < 0 ? -1 : 1);
|
|
276
|
+
out += token.length === 2 ? pad2(whole) : String(whole);
|
|
277
|
+
if (token.startsWith('h'))
|
|
278
|
+
sawHour = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
163
281
|
if (ch === '"') {
|
|
164
282
|
const end = body.indexOf('"', i + 1);
|
|
165
283
|
out += body.slice(i + 1, end < 0 ? body.length : end);
|
|
166
284
|
i = end < 0 ? body.length : end;
|
|
167
285
|
continue;
|
|
168
286
|
}
|
|
287
|
+
const fraction = sawSecond ? /^\.(0+)/.exec(body.slice(i)) : null;
|
|
288
|
+
if (fraction) {
|
|
289
|
+
const digits = fraction[1].length;
|
|
290
|
+
out += `.${String(date.getUTCMilliseconds()).padStart(3, '0').slice(0, digits).padEnd(digits, '0')}`;
|
|
291
|
+
i += fraction[0].length - 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
169
294
|
const m = DATE_TOKEN.exec(body.slice(i));
|
|
170
295
|
if (!m) {
|
|
171
296
|
out += ch;
|
|
@@ -173,6 +298,8 @@ function renderDate(body, date) {
|
|
|
173
298
|
}
|
|
174
299
|
const token = m[1];
|
|
175
300
|
i += token.length - 1;
|
|
301
|
+
if (token === 's' || token === 'ss')
|
|
302
|
+
sawSecond = true;
|
|
176
303
|
switch (token) {
|
|
177
304
|
case 'yyyy':
|
|
178
305
|
out += date.getUTCFullYear();
|
|
@@ -199,11 +326,11 @@ function renderDate(body, date) {
|
|
|
199
326
|
out += date.getUTCDate();
|
|
200
327
|
break;
|
|
201
328
|
case 'hh':
|
|
202
|
-
out += pad2(date
|
|
329
|
+
out += pad2(hourOf(date));
|
|
203
330
|
sawHour = true;
|
|
204
331
|
break;
|
|
205
332
|
case 'h':
|
|
206
|
-
out += date
|
|
333
|
+
out += hourOf(date);
|
|
207
334
|
sawHour = true;
|
|
208
335
|
break;
|
|
209
336
|
case 'ss':
|
|
@@ -212,10 +339,22 @@ function renderDate(body, date) {
|
|
|
212
339
|
case 's':
|
|
213
340
|
out += date.getUTCSeconds();
|
|
214
341
|
break;
|
|
342
|
+
// Excel prints what the token spells: AM/PM in full, A/P as the one
|
|
343
|
+
// letter, and each in the case it was written in.
|
|
215
344
|
case 'AM/PM':
|
|
345
|
+
out += date.getUTCHours() < 12 ? 'AM' : 'PM';
|
|
346
|
+
break;
|
|
216
347
|
case 'am/pm':
|
|
348
|
+
out += date.getUTCHours() < 12 ? 'am' : 'pm';
|
|
349
|
+
break;
|
|
217
350
|
case 'A/P':
|
|
218
|
-
out += date.getUTCHours() < 12 ? '
|
|
351
|
+
out += date.getUTCHours() < 12 ? 'A' : 'P';
|
|
352
|
+
break;
|
|
353
|
+
case 'a/p':
|
|
354
|
+
out += date.getUTCHours() < 12 ? 'a' : 'p';
|
|
355
|
+
break;
|
|
356
|
+
case 'mmmmm':
|
|
357
|
+
out += MONTHS[date.getUTCMonth()].slice(0, 1);
|
|
219
358
|
break;
|
|
220
359
|
// `m` and `mm` are MINUTES after an hour token, MONTHS otherwise. This
|
|
221
360
|
// is the one piece of context the scanner carries, and getting it wrong
|
|
@@ -231,7 +370,105 @@ function renderDate(body, date) {
|
|
|
231
370
|
}
|
|
232
371
|
return out;
|
|
233
372
|
}
|
|
234
|
-
|
|
373
|
+
const FRACTION = /^(?:([#0?]+)\s+)?([#0?]+)\/([#0?]+|\d+)$/;
|
|
374
|
+
/** The plan for a fraction section, or null when it is not one. */
|
|
375
|
+
function planFraction(body) {
|
|
376
|
+
// Only the bare shape reads as a fraction; anything with quoted text or an
|
|
377
|
+
// escape around it stays on the ordinary numeric path, where it always was.
|
|
378
|
+
const m = FRACTION.exec(body.trim());
|
|
379
|
+
if (!m)
|
|
380
|
+
return null;
|
|
381
|
+
const den = m[3];
|
|
382
|
+
return { int: m[1] ?? null, num: m[2], den, denLiteral: /^\d+$/.test(den) ? Number(den) : null };
|
|
383
|
+
}
|
|
384
|
+
/** The closest numerator and denominator, with the denominator capped. */
|
|
385
|
+
function bestFraction(value, maxDen) {
|
|
386
|
+
// Stern-Brocot: take the mediant of the two bounding fractions until the
|
|
387
|
+
// denominator would pass the cap. Exact for anything that fits, and it
|
|
388
|
+
// cannot run longer than the cap.
|
|
389
|
+
let loNum = 0;
|
|
390
|
+
let loDen = 1;
|
|
391
|
+
let hiNum = 1;
|
|
392
|
+
let hiDen = 0;
|
|
393
|
+
let bestNum = Math.round(value);
|
|
394
|
+
let bestDen = 1;
|
|
395
|
+
let bestErr = Math.abs(value - bestNum);
|
|
396
|
+
for (let i = 0; i < 64; i += 1) {
|
|
397
|
+
const num = loNum + hiNum;
|
|
398
|
+
const den = loDen + hiDen;
|
|
399
|
+
if (den > maxDen)
|
|
400
|
+
break;
|
|
401
|
+
const mediant = num / den;
|
|
402
|
+
const err = Math.abs(value - mediant);
|
|
403
|
+
if (err < bestErr - 1e-12) {
|
|
404
|
+
bestErr = err;
|
|
405
|
+
bestNum = num;
|
|
406
|
+
bestDen = den;
|
|
407
|
+
}
|
|
408
|
+
if (err < 1e-12)
|
|
409
|
+
break;
|
|
410
|
+
if (mediant < value) {
|
|
411
|
+
loNum = num;
|
|
412
|
+
loDen = den;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
hiNum = num;
|
|
416
|
+
hiDen = den;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { num: bestNum, den: bestDen };
|
|
420
|
+
}
|
|
421
|
+
/** A placeholder group as Excel pads it: `0` a zero, `?` a space, `#` nothing. */
|
|
422
|
+
function padHolders(text, holders) {
|
|
423
|
+
if (text.length >= holders.length)
|
|
424
|
+
return text;
|
|
425
|
+
const filler = holders.includes('0') ? '0' : holders.includes('?') ? ' ' : '';
|
|
426
|
+
return filler ? filler.repeat(holders.length - text.length) + text : text;
|
|
427
|
+
}
|
|
428
|
+
/** One number through a fraction pattern. */
|
|
429
|
+
function renderFraction(plan, value) {
|
|
430
|
+
const sign = value < 0 ? '-' : '';
|
|
431
|
+
const magnitude = Math.abs(value);
|
|
432
|
+
const whole = plan.int ? Math.floor(magnitude + 1e-12) : 0;
|
|
433
|
+
const rest = magnitude - whole;
|
|
434
|
+
const maxDen = plan.denLiteral ?? Math.pow(10, plan.den.length) - 1;
|
|
435
|
+
const found = plan.denLiteral
|
|
436
|
+
? { num: Math.round(rest * plan.denLiteral), den: plan.denLiteral }
|
|
437
|
+
: bestFraction(rest, maxDen);
|
|
438
|
+
let num = found.num;
|
|
439
|
+
const den = found.den;
|
|
440
|
+
let carried = whole;
|
|
441
|
+
// Rounding up to a whole: 0.99 over `# ?/?` is 1, not 1/1.
|
|
442
|
+
if (den > 0 && num >= den) {
|
|
443
|
+
carried += Math.floor(num / den);
|
|
444
|
+
num %= den;
|
|
445
|
+
}
|
|
446
|
+
if (!plan.int) {
|
|
447
|
+
return `${sign}${padHolders(String(num + carried * den), plan.num)}/${padHolders(String(den), plan.den)}`;
|
|
448
|
+
}
|
|
449
|
+
const blanks = (holders) => (holders.includes('?') ? ' '.repeat(holders.length) : '');
|
|
450
|
+
const wholeText = carried === 0 && !plan.int.includes('0')
|
|
451
|
+
? ' '.repeat(plan.int.includes('?') ? plan.int.length : 0)
|
|
452
|
+
: padHolders(String(carried), plan.int);
|
|
453
|
+
// No remainder: Excel leaves the fraction's width blank rather than
|
|
454
|
+
// printing 0/1, so a column of them still lines up.
|
|
455
|
+
const fraction = num === 0
|
|
456
|
+
? `${blanks(plan.num)} ${blanks(plan.den)}`
|
|
457
|
+
: `${padHolders(String(num), plan.num)}/${padHolders(String(den), plan.den)}`;
|
|
458
|
+
return `${sign}${wholeText} ${fraction}`.replace(/\s+$/, '');
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Pull a numeric section apart into literal text and digit placeholders.
|
|
462
|
+
*
|
|
463
|
+
* Literal text before the first digit is the prefix and after the last is
|
|
464
|
+
* the suffix. Literal text BETWEEN integer digits, as in `000-00-0000` or
|
|
465
|
+
* `(###) ###-####`, turns the integer side into a mask that the digits are
|
|
466
|
+
* laid into from the right, which is how Excel's Special formats work.
|
|
467
|
+
*
|
|
468
|
+
* `_x` (Excel: leave the width of x) and `*x` (fill the cell with x) both
|
|
469
|
+
* become one space here: text has no cell width to leave or fill, and one
|
|
470
|
+
* space is what keeps `_($* #,##0.00_)` reading as `$ 1,234.00`.
|
|
471
|
+
*/
|
|
235
472
|
function planNumeric(body) {
|
|
236
473
|
let prefix = '';
|
|
237
474
|
let suffix = '';
|
|
@@ -244,24 +481,35 @@ function planNumeric(body) {
|
|
|
244
481
|
let scale = 1;
|
|
245
482
|
let seenPlaceholder = false;
|
|
246
483
|
let afterDecimal = false;
|
|
484
|
+
// The integer side as tokens, literal text included, until the decimal
|
|
485
|
+
// point. `pending` holds literal text after the last integer digit, which
|
|
486
|
+
// is part of the mask only if another digit follows it.
|
|
487
|
+
const mask = [];
|
|
488
|
+
let pending = '';
|
|
489
|
+
let maskHasLiteral = false;
|
|
490
|
+
const literal = (text) => {
|
|
491
|
+
if (!seenPlaceholder)
|
|
492
|
+
prefix += text;
|
|
493
|
+
else if (afterDecimal || scientific)
|
|
494
|
+
suffix += text;
|
|
495
|
+
else
|
|
496
|
+
pending += text;
|
|
497
|
+
};
|
|
247
498
|
for (let i = 0; i < body.length; i += 1) {
|
|
248
499
|
const ch = body[i];
|
|
249
500
|
if (ch === '\\') {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
501
|
+
literal(body[i + 1] ?? '');
|
|
502
|
+
i += 1;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (ch === '_' || ch === '*') {
|
|
506
|
+
literal(' ');
|
|
255
507
|
i += 1;
|
|
256
508
|
continue;
|
|
257
509
|
}
|
|
258
510
|
if (ch === '"') {
|
|
259
511
|
const end = body.indexOf('"', i + 1);
|
|
260
|
-
|
|
261
|
-
if (seenPlaceholder)
|
|
262
|
-
suffix += text;
|
|
263
|
-
else
|
|
264
|
-
prefix += text;
|
|
512
|
+
literal(body.slice(i + 1, end < 0 ? body.length : end));
|
|
265
513
|
i = end < 0 ? body.length : end;
|
|
266
514
|
continue;
|
|
267
515
|
}
|
|
@@ -273,8 +521,15 @@ function planNumeric(body) {
|
|
|
273
521
|
expPlaceholders += ch;
|
|
274
522
|
else if (afterDecimal)
|
|
275
523
|
fracPlaceholders += ch;
|
|
276
|
-
else
|
|
524
|
+
else {
|
|
277
525
|
intPlaceholders += ch;
|
|
526
|
+
if (pending) {
|
|
527
|
+
mask.push({ kind: 'lit', text: pending });
|
|
528
|
+
maskHasLiteral = true;
|
|
529
|
+
pending = '';
|
|
530
|
+
}
|
|
531
|
+
mask.push({ kind: 'digit', ch });
|
|
532
|
+
}
|
|
278
533
|
continue;
|
|
279
534
|
}
|
|
280
535
|
if (ch === '.') {
|
|
@@ -294,10 +549,7 @@ function planNumeric(body) {
|
|
|
294
549
|
}
|
|
295
550
|
if (ch === '%') {
|
|
296
551
|
percent = true;
|
|
297
|
-
|
|
298
|
-
suffix += '%';
|
|
299
|
-
else
|
|
300
|
-
prefix += '%';
|
|
552
|
+
literal('%');
|
|
301
553
|
continue;
|
|
302
554
|
}
|
|
303
555
|
if ((ch === 'E' || ch === 'e') && /^[+-]/.test(body.slice(i + 1))) {
|
|
@@ -305,33 +557,74 @@ function planNumeric(body) {
|
|
|
305
557
|
i += 1;
|
|
306
558
|
continue;
|
|
307
559
|
}
|
|
308
|
-
|
|
309
|
-
suffix += ch;
|
|
310
|
-
else
|
|
311
|
-
prefix += ch;
|
|
560
|
+
literal(ch);
|
|
312
561
|
}
|
|
562
|
+
// Literal text after the last integer digit belongs to the suffix, before
|
|
563
|
+
// whatever the fraction and exponent added.
|
|
564
|
+
if (pending)
|
|
565
|
+
suffix = pending + suffix;
|
|
566
|
+
// A `?` on the integer side pads with a space, which the mask renderer
|
|
567
|
+
// does and the plain path (digits, grouping) does not.
|
|
568
|
+
const needsMask = maskHasLiteral || intPlaceholders.includes('?');
|
|
313
569
|
return {
|
|
314
|
-
prefix, suffix, intPlaceholders,
|
|
570
|
+
prefix, suffix, intPlaceholders, intMask: needsMask ? mask : null,
|
|
571
|
+
fracPlaceholders, expPlaceholders,
|
|
315
572
|
useGrouping, scale, percent, scientific,
|
|
316
573
|
};
|
|
317
574
|
}
|
|
318
|
-
|
|
319
|
-
|
|
575
|
+
/** Lay digits into a mask from the right: `000-00-0000` on 123456789 is
|
|
576
|
+
* `123-45-6789`; digits left over go in front, missing ones follow the
|
|
577
|
+
* placeholder's rule, so `###-####` on 1234 is `-1234` as Excel gives. */
|
|
578
|
+
function renderMask(mask, digits) {
|
|
579
|
+
let out = '';
|
|
580
|
+
let at = digits.length;
|
|
581
|
+
for (let i = mask.length - 1; i >= 0; i -= 1) {
|
|
582
|
+
const token = mask[i];
|
|
583
|
+
if (token.kind === 'lit') {
|
|
584
|
+
out = token.text + out;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (at > 0) {
|
|
588
|
+
at -= 1;
|
|
589
|
+
out = digits[at] + out;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (token.ch === '0')
|
|
593
|
+
out = `0${out}`;
|
|
594
|
+
else if (token.ch === '?')
|
|
595
|
+
out = ` ${out}`;
|
|
596
|
+
}
|
|
597
|
+
return digits.slice(0, at) + out;
|
|
598
|
+
}
|
|
599
|
+
function group(intText, mark) {
|
|
600
|
+
return intText.replace(/\B(?=(\d{3})+(?!\d))/g, mark);
|
|
320
601
|
}
|
|
321
|
-
function renderNumeric(plan, value) {
|
|
602
|
+
function renderNumeric(plan, value, marks) {
|
|
322
603
|
let n = value;
|
|
323
604
|
if (plan.percent)
|
|
324
605
|
n *= 100;
|
|
325
606
|
if (plan.scale !== 1)
|
|
326
607
|
n /= plan.scale;
|
|
327
608
|
if (plan.scientific) {
|
|
328
|
-
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
609
|
+
// The integer placeholders set the STEP of the exponent, which is what
|
|
610
|
+
// makes `##0.0E+0` engineering notation: three of them means the
|
|
611
|
+
// exponent moves in threes and the mantissa carries up to three integer
|
|
612
|
+
// digits, so 12345 reads 12.3E+3 rather than 1.2E+4. The everyday
|
|
613
|
+
// `0.00E+00` has one, and is the ordinary scientific form.
|
|
614
|
+
const step = Math.max(1, plan.intPlaceholders.length);
|
|
615
|
+
const decimals = plan.fracPlaceholders.length;
|
|
616
|
+
let exponent = n === 0 ? 0 : Math.floor(Math.log10(Math.abs(n)));
|
|
617
|
+
exponent = Math.floor(exponent / step) * step;
|
|
618
|
+
let mantissa = n / Math.pow(10, exponent);
|
|
619
|
+
// Rounding can carry the mantissa over its own width: 999.95 with one
|
|
620
|
+
// decimal is 1000.0, which is 1.0 a step further out.
|
|
621
|
+
if (Math.abs(Number(mantissa.toFixed(decimals))) >= Math.pow(10, step)) {
|
|
622
|
+
mantissa /= Math.pow(10, step);
|
|
623
|
+
exponent += step;
|
|
624
|
+
}
|
|
625
|
+
const sign = exponent < 0 ? '-' : '+';
|
|
626
|
+
const digits = String(Math.abs(exponent)).padStart(plan.expPlaceholders.length || 1, '0');
|
|
627
|
+
return `${plan.prefix}${mantissa.toFixed(decimals)}E${sign}${digits}${plan.suffix}`;
|
|
335
628
|
}
|
|
336
629
|
const decimals = plan.fracPlaceholders.length;
|
|
337
630
|
const fixed = Math.abs(n).toFixed(decimals);
|
|
@@ -340,12 +633,17 @@ function renderNumeric(plan, value) {
|
|
|
340
633
|
// leading zero entirely when the pattern is all `#`.
|
|
341
634
|
const minInt = plan.intPlaceholders.split('').filter((c) => c === '0').length;
|
|
342
635
|
let intText = rawInt;
|
|
343
|
-
if (
|
|
344
|
-
intText =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
636
|
+
if (plan.intMask) {
|
|
637
|
+
intText = renderMask(plan.intMask, rawInt === '0' && minInt === 0 ? '' : rawInt);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
if (intText.length < minInt)
|
|
641
|
+
intText = intText.padStart(minInt, '0');
|
|
642
|
+
if (minInt === 0 && intText === '0' && plan.intPlaceholders.length > 0)
|
|
643
|
+
intText = '';
|
|
644
|
+
if (plan.useGrouping && intText !== '')
|
|
645
|
+
intText = group(intText, marks.group);
|
|
646
|
+
}
|
|
349
647
|
// Trim optional decimals from the right: `0.0#` on 1.5 gives "1.5", not
|
|
350
648
|
// "1.50". A `0` placeholder is never trimmed, a `#` always may be, and a
|
|
351
649
|
// `?` becomes a space so columns line up.
|
|
@@ -358,7 +656,7 @@ function renderNumeric(plan, value) {
|
|
|
358
656
|
break;
|
|
359
657
|
fracText = kind === '?' ? `${fracText.slice(0, i)} ` : fracText.slice(0, i);
|
|
360
658
|
}
|
|
361
|
-
const body = fracText === '' ? intText : `${intText}
|
|
659
|
+
const body = fracText === '' ? intText : `${intText}${marks.decimal}${fracText}`;
|
|
362
660
|
// `#` on zero prints nothing, which is how "hide zeros" patterns work. The
|
|
363
661
|
// fallback below is only for a pattern with no digit placeholders at all.
|
|
364
662
|
return plan.prefix + body + plan.suffix;
|
|
@@ -368,6 +666,32 @@ function renderNumeric(plan, value) {
|
|
|
368
666
|
function hasPlaceholders(plan) {
|
|
369
667
|
return plan.intPlaceholders !== '' || plan.fracPlaceholders !== '';
|
|
370
668
|
}
|
|
669
|
+
/** The text section: `@` is the value, quotes and backslashes wrap
|
|
670
|
+
* literals, `_x` and `*x` are one space, as on the numeric side. */
|
|
671
|
+
function renderText(body, value) {
|
|
672
|
+
let out = '';
|
|
673
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
674
|
+
const ch = body[i];
|
|
675
|
+
if (ch === '\\') {
|
|
676
|
+
out += body[i + 1] ?? '';
|
|
677
|
+
i += 1;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (ch === '_' || ch === '*') {
|
|
681
|
+
out += ' ';
|
|
682
|
+
i += 1;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (ch === '"') {
|
|
686
|
+
const end = body.indexOf('"', i + 1);
|
|
687
|
+
out += body.slice(i + 1, end < 0 ? body.length : end);
|
|
688
|
+
i = end < 0 ? body.length : end;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
out += ch === '@' ? value : ch;
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
371
695
|
/** Excel's General: up to 11 significant digits, no trailing zeros. */
|
|
372
696
|
function general(value) {
|
|
373
697
|
if (typeof value === 'number') {
|
|
@@ -386,8 +710,12 @@ function general(value) {
|
|
|
386
710
|
* ten thousand cells sharing one format parses it once.
|
|
387
711
|
*/
|
|
388
712
|
const cache = new Map();
|
|
389
|
-
export
|
|
390
|
-
|
|
713
|
+
export const INVARIANT_MARKS = { decimal: '.', group: ',' };
|
|
714
|
+
export function compileNumberFormat(pattern, marks = INVARIANT_MARKS) {
|
|
715
|
+
// The marks join the cache key, so a sheet showing one locale does not
|
|
716
|
+
// serve its rendered numbers to a sheet showing another.
|
|
717
|
+
const key = marks === INVARIANT_MARKS ? pattern : `${pattern}\u0000${marks.decimal}${marks.group}`;
|
|
718
|
+
const hit = cache.get(key);
|
|
391
719
|
if (hit)
|
|
392
720
|
return hit;
|
|
393
721
|
const sections = splitSections(pattern).map(parseSection);
|
|
@@ -395,15 +723,22 @@ export function compileNumberFormat(pattern) {
|
|
|
395
723
|
const negative = sections[1];
|
|
396
724
|
const zero = sections[2];
|
|
397
725
|
const text = sections[3];
|
|
726
|
+
const conditional = sections.some((sec) => sec.condition);
|
|
398
727
|
// Plans are built once here, not per value.
|
|
399
728
|
const plans = new Map();
|
|
729
|
+
const fractions = new Map();
|
|
400
730
|
const dateFlags = new Map();
|
|
401
731
|
for (const s of sections) {
|
|
402
732
|
if (!s)
|
|
403
733
|
continue;
|
|
404
734
|
const isDate = looksLikeDate(s.body);
|
|
405
735
|
dateFlags.set(s, isDate);
|
|
406
|
-
if (
|
|
736
|
+
if (isDate)
|
|
737
|
+
continue;
|
|
738
|
+
const fraction = planFraction(s.body);
|
|
739
|
+
if (fraction)
|
|
740
|
+
fractions.set(s, fraction);
|
|
741
|
+
else
|
|
407
742
|
plans.set(s, planNumeric(s.body));
|
|
408
743
|
}
|
|
409
744
|
const compiled = {
|
|
@@ -413,15 +748,15 @@ export function compileNumberFormat(pattern) {
|
|
|
413
748
|
return { text: general(value) };
|
|
414
749
|
}
|
|
415
750
|
if (typeof value === 'string' && value !== '') {
|
|
416
|
-
|
|
751
|
+
// Excel's FOURTH section is the one that governs text. With one, `@`
|
|
752
|
+
// puts the text where it stands and a section without an `@` shows
|
|
753
|
+
// only its own literals, which is what makes `;;;` hide a cell and
|
|
754
|
+
// `;;;"n/a"` show n/a whatever was typed. With no fourth section the
|
|
755
|
+
// text is shown as it is, even under a numeric pattern like `0.00`.
|
|
756
|
+
const section = text ?? (positive?.body.includes('@') ? positive : undefined);
|
|
417
757
|
if (!section)
|
|
418
758
|
return { text: value };
|
|
419
|
-
|
|
420
|
-
// section's literal text is all you get.
|
|
421
|
-
const body = section.body.includes('@')
|
|
422
|
-
? section.body.replace(/@/g, value).replace(/"/g, '')
|
|
423
|
-
: value;
|
|
424
|
-
return { text: body, color: section.color };
|
|
759
|
+
return { text: renderText(section.body, section.body.includes('@') ? value : ''), color: section.color };
|
|
425
760
|
}
|
|
426
761
|
if (value === null || value === undefined || value === '')
|
|
427
762
|
return { text: '' };
|
|
@@ -436,55 +771,113 @@ export function compileNumberFormat(pattern) {
|
|
|
436
771
|
return { text: String(value) };
|
|
437
772
|
}
|
|
438
773
|
// Pick the section. Two sections means the second covers zero too.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
774
|
+
// With conditions, the first section whose condition the value passes
|
|
775
|
+
// wins and an unconditioned one is the fallback, as in Excel's
|
|
776
|
+
// `[<=9999999]###-####;(###) ###-####`.
|
|
777
|
+
let section;
|
|
778
|
+
if (conditional) {
|
|
779
|
+
section = sections.find((sec) => sec && (sec.condition ? passes(sec.condition, n) : true));
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
section = positive;
|
|
783
|
+
if (n < 0 && negative)
|
|
784
|
+
section = negative;
|
|
785
|
+
else if (n === 0 && zero)
|
|
786
|
+
section = zero;
|
|
787
|
+
}
|
|
444
788
|
if (!section)
|
|
445
789
|
return { text: general(value) };
|
|
446
790
|
if (dateFlags.get(section)) {
|
|
447
791
|
const d = toDate(value);
|
|
792
|
+
// `n` is the serial the value stands for, which is what an elapsed
|
|
793
|
+
// token totals: 1.5 is a day and a half, [h]:mm reads 36:00.
|
|
448
794
|
return d
|
|
449
|
-
? { text: renderDate(section.body, d), color: section.color }
|
|
795
|
+
? { text: renderDate(section.body, d, n), color: section.color }
|
|
450
796
|
: { text: general(value) };
|
|
451
797
|
}
|
|
798
|
+
const asFraction = fractions.get(section);
|
|
799
|
+
if (asFraction) {
|
|
800
|
+
// A negative with its own section is rendered from its magnitude,
|
|
801
|
+
// as everywhere else: the section supplies the sign.
|
|
802
|
+
const usingNegative = n < 0 && section === negative && !conditional;
|
|
803
|
+
return { text: renderFraction(asFraction, usingNegative ? Math.abs(n) : n), color: section.color };
|
|
804
|
+
}
|
|
452
805
|
const plan = plans.get(section);
|
|
453
806
|
if (!hasPlaceholders(plan)) {
|
|
807
|
+
// `@` is the text placeholder, and a NUMBER in a cell formatted as
|
|
808
|
+
// Text is still shown by Excel as the number rather than as the
|
|
809
|
+
// placeholder: `@` over 5 reads 5, not @.
|
|
810
|
+
if (section.body.includes('@')) {
|
|
811
|
+
return { text: renderText(section.body, general(value)), color: section.color };
|
|
812
|
+
}
|
|
454
813
|
return { text: plan.prefix + plan.suffix, color: section.color };
|
|
455
814
|
}
|
|
456
815
|
// A negative shown by its own section renders its ABSOLUTE value: the
|
|
457
816
|
// section supplies the sign, usually as parentheses or a literal minus.
|
|
458
|
-
const usingNegativeSection = n < 0 && section === negative;
|
|
817
|
+
const usingNegativeSection = n < 0 && section === negative && !conditional;
|
|
459
818
|
const magnitude = usingNegativeSection ? Math.abs(n) : n;
|
|
460
|
-
let out = renderNumeric(plan, magnitude);
|
|
819
|
+
let out = renderNumeric(plan, magnitude, marks);
|
|
461
820
|
// With no negative section, put the sign back on.
|
|
462
821
|
if (n < 0 && !usingNegativeSection && !out.startsWith('-'))
|
|
463
822
|
out = `-${out}`;
|
|
464
823
|
return { text: out, color: section.color };
|
|
465
824
|
},
|
|
466
825
|
};
|
|
467
|
-
cache.set(
|
|
826
|
+
cache.set(key, compiled);
|
|
468
827
|
return compiled;
|
|
469
828
|
}
|
|
470
829
|
/** Format one value against a pattern. Convenience over compile-then-call. */
|
|
471
830
|
export function formatWithPattern(value, pattern) {
|
|
472
831
|
return compileNumberFormat(pattern).format(value).text;
|
|
473
832
|
}
|
|
474
|
-
/** The presets behind Ctrl+Shift+1 through 6.
|
|
833
|
+
/** The presets behind Ctrl+Shift+1 through 6 and the ribbon's $ % , buttons.
|
|
834
|
+
* Percent is Excel's Percent Style, no decimals: 0.425 reads 43%. */
|
|
475
835
|
export const FORMAT_PRESETS = {
|
|
476
836
|
general: 'General',
|
|
477
837
|
number: '#,##0.00',
|
|
478
838
|
time: 'h:mm AM/PM',
|
|
479
839
|
date: 'yyyy-mm-dd',
|
|
480
840
|
currency: '$#,##0.00;($#,##0.00)',
|
|
481
|
-
|
|
841
|
+
accounting: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)',
|
|
842
|
+
percent: '0%',
|
|
482
843
|
scientific: '0.00E+00',
|
|
483
844
|
};
|
|
845
|
+
/** What the ribbon's Number Format combo applies for a category. Excel's
|
|
846
|
+
* "Percentage" there carries two decimals where its % button carries none;
|
|
847
|
+
* the other entries are the presets. */
|
|
848
|
+
export const FORMAT_CATEGORY_PATTERNS = {
|
|
849
|
+
...FORMAT_PRESETS,
|
|
850
|
+
percent: '0.00%',
|
|
851
|
+
};
|
|
852
|
+
/** Excel's Special category: fixed-shape numbers laid into a mask. */
|
|
853
|
+
export const SPECIAL_FORMATS = {
|
|
854
|
+
zip: { label: 'Zip Code', pattern: '00000' },
|
|
855
|
+
zip4: { label: 'Zip Code + 4', pattern: '00000-0000' },
|
|
856
|
+
phone: { label: 'Phone Number', pattern: '[<=9999999]###-####;(###) ###-####' },
|
|
857
|
+
ssn: { label: 'Social Security Number', pattern: '000-00-0000' },
|
|
858
|
+
};
|
|
859
|
+
/** The pattern Excel's Accounting category spells for a symbol and a
|
|
860
|
+
* number of decimals; an empty symbol is its "None". */
|
|
861
|
+
export function accountingPattern(symbol, decimals) {
|
|
862
|
+
const digits = `#,##0${decimals > 0 ? '.' + '0'.repeat(decimals) : ''}`;
|
|
863
|
+
const sym = symbol === '$' ? '$* ' : symbol ? `${JSON.stringify(symbol)}* ` : '* ';
|
|
864
|
+
return `_(${sym}${digits}_);_(${sym}(${digits});_(${sym}"-"${'?'.repeat(decimals)}_);_(@_)`;
|
|
865
|
+
}
|
|
866
|
+
/** The symbol and decimals an accounting pattern was built from, or null
|
|
867
|
+
* when the pattern is not one `accountingPattern` would spell. */
|
|
868
|
+
export function accountingParts(fmt) {
|
|
869
|
+
if (!fmt)
|
|
870
|
+
return null;
|
|
871
|
+
const m = /^_\((?:"([^"]*)"|\$)?\* #,##0(\.0+)?_\);/.exec(fmt);
|
|
872
|
+
if (!m)
|
|
873
|
+
return null;
|
|
874
|
+
const symbol = m[1] !== undefined ? m[1] : fmt.startsWith('_($') ? '$' : '';
|
|
875
|
+
return { symbol, decimals: m[2] ? m[2].length - 1 : 0 };
|
|
876
|
+
}
|
|
484
877
|
/**
|
|
485
878
|
* Which of Excel's categories a pattern belongs to, with the decimals and
|
|
486
879
|
* separator it carries, so the ribbon's combo can say "Percentage" for a
|
|
487
|
-
* cell holding `0%` (a typed 12%) as well as for the
|
|
880
|
+
* cell holding `0%` (a typed 12%) as well as for the combo's `0.00%`, and
|
|
488
881
|
* Format Cells can open on the right category. A pattern that is none of
|
|
489
882
|
* them is 'custom'.
|
|
490
883
|
*/
|
|
@@ -492,6 +885,10 @@ export function formatCategory(fmt) {
|
|
|
492
885
|
if (!fmt || fmt === 'General')
|
|
493
886
|
return { category: 'general', decimals: 2, thousands: true };
|
|
494
887
|
const decimals = fmt.match(/\.(0+)/)?.[1]?.length ?? 0;
|
|
888
|
+
if (accountingParts(fmt))
|
|
889
|
+
return { category: 'accounting', decimals, thousands: true };
|
|
890
|
+
if (Object.values(SPECIAL_FORMATS).some((sp) => sp.pattern === fmt))
|
|
891
|
+
return { category: 'special', decimals: 0, thousands: false };
|
|
495
892
|
if (/E\+/i.test(fmt))
|
|
496
893
|
return { category: 'scientific', decimals, thousands: false };
|
|
497
894
|
if (/%$/.test(fmt))
|