@trebco/treb 23.6.2 → 25.0.0-rc1
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/.eslintignore +8 -0
- package/.eslintrc.js +164 -0
- package/README-shadow-DOM.md +88 -0
- package/README.md +37 -130
- package/api-config.json +29 -0
- package/api-generator/api-generator-types.ts +82 -0
- package/api-generator/api-generator.ts +1172 -0
- package/api-generator/package.json +3 -0
- package/build/treb-spreadsheet.mjs +14 -0
- package/{treb.d.ts → build/treb.d.ts} +293 -299
- package/esbuild-custom-element.mjs +336 -0
- package/esbuild.js +305 -0
- package/package.json +43 -14
- package/treb-base-types/package.json +5 -0
- package/treb-base-types/src/api_types.ts +36 -0
- package/treb-base-types/src/area.ts +583 -0
- package/treb-base-types/src/basic_types.ts +45 -0
- package/treb-base-types/src/cell.ts +612 -0
- package/treb-base-types/src/cells.ts +1066 -0
- package/treb-base-types/src/color.ts +124 -0
- package/treb-base-types/src/import.ts +71 -0
- package/treb-base-types/src/index-standalone.ts +29 -0
- package/treb-base-types/src/index.ts +42 -0
- package/treb-base-types/src/layout.ts +47 -0
- package/treb-base-types/src/localization.ts +187 -0
- package/treb-base-types/src/rectangle.ts +145 -0
- package/treb-base-types/src/render_text.ts +72 -0
- package/treb-base-types/src/style.ts +545 -0
- package/treb-base-types/src/table.ts +109 -0
- package/treb-base-types/src/text_part.ts +54 -0
- package/treb-base-types/src/theme.ts +608 -0
- package/treb-base-types/src/union.ts +152 -0
- package/treb-base-types/src/value-type.ts +164 -0
- package/treb-base-types/style/resizable.css +59 -0
- package/treb-calculator/modern.tsconfig.json +11 -0
- package/treb-calculator/package.json +5 -0
- package/treb-calculator/src/calculator.ts +2546 -0
- package/treb-calculator/src/complex-math.ts +558 -0
- package/treb-calculator/src/dag/array-vertex.ts +198 -0
- package/treb-calculator/src/dag/graph.ts +951 -0
- package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
- package/treb-calculator/src/dag/vertex.ts +352 -0
- package/treb-calculator/src/descriptors.ts +162 -0
- package/treb-calculator/src/expression-calculator.ts +1069 -0
- package/treb-calculator/src/function-error.ts +103 -0
- package/treb-calculator/src/function-library.ts +103 -0
- package/treb-calculator/src/functions/base-functions.ts +1214 -0
- package/treb-calculator/src/functions/checkbox.ts +164 -0
- package/treb-calculator/src/functions/complex-functions.ts +253 -0
- package/treb-calculator/src/functions/finance-functions.ts +399 -0
- package/treb-calculator/src/functions/information-functions.ts +102 -0
- package/treb-calculator/src/functions/matrix-functions.ts +182 -0
- package/treb-calculator/src/functions/sparkline.ts +335 -0
- package/treb-calculator/src/functions/statistics-functions.ts +350 -0
- package/treb-calculator/src/functions/text-functions.ts +298 -0
- package/treb-calculator/src/index.ts +27 -0
- package/treb-calculator/src/notifier-types.ts +59 -0
- package/treb-calculator/src/primitives.ts +428 -0
- package/treb-calculator/src/utilities.ts +305 -0
- package/treb-charts/package.json +5 -0
- package/treb-charts/src/chart-functions.ts +156 -0
- package/treb-charts/src/chart-types.ts +230 -0
- package/treb-charts/src/chart.ts +1288 -0
- package/treb-charts/src/index.ts +24 -0
- package/treb-charts/src/main.ts +37 -0
- package/treb-charts/src/rectangle.ts +52 -0
- package/treb-charts/src/renderer.ts +1841 -0
- package/treb-charts/src/util.ts +122 -0
- package/treb-charts/style/charts.scss +221 -0
- package/treb-charts/style/old-charts.scss +250 -0
- package/treb-embed/markup/layout.html +137 -0
- package/treb-embed/markup/toolbar.html +175 -0
- package/treb-embed/modern.tsconfig.json +25 -0
- package/treb-embed/src/custom-element/content-types.d.ts +18 -0
- package/treb-embed/src/custom-element/global.d.ts +11 -0
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
- package/treb-embed/src/custom-element/treb-global.ts +44 -0
- package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
- package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
- package/treb-embed/src/index.ts +16 -0
- package/treb-embed/src/language-model.ts +41 -0
- package/treb-embed/src/options.ts +320 -0
- package/treb-embed/src/progress-dialog.ts +228 -0
- package/treb-embed/src/selection-state.ts +16 -0
- package/treb-embed/src/spinner.ts +42 -0
- package/treb-embed/src/toolbar-message.ts +96 -0
- package/treb-embed/src/types.ts +167 -0
- package/treb-embed/style/autocomplete.scss +103 -0
- package/treb-embed/style/dark-theme.scss +114 -0
- package/treb-embed/style/defaults.scss +36 -0
- package/treb-embed/style/dialog.scss +181 -0
- package/treb-embed/style/dropdown-select.scss +101 -0
- package/treb-embed/style/formula-bar.scss +193 -0
- package/treb-embed/style/grid.scss +374 -0
- package/treb-embed/style/layout.scss +424 -0
- package/treb-embed/style/mouse-mask.scss +67 -0
- package/treb-embed/style/note.scss +92 -0
- package/treb-embed/style/overlay-editor.scss +102 -0
- package/treb-embed/style/spinner.scss +92 -0
- package/treb-embed/style/tab-bar.scss +228 -0
- package/treb-embed/style/table.scss +80 -0
- package/treb-embed/style/theme-defaults.scss +444 -0
- package/treb-embed/style/toolbar.scss +416 -0
- package/treb-embed/style/tooltip.scss +68 -0
- package/treb-embed/style/treb-icons.scss +130 -0
- package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
- package/treb-embed/style/z-index.scss +43 -0
- package/treb-export/docs/charts.md +68 -0
- package/treb-export/modern.tsconfig.json +19 -0
- package/treb-export/package.json +4 -0
- package/treb-export/src/address-type.ts +77 -0
- package/treb-export/src/base-template.ts +22 -0
- package/treb-export/src/column-width.ts +85 -0
- package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
- package/treb-export/src/drawing2/chart2.ts +282 -0
- package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
- package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
- package/treb-export/src/drawing2/drawing2.ts +355 -0
- package/treb-export/src/drawing2/embedded-image.ts +71 -0
- package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
- package/treb-export/src/export-worker/export-worker.ts +99 -0
- package/treb-export/src/export-worker/index-modern.ts +22 -0
- package/treb-export/src/export2.ts +2204 -0
- package/treb-export/src/import2.ts +882 -0
- package/treb-export/src/relationship.ts +36 -0
- package/treb-export/src/shared-strings2.ts +128 -0
- package/treb-export/src/template-2.ts +22 -0
- package/treb-export/src/unescape_xml.ts +47 -0
- package/treb-export/src/workbook-sheet2.ts +182 -0
- package/treb-export/src/workbook-style2.ts +1285 -0
- package/treb-export/src/workbook-theme2.ts +88 -0
- package/treb-export/src/workbook2.ts +491 -0
- package/treb-export/src/xml-utils.ts +201 -0
- package/treb-export/template/base/[Content_Types].xml +2 -0
- package/treb-export/template/base/_rels/.rels +2 -0
- package/treb-export/template/base/docProps/app.xml +2 -0
- package/treb-export/template/base/docProps/core.xml +12 -0
- package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
- package/treb-export/template/base/xl/sharedStrings.xml +2 -0
- package/treb-export/template/base/xl/styles.xml +2 -0
- package/treb-export/template/base/xl/theme/theme1.xml +2 -0
- package/treb-export/template/base/xl/workbook.xml +2 -0
- package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
- package/treb-export/template/base.xlsx +0 -0
- package/treb-format/package.json +8 -0
- package/treb-format/src/format.test.ts +213 -0
- package/treb-format/src/format.ts +942 -0
- package/treb-format/src/format_cache.ts +199 -0
- package/treb-format/src/format_parser.ts +723 -0
- package/treb-format/src/index.ts +25 -0
- package/treb-format/src/number_format_section.ts +100 -0
- package/treb-format/src/value_parser.ts +337 -0
- package/treb-grid/package.json +5 -0
- package/treb-grid/src/editors/autocomplete.ts +394 -0
- package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
- package/treb-grid/src/editors/formula_bar.ts +473 -0
- package/treb-grid/src/editors/formula_editor_base.ts +910 -0
- package/treb-grid/src/editors/overlay_editor.ts +511 -0
- package/treb-grid/src/index.ts +37 -0
- package/treb-grid/src/layout/base_layout.ts +2618 -0
- package/treb-grid/src/layout/grid_layout.ts +299 -0
- package/treb-grid/src/layout/rectangle_cache.ts +86 -0
- package/treb-grid/src/render/selection-renderer.ts +414 -0
- package/treb-grid/src/render/svg_header_overlay.ts +93 -0
- package/treb-grid/src/render/svg_selection_block.ts +187 -0
- package/treb-grid/src/render/tile_renderer.ts +2122 -0
- package/treb-grid/src/types/annotation.ts +216 -0
- package/treb-grid/src/types/border_constants.ts +34 -0
- package/treb-grid/src/types/clipboard_data.ts +31 -0
- package/treb-grid/src/types/data_model.ts +334 -0
- package/treb-grid/src/types/drag_mask.ts +81 -0
- package/treb-grid/src/types/grid.ts +7743 -0
- package/treb-grid/src/types/grid_base.ts +3644 -0
- package/treb-grid/src/types/grid_command.ts +470 -0
- package/treb-grid/src/types/grid_events.ts +124 -0
- package/treb-grid/src/types/grid_options.ts +97 -0
- package/treb-grid/src/types/grid_selection.ts +60 -0
- package/treb-grid/src/types/named_range.ts +369 -0
- package/treb-grid/src/types/scale-control.ts +202 -0
- package/treb-grid/src/types/serialize_options.ts +72 -0
- package/treb-grid/src/types/set_range_options.ts +52 -0
- package/treb-grid/src/types/sheet.ts +3099 -0
- package/treb-grid/src/types/sheet_types.ts +95 -0
- package/treb-grid/src/types/tab_bar.ts +464 -0
- package/treb-grid/src/types/tile.ts +59 -0
- package/treb-grid/src/types/update_flags.ts +75 -0
- package/treb-grid/src/util/dom_utilities.ts +44 -0
- package/treb-grid/src/util/fontmetrics2.ts +179 -0
- package/treb-grid/src/util/ua.ts +104 -0
- package/treb-logo.svg +18 -0
- package/treb-parser/package.json +5 -0
- package/treb-parser/src/csv-parser.ts +122 -0
- package/treb-parser/src/index.ts +25 -0
- package/treb-parser/src/md-parser.ts +526 -0
- package/treb-parser/src/parser-types.ts +397 -0
- package/treb-parser/src/parser.test.ts +298 -0
- package/treb-parser/src/parser.ts +2673 -0
- package/treb-utils/package.json +5 -0
- package/treb-utils/src/dispatch.ts +57 -0
- package/treb-utils/src/event_source.ts +147 -0
- package/treb-utils/src/ievent_source.ts +33 -0
- package/treb-utils/src/index.ts +31 -0
- package/treb-utils/src/measurement.ts +174 -0
- package/treb-utils/src/resizable.ts +160 -0
- package/treb-utils/src/scale.ts +137 -0
- package/treb-utils/src/serialize_html.ts +124 -0
- package/treb-utils/src/template.ts +70 -0
- package/treb-utils/src/validate_uri.ts +61 -0
- package/tsconfig.json +10 -0
- package/tsproject.json +30 -0
- package/util/license-plugin-esbuild.js +86 -0
- package/util/list-css-vars.sh +46 -0
- package/README-esm.md +0 -37
- package/treb-bundle.css +0 -2
- package/treb-bundle.mjs +0 -15
|
@@ -0,0 +1,2546 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of TREB.
|
|
3
|
+
*
|
|
4
|
+
* TREB is free software: you can redistribute it and/or modify it under the
|
|
5
|
+
* terms of the GNU General Public License as published by the Free Software
|
|
6
|
+
* Foundation, either version 3 of the License, or (at your option) any
|
|
7
|
+
* later version.
|
|
8
|
+
*
|
|
9
|
+
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
10
|
+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
11
|
+
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
12
|
+
* details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License along
|
|
15
|
+
* with TREB. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
* Copyright 2022-2023 trebco, llc.
|
|
18
|
+
* info@treb.app
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Localization, Cell, Area, ICellAddress, ICellAddress2, ValueType, UnionValue,
|
|
23
|
+
ArrayUnion, IArea, IsCellAddress, FlatCellData} from 'treb-base-types';
|
|
24
|
+
|
|
25
|
+
import { Parser, ExpressionUnit, DependencyList, UnitRange,
|
|
26
|
+
DecimalMarkType, ArgumentSeparatorType, UnitAddress, UnitIdentifier } from 'treb-parser';
|
|
27
|
+
|
|
28
|
+
import { Graph } from './dag/graph';
|
|
29
|
+
import type { SpreadsheetVertex } from './dag/spreadsheet_vertex';
|
|
30
|
+
import type { CalculationResult } from './dag/spreadsheet_vertex_base';
|
|
31
|
+
|
|
32
|
+
import { ExpressionCalculator, UnionIsMetadata } from './expression-calculator';
|
|
33
|
+
import * as Utilities from './utilities';
|
|
34
|
+
|
|
35
|
+
import { FunctionLibrary } from './function-library';
|
|
36
|
+
import { FunctionMap, ReturnType } from './descriptors';
|
|
37
|
+
import { AltFunctionLibrary, BaseFunctionLibrary } from './functions/base-functions';
|
|
38
|
+
import { FinanceFunctionLibrary } from './functions/finance-functions';
|
|
39
|
+
import { TextFunctionLibrary, TextFunctionAliases } from './functions/text-functions';
|
|
40
|
+
import { InformationFunctionLibrary } from './functions/information-functions';
|
|
41
|
+
import { StatisticsFunctionLibrary, StatisticsFunctionAliases } from './functions/statistics-functions';
|
|
42
|
+
import { ComplexFunctionLibrary } from './functions/complex-functions';
|
|
43
|
+
import { MatrixFunctionLibrary } from './functions/matrix-functions';
|
|
44
|
+
|
|
45
|
+
import { Variance } from './functions/statistics-functions';
|
|
46
|
+
|
|
47
|
+
import * as Primitives from './primitives';
|
|
48
|
+
|
|
49
|
+
import type { DataModel, Annotation, FunctionDescriptor, Sheet } from 'treb-grid';
|
|
50
|
+
import { LeafVertex } from './dag/leaf_vertex';
|
|
51
|
+
|
|
52
|
+
import { ArgumentError, ReferenceError, UnknownError, ValueError, ExpressionError, NAError, DivideByZeroError } from './function-error';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* breaking this out so we can use it for export (TODO)
|
|
56
|
+
*
|
|
57
|
+
* @param type
|
|
58
|
+
* @returns
|
|
59
|
+
*/
|
|
60
|
+
const TranslateSubtotalType = (type: string|number): number => {
|
|
61
|
+
|
|
62
|
+
if (typeof type === 'string') {
|
|
63
|
+
type = type.toUpperCase();
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'AVERAGE':
|
|
66
|
+
case 'MEAN':
|
|
67
|
+
type = 101;
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case 'COUNT':
|
|
71
|
+
type = 102;
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'COUNTA':
|
|
75
|
+
type = 103;
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case 'MAX':
|
|
79
|
+
type = 104;
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case 'MIN':
|
|
83
|
+
type = 105;
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case 'PRODUCT':
|
|
87
|
+
type = 106;
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'STDEV':
|
|
91
|
+
type = 107;
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'STDEVP':
|
|
95
|
+
type = 108;
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'SUM':
|
|
99
|
+
type = 109;
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'VAR':
|
|
103
|
+
type = 110;
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'VARP':
|
|
107
|
+
type = 111;
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
type = 0;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return type;
|
|
117
|
+
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* options for the evaluate function
|
|
122
|
+
*/
|
|
123
|
+
export interface EvaluateOptions {
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* argument separator to use when parsing input. set this option to
|
|
127
|
+
* use a consistent argument separator independent of current locale.
|
|
128
|
+
*/
|
|
129
|
+
argument_separator?: ','|';';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* allow R1C1-style references. the Evaluate function cannot use
|
|
133
|
+
* relative references (e.g. R[-1]C[0]), so those will always fail.
|
|
134
|
+
* however it may be useful to use direct R1C1 references (e.g. R3C4),
|
|
135
|
+
* so we optionally support that behind this flag.
|
|
136
|
+
*/
|
|
137
|
+
r1c1?: boolean;
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* we're providing a runtime option for how to handle complex numbers.
|
|
143
|
+
* we will need to pass that into the calculator when it's created to
|
|
144
|
+
* control which functions are loaded.
|
|
145
|
+
*/
|
|
146
|
+
export interface CalculatorOptions {
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* enable handling complex numbers in function calculation.
|
|
150
|
+
* @see EmbeddedSpreadsheetOptions
|
|
151
|
+
*/
|
|
152
|
+
complex_numbers: 'on'|'off';
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const default_calculator_options: CalculatorOptions = {
|
|
157
|
+
complex_numbers: 'off',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Calculator now extends graph. there's a 1-1 relationship between the
|
|
162
|
+
* two, and we wind up passing a lot of operations from one to the other.
|
|
163
|
+
* this also simplifies the callback structure, as we can use local methods.
|
|
164
|
+
*
|
|
165
|
+
* NOTE: graph vertices hold references to cells. while that makes lookups
|
|
166
|
+
* more efficient, it causes problems if you mutate the sheet (adding or
|
|
167
|
+
* removing rows or columns).
|
|
168
|
+
*
|
|
169
|
+
* in that event, you need to flush the graph to force rebuilding references
|
|
170
|
+
* (TODO: just rebuild references). after mutating the sheet, call
|
|
171
|
+
* ```
|
|
172
|
+
* Calculator.Reset();
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
*/
|
|
176
|
+
export class Calculator extends Graph {
|
|
177
|
+
|
|
178
|
+
// FIXME: need a way to share/pass parser flags
|
|
179
|
+
|
|
180
|
+
public readonly parser: Parser = new Parser();
|
|
181
|
+
|
|
182
|
+
protected readonly library = new FunctionLibrary();
|
|
183
|
+
|
|
184
|
+
protected registered_libraries: Record<string, boolean> = {};
|
|
185
|
+
|
|
186
|
+
// protected notifier_id_source = 100;
|
|
187
|
+
// protected notifiers: InternalNotifierType[] = [];
|
|
188
|
+
|
|
189
|
+
// protected graph: Graph = new Graph(); // |null = null;
|
|
190
|
+
// protected status: GraphStatus = GraphStatus.OK;
|
|
191
|
+
|
|
192
|
+
// FIXME: why is this a separate class? [actually is this a composition issue?]
|
|
193
|
+
protected expression_calculator = new ExpressionCalculator(
|
|
194
|
+
this.library,
|
|
195
|
+
this.parser);
|
|
196
|
+
|
|
197
|
+
/** the next calculation must do a full rebuild -- set on reset */
|
|
198
|
+
protected full_rebuild_required = false;
|
|
199
|
+
|
|
200
|
+
constructor(protected readonly model: DataModel, calculator_options: Partial<CalculatorOptions> = {}) {
|
|
201
|
+
|
|
202
|
+
super();
|
|
203
|
+
|
|
204
|
+
// at the moment options are only used here; in the future
|
|
205
|
+
// we may need to extend handling.
|
|
206
|
+
|
|
207
|
+
const options: CalculatorOptions = {
|
|
208
|
+
...default_calculator_options,
|
|
209
|
+
...calculator_options,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (options.complex_numbers === 'on') {
|
|
213
|
+
|
|
214
|
+
// complex number handling: we need to change SQRT, POWER and ^
|
|
215
|
+
|
|
216
|
+
for (const key of Object.keys(AltFunctionLibrary)) {
|
|
217
|
+
BaseFunctionLibrary[key] = AltFunctionLibrary[key];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Primitives.UseComplex();
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.UpdateLocale(); // for parser
|
|
226
|
+
|
|
227
|
+
// base functions
|
|
228
|
+
this.library.Register(
|
|
229
|
+
BaseFunctionLibrary,
|
|
230
|
+
TextFunctionLibrary, // we split out text functions
|
|
231
|
+
StatisticsFunctionLibrary, // also stats (wip)
|
|
232
|
+
FinanceFunctionLibrary, // also this (wip)
|
|
233
|
+
InformationFunctionLibrary, // etc
|
|
234
|
+
ComplexFunctionLibrary,
|
|
235
|
+
MatrixFunctionLibrary,
|
|
236
|
+
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// aliases
|
|
240
|
+
for (const key of Object.keys(StatisticsFunctionAliases)) {
|
|
241
|
+
this.library.Alias(key, StatisticsFunctionAliases[key]);
|
|
242
|
+
}
|
|
243
|
+
for (const key of Object.keys(TextFunctionAliases)) {
|
|
244
|
+
this.library.Alias(key, TextFunctionAliases[key]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// special functions... need reference to the graph (this)
|
|
248
|
+
|
|
249
|
+
this.library.Register({
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* this function is here because it checks whether rows are hidden or
|
|
253
|
+
* not. cell dependencies don't track that, so we need to do it here.
|
|
254
|
+
* and it needs to be volatile. this is an ugly, ugly function.
|
|
255
|
+
*/
|
|
256
|
+
Subtotal: {
|
|
257
|
+
arguments: [
|
|
258
|
+
{ name: 'type' },
|
|
259
|
+
{ name: 'range', metadata: true, }
|
|
260
|
+
],
|
|
261
|
+
fn: (type: number|string, ...args: any[]): UnionValue => {
|
|
262
|
+
|
|
263
|
+
type = TranslateSubtotalType(type);
|
|
264
|
+
|
|
265
|
+
// validate, I guess
|
|
266
|
+
|
|
267
|
+
if (type > 100) {
|
|
268
|
+
type -= 100;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (type < 1 || type > 11) {
|
|
272
|
+
return ArgumentError();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// any number of ranges are allowed, they will inherit
|
|
276
|
+
// the properties of the last argument so they will all
|
|
277
|
+
// return metadata
|
|
278
|
+
|
|
279
|
+
const flat = Utilities.FlattenBoxed(args);
|
|
280
|
+
|
|
281
|
+
// values is the set of values from the arguments that
|
|
282
|
+
// are numbers -- not strings, not errors -- and are not
|
|
283
|
+
// hidden. that last thing is the hard part.
|
|
284
|
+
|
|
285
|
+
// there's one other thing we care about which is non-empty,
|
|
286
|
+
// for COUNTA -- we can do that separately
|
|
287
|
+
|
|
288
|
+
const values: number[] = [];
|
|
289
|
+
let counta = 0;
|
|
290
|
+
let sum = 0;
|
|
291
|
+
|
|
292
|
+
let sheet: Sheet|undefined;
|
|
293
|
+
|
|
294
|
+
for (const entry of flat) {
|
|
295
|
+
|
|
296
|
+
// where is the metadata type? sigh
|
|
297
|
+
|
|
298
|
+
const address = (entry.value?.address) as UnitAddress;
|
|
299
|
+
if (!address) {
|
|
300
|
+
return ReferenceError();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!sheet || sheet.id !== address.sheet_id) {
|
|
304
|
+
|
|
305
|
+
if (!address.sheet_id) {
|
|
306
|
+
console.warn('invalid reference in metadata')
|
|
307
|
+
return ReferenceError();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
sheet = this.model.sheets.Find(address.sheet_id);
|
|
311
|
+
if (!sheet) {
|
|
312
|
+
console.warn('invalid sheet in metadata')
|
|
313
|
+
return ReferenceError();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const height = sheet.GetRowHeight(address.row);
|
|
319
|
+
if (!height) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const entry_value = entry.value?.value;
|
|
324
|
+
|
|
325
|
+
// counta includes empty strings
|
|
326
|
+
|
|
327
|
+
if (typeof entry_value === 'undefined') {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
counta++;
|
|
332
|
+
|
|
333
|
+
if (typeof entry_value === 'number') {
|
|
334
|
+
sum += entry_value;
|
|
335
|
+
values.push(entry_value);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let value = 0;
|
|
341
|
+
|
|
342
|
+
switch (type) {
|
|
343
|
+
case 1: // average
|
|
344
|
+
if (values.length === 0) { return DivideByZeroError(); }
|
|
345
|
+
value = sum / values.length;
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case 2: // count
|
|
349
|
+
value = values.length;
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case 3: // counta
|
|
353
|
+
value = counta;
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
case 4: // max
|
|
357
|
+
if (values.length === 0) { return ValueError(); }
|
|
358
|
+
value = Math.max.apply(0, values);
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case 5: // min
|
|
362
|
+
if (values.length === 0) { return ValueError(); }
|
|
363
|
+
value = Math.min.apply(0, values);
|
|
364
|
+
break;
|
|
365
|
+
|
|
366
|
+
case 6: // product
|
|
367
|
+
if (values.length === 0) { return ValueError(); }
|
|
368
|
+
value = 1;
|
|
369
|
+
for (const entry of values) {
|
|
370
|
+
value *= entry;
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 7: // stdev.s
|
|
375
|
+
if (values.length < 2) { return DivideByZeroError(); }
|
|
376
|
+
value = Math.sqrt(Variance(values, true));
|
|
377
|
+
break;
|
|
378
|
+
|
|
379
|
+
case 8: // stdev.p
|
|
380
|
+
if (values.length === 0) { return DivideByZeroError(); }
|
|
381
|
+
value = Math.sqrt(Variance(values, false));
|
|
382
|
+
break;
|
|
383
|
+
|
|
384
|
+
case 9: // sum
|
|
385
|
+
value = sum;
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
case 10: // var.s
|
|
389
|
+
if (values.length < 2) { return DivideByZeroError(); }
|
|
390
|
+
value = Variance(values, true);
|
|
391
|
+
break;
|
|
392
|
+
|
|
393
|
+
case 11: // var.p
|
|
394
|
+
if (values.length === 0) { return DivideByZeroError(); }
|
|
395
|
+
value = Variance(values, false);
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// console.info({type, args, flat, values});
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
type: ValueType.number,
|
|
404
|
+
value,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* this function is here so it has access to the parser.
|
|
412
|
+
* this is crazy expensive. is there a way to reduce cost?
|
|
413
|
+
*
|
|
414
|
+
* we could, in theory, consider that there are only a few
|
|
415
|
+
* valid operations here -- all binary. instead of using a
|
|
416
|
+
* generic call to the CalculateExpression routine, we could
|
|
417
|
+
* short-cut and call the binary method.
|
|
418
|
+
*
|
|
419
|
+
* OTOH that makes it more fragile, and might not really
|
|
420
|
+
* provide that much in the way of savings. still, it would
|
|
421
|
+
* be good if we could somehow cache some of the effort,
|
|
422
|
+
* particularly if the list data changes but not the expression.
|
|
423
|
+
*
|
|
424
|
+
*/
|
|
425
|
+
CountIf: {
|
|
426
|
+
arguments: [
|
|
427
|
+
{ name: 'range', },
|
|
428
|
+
{ name: 'criteria', }
|
|
429
|
+
],
|
|
430
|
+
fn: (range, criteria): UnionValue => {
|
|
431
|
+
|
|
432
|
+
const data = Utilities.FlattenUnboxed(range);
|
|
433
|
+
|
|
434
|
+
// console.info({range, data});
|
|
435
|
+
|
|
436
|
+
// console.info({range});
|
|
437
|
+
|
|
438
|
+
if (typeof criteria !== 'string') {
|
|
439
|
+
criteria = '=' + (criteria || 0).toString();
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
criteria = criteria.trim();
|
|
443
|
+
if (!/^[=<>]/.test(criteria)) {
|
|
444
|
+
criteria = '=' + criteria;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// switching to an array. doesn't actually seem to be any
|
|
449
|
+
// faster... more appropriate, though.
|
|
450
|
+
|
|
451
|
+
const parse_result = this.parser.Parse('{}' + criteria);
|
|
452
|
+
const expression = parse_result.expression;
|
|
453
|
+
|
|
454
|
+
if (parse_result.error || !expression) {
|
|
455
|
+
return ExpressionError();
|
|
456
|
+
}
|
|
457
|
+
if (expression.type !== 'binary') {
|
|
458
|
+
// console.warn('invalid expression [1]', expression);
|
|
459
|
+
return ExpressionError();
|
|
460
|
+
}
|
|
461
|
+
if (expression.left.type !== 'array') {
|
|
462
|
+
// console.warn('invalid expression [1]', expression);
|
|
463
|
+
return ExpressionError();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
expression.left.values = [data];
|
|
467
|
+
const result = this.CalculateExpression(expression);
|
|
468
|
+
|
|
469
|
+
// console.info({expression, result});
|
|
470
|
+
|
|
471
|
+
// this is no longer the case because we're getting
|
|
472
|
+
// a boxed result (union)
|
|
473
|
+
|
|
474
|
+
/*
|
|
475
|
+
if (Array.isArray(result)) {
|
|
476
|
+
let count = 0;
|
|
477
|
+
for (const column of result) {
|
|
478
|
+
for (const cell of column) {
|
|
479
|
+
if (cell.value) { count++; }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return { type: ValueType.number, value: count };
|
|
483
|
+
}
|
|
484
|
+
*/
|
|
485
|
+
|
|
486
|
+
if (result.type === ValueType.array) {
|
|
487
|
+
let count = 0;
|
|
488
|
+
for (const column of (result as ArrayUnion).value) {
|
|
489
|
+
for (const cell of column) {
|
|
490
|
+
if (cell.value) { count++; }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return { type: ValueType.number, value: count };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return result; // error?
|
|
497
|
+
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
/** like indirect, this creates dependencies at calc time */
|
|
502
|
+
Offset: {
|
|
503
|
+
arguments: [{
|
|
504
|
+
name: 'reference', description: 'Base reference', metadata: true, }, {
|
|
505
|
+
name: 'rows', description: 'number of rows to offset' }, {
|
|
506
|
+
name: 'columns', description: 'number of columns to offset' }, {
|
|
507
|
+
name: 'height', }, {
|
|
508
|
+
name: 'width', },
|
|
509
|
+
|
|
510
|
+
],
|
|
511
|
+
return_type: ReturnType.reference,
|
|
512
|
+
volatile: true,
|
|
513
|
+
fn: ((reference: UnionValue, rows = 0, columns = 0, height?: number, width?: number): UnionValue => {
|
|
514
|
+
|
|
515
|
+
if (!reference) {
|
|
516
|
+
return ArgumentError();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// const parse_result = this.parser.Parse(reference);
|
|
520
|
+
// if (parse_result.error || !parse_result.expression) {
|
|
521
|
+
// return ReferenceError;
|
|
522
|
+
//}
|
|
523
|
+
|
|
524
|
+
if (reference.type === ValueType.array) {
|
|
525
|
+
|
|
526
|
+
// subset array. this is constructed, so we can take ownership
|
|
527
|
+
// and modify it, although it would be safer to copy. also, what's
|
|
528
|
+
// the cost of functional vs imperative loops these days?
|
|
529
|
+
|
|
530
|
+
const end_row = typeof height === 'number' ? (rows + height) : undefined;
|
|
531
|
+
const end_column = typeof width === 'number' ? (columns + width) : undefined;
|
|
532
|
+
|
|
533
|
+
const result: UnionValue = {
|
|
534
|
+
type: ValueType.array,
|
|
535
|
+
value: reference.value.slice(rows, end_row).map(row => row.slice(columns, end_column)),
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
return result;
|
|
539
|
+
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// we need a proper type for this... also it might be a range
|
|
543
|
+
|
|
544
|
+
if (!UnionIsMetadata(reference)) {
|
|
545
|
+
console.info('e2', {reference})
|
|
546
|
+
return ReferenceError();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const check_result = this.DynamicDependencies(
|
|
550
|
+
reference.value.address,
|
|
551
|
+
this.expression_calculator.context.address,
|
|
552
|
+
true, rows, columns, width, height);
|
|
553
|
+
|
|
554
|
+
if (!check_result) {
|
|
555
|
+
console.info('e1', {check_result})
|
|
556
|
+
return ReferenceError();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (check_result.dirty) {
|
|
560
|
+
const current_vertex =
|
|
561
|
+
this.GetVertex(this.expression_calculator.context.address, true) as SpreadsheetVertex;
|
|
562
|
+
current_vertex.short_circuit = true;
|
|
563
|
+
return { type: ValueType.undefined, value: undefined };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (check_result.area) {
|
|
567
|
+
|
|
568
|
+
const start: ExpressionUnit = {
|
|
569
|
+
type: 'address', ...check_result.area.start,
|
|
570
|
+
label: '', position: 0,
|
|
571
|
+
// id: parse_result.expression.id,
|
|
572
|
+
id: 0,
|
|
573
|
+
};
|
|
574
|
+
const end: ExpressionUnit = {
|
|
575
|
+
type: 'address', ...check_result.area.end,
|
|
576
|
+
label: '', position: 0,
|
|
577
|
+
// id: parse_result.expression.id,
|
|
578
|
+
id: 0,
|
|
579
|
+
};
|
|
580
|
+
const expression: ExpressionUnit = check_result.area.count === 1 ? start : {
|
|
581
|
+
type: 'range', start, end,
|
|
582
|
+
label: '', position: 0,
|
|
583
|
+
// id: parse_result.expression.id,
|
|
584
|
+
id: 0,
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// return this.CalculateExpression(expression, undefined, true);
|
|
588
|
+
|
|
589
|
+
// return expression;
|
|
590
|
+
return { type: ValueType.object, value: expression };
|
|
591
|
+
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return ValueError();
|
|
595
|
+
|
|
596
|
+
}).bind(this),
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
Indirect: {
|
|
600
|
+
arguments: [
|
|
601
|
+
{ name: 'reference', description: 'Cell reference (string)' },
|
|
602
|
+
],
|
|
603
|
+
return_type: ReturnType.reference,
|
|
604
|
+
volatile: true, // necessary?
|
|
605
|
+
fn: ((reference: string) => {
|
|
606
|
+
|
|
607
|
+
if (!reference || (typeof reference !== 'string')) {
|
|
608
|
+
return ArgumentError();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const parse_result = this.parser.Parse(reference);
|
|
612
|
+
if (parse_result.error || !parse_result.expression ||
|
|
613
|
+
(parse_result.expression.type !== 'address' && parse_result.expression.type !== 'range')) {
|
|
614
|
+
return ReferenceError();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const check_result = this.DynamicDependencies(
|
|
618
|
+
parse_result.expression,
|
|
619
|
+
this.expression_calculator.context.address);
|
|
620
|
+
|
|
621
|
+
if (!check_result) {
|
|
622
|
+
return ReferenceError();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (check_result.dirty) {
|
|
626
|
+
const current_vertex =
|
|
627
|
+
this.GetVertex(this.expression_calculator.context.address, true) as SpreadsheetVertex;
|
|
628
|
+
current_vertex.short_circuit = true;
|
|
629
|
+
return { type: ValueType.undefined, value: undefined };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return { type: ValueType.object, value: parse_result.expression as any };
|
|
633
|
+
|
|
634
|
+
}).bind(this),
|
|
635
|
+
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* FIXME: there are cases we are not handling
|
|
640
|
+
*
|
|
641
|
+
* match seems to return either the matching row, in a column set,
|
|
642
|
+
* or matching column, in a row set. you can't search a 2d array.
|
|
643
|
+
* match also supports inexact matching but assumes data is ordered.
|
|
644
|
+
* (TODO).
|
|
645
|
+
*
|
|
646
|
+
* FIXME: we also need to icase match strings
|
|
647
|
+
*
|
|
648
|
+
*/
|
|
649
|
+
Match: {
|
|
650
|
+
arguments: [
|
|
651
|
+
{ name: 'value', boxed: true },
|
|
652
|
+
{ name: 'range', boxed: true },
|
|
653
|
+
{ name: 'type', },
|
|
654
|
+
],
|
|
655
|
+
fn: (value: UnionValue, range: UnionValue, type = 0) => {
|
|
656
|
+
|
|
657
|
+
if (type) {
|
|
658
|
+
console.warn('inexact match not supported', {value, range, type});
|
|
659
|
+
return NAError();
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
|
|
663
|
+
// I suppose you can match on a single value
|
|
664
|
+
if (range.type === ValueType.array) {
|
|
665
|
+
if (range.value.length === 1) {
|
|
666
|
+
const arr = range.value[0];
|
|
667
|
+
for (let i = 0; i < arr.length; i++) {
|
|
668
|
+
if (value.type == arr[i].type && value.value === arr[i].value) {
|
|
669
|
+
return {type: ValueType.number, value: i + 1};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
for (let i = 0; i < range.value.length; i++) {
|
|
675
|
+
const arr = range.value[i];
|
|
676
|
+
if (arr.length !== 1) {
|
|
677
|
+
return NAError();
|
|
678
|
+
}
|
|
679
|
+
if (value.type == arr[0].type && value.value === arr[0].value) {
|
|
680
|
+
return {type: ValueType.number, value: i + 1};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return NAError();
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
if (value.type === range.type && value.value === range.value) {
|
|
688
|
+
return {
|
|
689
|
+
type: ValueType.number, value: 1,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return NAError();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return ArgumentError();
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* FIXME: there are cases we are not handling
|
|
701
|
+
*/
|
|
702
|
+
Index: {
|
|
703
|
+
arguments: [
|
|
704
|
+
{ name: 'range', boxed: true },
|
|
705
|
+
{ name: 'row', },
|
|
706
|
+
{ name: 'column', }
|
|
707
|
+
],
|
|
708
|
+
volatile: false,
|
|
709
|
+
|
|
710
|
+
// FIXME: handle full row, full column calls
|
|
711
|
+
fn: (data: UnionValue, row?: number, column?: number) => {
|
|
712
|
+
|
|
713
|
+
// ensure array
|
|
714
|
+
if (data && data.type !== ValueType.array) {
|
|
715
|
+
data = {
|
|
716
|
+
type: ValueType.array,
|
|
717
|
+
value: [[data]],
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (row && column) {
|
|
722
|
+
|
|
723
|
+
// simple case: 2 indexes
|
|
724
|
+
|
|
725
|
+
const c = data.value[column - 1];
|
|
726
|
+
if (c) {
|
|
727
|
+
const cell = c[row - 1];
|
|
728
|
+
if (cell) {
|
|
729
|
+
return cell;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else if (row) {
|
|
734
|
+
|
|
735
|
+
// return an array
|
|
736
|
+
|
|
737
|
+
const value: UnionValue[][] = [];
|
|
738
|
+
for (const c of data.value) {
|
|
739
|
+
if (!c[row - 1]) {
|
|
740
|
+
return ArgumentError();
|
|
741
|
+
}
|
|
742
|
+
value.push([c[row-1]]);
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
type: ValueType.array,
|
|
746
|
+
value,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
else if (column) {
|
|
750
|
+
|
|
751
|
+
// return an array
|
|
752
|
+
|
|
753
|
+
const c = data.value[column - 1];
|
|
754
|
+
if (c) {
|
|
755
|
+
return {
|
|
756
|
+
type: ValueType.array,
|
|
757
|
+
value: [c],
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return ArgumentError();
|
|
763
|
+
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* this one does not have to be here, it's just here because
|
|
769
|
+
* the rest of the reference/lookup functions are here
|
|
770
|
+
*/
|
|
771
|
+
Rows: {
|
|
772
|
+
arguments: [{
|
|
773
|
+
name: 'reference', description: 'Array or reference' },
|
|
774
|
+
],
|
|
775
|
+
volatile: false,
|
|
776
|
+
fn: (reference: unknown) => {
|
|
777
|
+
if (!reference) {
|
|
778
|
+
return ArgumentError();
|
|
779
|
+
}
|
|
780
|
+
if (Array.isArray(reference)) {
|
|
781
|
+
const column = reference[0];
|
|
782
|
+
if (Array.isArray(column)) {
|
|
783
|
+
return { type: ValueType.number, value: column.length };
|
|
784
|
+
}
|
|
785
|
+
return ValueError();
|
|
786
|
+
}
|
|
787
|
+
return { type: ValueType.number, value: 1 };
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* this one does not have to be here, it's just here because
|
|
793
|
+
* the rest of the reference/lookup functions are here
|
|
794
|
+
*/
|
|
795
|
+
Columns: {
|
|
796
|
+
arguments: [{
|
|
797
|
+
name: 'reference', description: 'Array or reference' },
|
|
798
|
+
],
|
|
799
|
+
volatile: false,
|
|
800
|
+
fn: (reference: unknown) => {
|
|
801
|
+
if (!reference) {
|
|
802
|
+
return ArgumentError();
|
|
803
|
+
}
|
|
804
|
+
if (Array.isArray(reference)) {
|
|
805
|
+
return { type: ValueType.number, value: reference.length };
|
|
806
|
+
}
|
|
807
|
+
return { type: ValueType.number, value: 1 };
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* this should be in the 'information' library but it needs reference
|
|
813
|
+
* to the underlying cell (unresolved)
|
|
814
|
+
*/
|
|
815
|
+
IsFormula: {
|
|
816
|
+
description: 'Returns true if the reference is a formula',
|
|
817
|
+
arguments: [{
|
|
818
|
+
name: 'Reference',
|
|
819
|
+
metadata: true, /* OK with array metadata */
|
|
820
|
+
}],
|
|
821
|
+
fn: Utilities.ApplyAsArray((ref: UnionValue): UnionValue => {
|
|
822
|
+
|
|
823
|
+
// this is wasteful because we know that the range will all
|
|
824
|
+
// be in the same sheet... we don't need to look up every time
|
|
825
|
+
|
|
826
|
+
const sheet = this.model.sheets.Find(ref?.value?.address?.sheet_id || 0);
|
|
827
|
+
if (sheet) {
|
|
828
|
+
const cell = sheet.cells.GetCell(ref.value.address, false);
|
|
829
|
+
return {
|
|
830
|
+
type: ValueType.boolean,
|
|
831
|
+
value: cell?.type === ValueType.formula,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
type: ValueType.boolean, value: false,
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
}),
|
|
840
|
+
},
|
|
841
|
+
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* support for co-editing. we need to export calculated values from
|
|
849
|
+
* the leader instance, because things like RAND() and NOW() are
|
|
850
|
+
* nondeterministic (within reason).
|
|
851
|
+
*
|
|
852
|
+
* so the leader does the calculation and then we broadcast calculated
|
|
853
|
+
* values to followers.
|
|
854
|
+
*/
|
|
855
|
+
public ExportCalculatedValues(): Record<number, FlatCellData[]> {
|
|
856
|
+
const data: any = {};
|
|
857
|
+
for (const sheet of this.model.sheets.list) {
|
|
858
|
+
const calculated = sheet.cells.toJSON({calculated_value: true}).data as FlatCellData[];
|
|
859
|
+
data[sheet.id] = calculated.filter(test => test.calculated !== undefined);
|
|
860
|
+
}
|
|
861
|
+
return data;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* support for co-editing. if we get calculated values from the leader,
|
|
866
|
+
* we need to apply them to cells.
|
|
867
|
+
*
|
|
868
|
+
* to _see_ the data, you still have to make a couple of calls to repaint
|
|
869
|
+
* and update annotations. see EmbeddedSpreadsheetBase.Recalculate for hints.
|
|
870
|
+
*
|
|
871
|
+
* note that we're checking for list mismatch in one direction but not the
|
|
872
|
+
* other direction. should probably check both.
|
|
873
|
+
*/
|
|
874
|
+
public ApplyCalculatedValues(data: Record<number, FlatCellData[]>): void {
|
|
875
|
+
for (const sheet of this.model.sheets.list) {
|
|
876
|
+
const cells = data[sheet.id];
|
|
877
|
+
if (!cells) {
|
|
878
|
+
console.info('mismatch', sheet.id);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
for (const cell of cells) {
|
|
882
|
+
sheet.cells.data[cell.row][cell.column].SetCalculatedValue(cell.calculated);
|
|
883
|
+
// console.info(sheet.id, cell.row, cell.column, '->', cell.calculated);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* this is a mess [not as bad as it used to be]
|
|
891
|
+
*/
|
|
892
|
+
public SpreadCallback(vertex: SpreadsheetVertex, value: UnionValue): void {
|
|
893
|
+
|
|
894
|
+
if (!vertex.address || !vertex.address.sheet_id) {
|
|
895
|
+
throw new Error('spread callback called without sheet id');
|
|
896
|
+
}
|
|
897
|
+
// const cells = this.cells_map[vertex.address.sheet_id];
|
|
898
|
+
const cells = this.model.sheets.Find(vertex.address.sheet_id)?.cells;
|
|
899
|
+
|
|
900
|
+
if (!cells) {
|
|
901
|
+
throw new Error('spread callback called without cells');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (!vertex || !vertex.reference) return;
|
|
905
|
+
const area = vertex.reference.area;
|
|
906
|
+
|
|
907
|
+
if (area) {
|
|
908
|
+
const rows = area.rows;
|
|
909
|
+
const columns = area.columns;
|
|
910
|
+
|
|
911
|
+
// if (Array.isArray(value)) {
|
|
912
|
+
if (value.type === ValueType.array) {
|
|
913
|
+
|
|
914
|
+
// value = Utilities.Transpose2(value);
|
|
915
|
+
const values = Utilities.Transpose2((value as ArrayUnion).value);
|
|
916
|
+
|
|
917
|
+
// FIXME: recycle [?]
|
|
918
|
+
|
|
919
|
+
for (let row = 0; row < rows; row++) {
|
|
920
|
+
if (values[row]) {
|
|
921
|
+
let column = 0;
|
|
922
|
+
for (; column < columns && column < values[row].length; column++) {
|
|
923
|
+
cells.data[row + area.start.row][column + area.start.column].SetCalculatedValue(values[row][column].value, values[row][column].type);
|
|
924
|
+
}
|
|
925
|
+
for (; column < columns; column++) {
|
|
926
|
+
cells.data[row + area.start.row][column + area.start.column].SetCalculatedValue(undefined, ValueType.undefined);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
for (let column = 0; column < columns; column++) {
|
|
931
|
+
cells.data[row + area.start.row][column + area.start.column].SetCalculatedValue(undefined, ValueType.undefined);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
|
|
939
|
+
// single, recycle
|
|
940
|
+
|
|
941
|
+
for (let row = 0; row < rows; row++) {
|
|
942
|
+
for (let column = 0; column < columns; column++) {
|
|
943
|
+
cells.data[row + area.start.row][column + area.start.column].SetCalculatedValue(value.value, value.type);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* FIXME: for this version, this should be synchronous; the whole thing
|
|
955
|
+
* should run in a worker. should be much faster than context switching
|
|
956
|
+
* every time.
|
|
957
|
+
*/
|
|
958
|
+
public CalculationCallback(vertex: SpreadsheetVertex): CalculationResult {
|
|
959
|
+
|
|
960
|
+
// must have address [UPDATE: don't do this]
|
|
961
|
+
if (!vertex.address) throw(new Error('vertex missing address'));
|
|
962
|
+
if (vertex.expression_error) {
|
|
963
|
+
return {
|
|
964
|
+
value: UnknownError(),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return this.expression_calculator.Calculate(vertex.expression, vertex.address); // <- this one
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* generic function, broken out from the Indirect function. checks dynamic
|
|
974
|
+
* dependency for missing edges, and adds those edges.
|
|
975
|
+
*
|
|
976
|
+
* returns error on bad reference or circular dependency. this method
|
|
977
|
+
* does not set the "short circuit" flag, callers should set as appropriate.
|
|
978
|
+
*/
|
|
979
|
+
public DynamicDependencies(
|
|
980
|
+
expression: ExpressionUnit,
|
|
981
|
+
context?: ICellAddress,
|
|
982
|
+
offset = false,
|
|
983
|
+
offset_rows = 0,
|
|
984
|
+
offset_columns = 0,
|
|
985
|
+
resize_rows = 1,
|
|
986
|
+
resize_columns = 1,
|
|
987
|
+
) : {dirty: boolean, area: Area}|undefined {
|
|
988
|
+
|
|
989
|
+
// UPDATE: use current context (passed in as argument) to resolve
|
|
990
|
+
// relative references. otherwise the reference will change depending
|
|
991
|
+
// on current/active sheet
|
|
992
|
+
|
|
993
|
+
let area = this.ResolveExpressionAddress(expression, context);
|
|
994
|
+
|
|
995
|
+
if (!area) { return undefined; }
|
|
996
|
+
|
|
997
|
+
// flag. we're going to check _all_ dependencies at once, just in
|
|
998
|
+
// case (for this function this would only happen if the argument
|
|
999
|
+
// is an array).
|
|
1000
|
+
|
|
1001
|
+
let dirty = false;
|
|
1002
|
+
|
|
1003
|
+
// if (area) {
|
|
1004
|
+
|
|
1005
|
+
let sheet: Sheet|undefined;
|
|
1006
|
+
|
|
1007
|
+
if (expression.type === 'address' || expression.type === 'range') {
|
|
1008
|
+
const address_expression = (expression.type === 'range') ? expression.start : expression;
|
|
1009
|
+
if (address_expression.sheet_id) {
|
|
1010
|
+
sheet = this.model.sheets.Find(address_expression.sheet_id);
|
|
1011
|
+
|
|
1012
|
+
/*
|
|
1013
|
+
for (const test of this.model.sheets) {
|
|
1014
|
+
if (test.id === address_expression.sheet_id) {
|
|
1015
|
+
sheet = test;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
*/
|
|
1020
|
+
}
|
|
1021
|
+
else if (address_expression.sheet) {
|
|
1022
|
+
sheet = this.model.sheets.Find(address_expression.sheet);
|
|
1023
|
+
|
|
1024
|
+
/*
|
|
1025
|
+
const lc = address_expression.sheet.toLowerCase();
|
|
1026
|
+
for (const test of this.model.sheets) {
|
|
1027
|
+
if (test.name.toLowerCase() === lc) {
|
|
1028
|
+
sheet = test;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
*/
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!sheet && context?.sheet_id) {
|
|
1037
|
+
sheet = this.model.sheets.Find(context.sheet_id);
|
|
1038
|
+
|
|
1039
|
+
/*
|
|
1040
|
+
for (const test of this.model.sheets) {
|
|
1041
|
+
if (test.id === context.sheet_id) {
|
|
1042
|
+
sheet = test;
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
*/
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (!sheet) {
|
|
1050
|
+
throw new Error('missing sheet in dynamic dependencies [b21]');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// check any dirty...
|
|
1054
|
+
|
|
1055
|
+
// THIS IS ALMOST CERTAINLY WRONG. we should not be using active_sheet
|
|
1056
|
+
// here, we should use the area sheet. FIXME
|
|
1057
|
+
|
|
1058
|
+
area = sheet.RealArea(area);
|
|
1059
|
+
|
|
1060
|
+
const sheet_id = area.start.sheet_id;
|
|
1061
|
+
|
|
1062
|
+
if (offset) {
|
|
1063
|
+
area = new Area({
|
|
1064
|
+
column: area.start.column + offset_columns,
|
|
1065
|
+
row: area.start.row + offset_rows,
|
|
1066
|
+
sheet_id: area.start.sheet_id,
|
|
1067
|
+
}, {
|
|
1068
|
+
column: area.start.column + offset_columns + resize_rows - 1,
|
|
1069
|
+
row: area.start.row + offset_rows + resize_columns - 1,
|
|
1070
|
+
sheet_id: area.end.sheet_id,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
for (let row = area.start.row; row <= area.end.row; row++ ){
|
|
1075
|
+
for (let column = area.start.column; column <= area.end.column; column++ ){
|
|
1076
|
+
const vertex = this.GetVertex({row, column, sheet_id}, false);
|
|
1077
|
+
if (vertex && vertex.dirty) {
|
|
1078
|
+
|
|
1079
|
+
// so we know, given the structure of calculation, that there
|
|
1080
|
+
// is not an edge between these two vertices. we know that
|
|
1081
|
+
// because calculate() is never called on a vertex that has
|
|
1082
|
+
// dirty dependencies.
|
|
1083
|
+
|
|
1084
|
+
// so if we create an edge here, the calculate method can
|
|
1085
|
+
// short-circuit, and then this cell will be re-evaluated
|
|
1086
|
+
// when that cell is calculated.
|
|
1087
|
+
|
|
1088
|
+
// so all we have to do is add the edge. the question is,
|
|
1089
|
+
// do we need to remove that edge after the calculation?
|
|
1090
|
+
// or can we just wait for it to clean up on a rebuild?
|
|
1091
|
+
// (...) don't know for sure atm, test.
|
|
1092
|
+
|
|
1093
|
+
// actually we have to set some flag to tell the vertex to
|
|
1094
|
+
// short-circuit...
|
|
1095
|
+
|
|
1096
|
+
// before you set the short-circuit flag, test result so we
|
|
1097
|
+
// can error on circular ref
|
|
1098
|
+
|
|
1099
|
+
// const edge_result =
|
|
1100
|
+
|
|
1101
|
+
this.AddEdge({row, column, sheet_id}, this.expression_calculator.context.address);
|
|
1102
|
+
|
|
1103
|
+
//if (edge_result) {
|
|
1104
|
+
// return ReferenceError;
|
|
1105
|
+
//}
|
|
1106
|
+
|
|
1107
|
+
dirty = true;
|
|
1108
|
+
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// }
|
|
1113
|
+
|
|
1114
|
+
return { dirty, area };
|
|
1115
|
+
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* if locale has changed in Localization, update local resources.
|
|
1120
|
+
* this is necessary because (in chrome) worker doesn't get the system
|
|
1121
|
+
* locale properly (also, we might change it via parameter). we used to
|
|
1122
|
+
* just drop and reconstruct calculator, but we want to stop doing that
|
|
1123
|
+
* as part of supporting dynamic extension.
|
|
1124
|
+
*/
|
|
1125
|
+
public UpdateLocale(): void {
|
|
1126
|
+
|
|
1127
|
+
// don't assume default, always set
|
|
1128
|
+
|
|
1129
|
+
if (Localization.decimal_separator === ',') {
|
|
1130
|
+
this.parser.decimal_mark = DecimalMarkType.Comma;
|
|
1131
|
+
this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
this.parser.decimal_mark = DecimalMarkType.Period;
|
|
1135
|
+
this.parser.argument_separator = ArgumentSeparatorType.Comma;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// this.expression_calculator.UpdateLocale();
|
|
1139
|
+
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/* *
|
|
1143
|
+
* lookup in function library
|
|
1144
|
+
*
|
|
1145
|
+
* it seems like the only place this is called is within this class,
|
|
1146
|
+
* so we could probably inline and drop this function
|
|
1147
|
+
*
|
|
1148
|
+
* @deprecated
|
|
1149
|
+
* /
|
|
1150
|
+
public GetFunction(name: string): ExtendedFunctionDescriptor {
|
|
1151
|
+
return this.library.Get(name);
|
|
1152
|
+
}
|
|
1153
|
+
*/
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* returns a list of available functions, for AC/tooltips
|
|
1157
|
+
* FIXME: categories?
|
|
1158
|
+
* FIXME: need to separate annotation functions and sheet functions
|
|
1159
|
+
*/
|
|
1160
|
+
public SupportedFunctions(): FunctionDescriptor[] {
|
|
1161
|
+
|
|
1162
|
+
const list = this.library.List();
|
|
1163
|
+
|
|
1164
|
+
const function_list = Object.keys(list).map((key) => {
|
|
1165
|
+
let name = list[key].canonical_name;
|
|
1166
|
+
if (!name) name = key.replace(/_/g, '.');
|
|
1167
|
+
return {
|
|
1168
|
+
name,
|
|
1169
|
+
description: list[key].description,
|
|
1170
|
+
arguments: (list[key].arguments || []).map((argument) => {
|
|
1171
|
+
return { name: argument.name || '' };
|
|
1172
|
+
}),
|
|
1173
|
+
};
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
for (const macro of this.model.macro_functions.values()) {
|
|
1177
|
+
function_list.push({
|
|
1178
|
+
name: macro.name,
|
|
1179
|
+
description: macro.description,
|
|
1180
|
+
arguments: (macro.argument_names || []).map(argument => {
|
|
1181
|
+
return { name: argument };
|
|
1182
|
+
}),
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/*
|
|
1187
|
+
for (const key of Object.keys(this.model.macro_functions)) {
|
|
1188
|
+
const macro = this.model.macro_functions[key];
|
|
1189
|
+
function_list.push({
|
|
1190
|
+
name: macro.name,
|
|
1191
|
+
description: macro.description,
|
|
1192
|
+
arguments: (macro.argument_names || []).map(argument => {
|
|
1193
|
+
return { name: argument };
|
|
1194
|
+
}),
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
*/
|
|
1198
|
+
|
|
1199
|
+
return function_list;
|
|
1200
|
+
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
*
|
|
1205
|
+
* @param name
|
|
1206
|
+
* @param map
|
|
1207
|
+
*/
|
|
1208
|
+
public RegisterLibrary(name: string, map: FunctionMap): boolean {
|
|
1209
|
+
if (this.registered_libraries[name]) {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
this.RegisterFunction(map);
|
|
1213
|
+
this.registered_libraries[name] = true;
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* dynamic extension
|
|
1219
|
+
*/
|
|
1220
|
+
public RegisterFunction(map: FunctionMap): void {
|
|
1221
|
+
|
|
1222
|
+
for (const name of Object.keys(map)) {
|
|
1223
|
+
const descriptor = map[name];
|
|
1224
|
+
const original_function = descriptor.fn;
|
|
1225
|
+
|
|
1226
|
+
// we don't bind to the actual context because that would allow
|
|
1227
|
+
// functions to change it, and potentially break subsequent functions
|
|
1228
|
+
// that rely on it. which is a pretty far-fetched scenario, but we might
|
|
1229
|
+
// as well protect against it.
|
|
1230
|
+
|
|
1231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1232
|
+
descriptor.fn = (...args: any[]) => {
|
|
1233
|
+
return original_function.apply({
|
|
1234
|
+
address: { ...this.expression_calculator.context.address},
|
|
1235
|
+
}, args);
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
this.library.Register({[name]: descriptor});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* wrap the attachdata function so we can update the expression calculator
|
|
1245
|
+
* at the same time (we should unwind this a little bit, it's an artifact
|
|
1246
|
+
* of graph being a separate class)
|
|
1247
|
+
*/
|
|
1248
|
+
public AttachModel(): void {
|
|
1249
|
+
// this.RebuildMap();
|
|
1250
|
+
this.expression_calculator.SetModel(this.model);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* wrapper method for calculation
|
|
1255
|
+
*/
|
|
1256
|
+
public Calculate(subset?: Area): void {
|
|
1257
|
+
|
|
1258
|
+
this.AttachModel();
|
|
1259
|
+
|
|
1260
|
+
// this gets checked later, now... it would be better if we could
|
|
1261
|
+
// check it here are skip the later check, but that field is optional
|
|
1262
|
+
// it's better to report the error here so we can trace
|
|
1263
|
+
|
|
1264
|
+
if (subset && !subset.start.sheet_id) {
|
|
1265
|
+
throw new Error('CalculateInternal called with subset w/out sheet ID')
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (this.full_rebuild_required) {
|
|
1269
|
+
subset = undefined;
|
|
1270
|
+
this.UpdateAnnotations();
|
|
1271
|
+
// this.UpdateNotifiers();
|
|
1272
|
+
this.full_rebuild_required = false; // unset
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// this.expression_calculator.SetModel(model);
|
|
1276
|
+
|
|
1277
|
+
this.RebuildGraph(subset);
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
this.Recalculate();
|
|
1281
|
+
}
|
|
1282
|
+
catch (err){
|
|
1283
|
+
console.error(err);
|
|
1284
|
+
console.info('calculation error trapped');
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/*
|
|
1288
|
+
const callbacks: NotifierType[] = [];
|
|
1289
|
+
for (const notifier of this.notifiers) {
|
|
1290
|
+
if (notifier.vertex.state_id !== notifier.state) {
|
|
1291
|
+
notifier.state = notifier.vertex.state_id;
|
|
1292
|
+
if (notifier.notifier.callback) {
|
|
1293
|
+
callbacks.push(notifier.notifier);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (callbacks.length) {
|
|
1299
|
+
Promise.resolve().then(() => {
|
|
1300
|
+
for (const notifier of callbacks) {
|
|
1301
|
+
if (notifier.callback) {
|
|
1302
|
+
notifier.callback.call(undefined, notifier);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
*/
|
|
1308
|
+
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* resets graph and graph status. this is called when structure changes --
|
|
1313
|
+
* such as adding or removing sheets -- so we need to preserve notifiers
|
|
1314
|
+
* across resets. we need to either add a flag or add a separate method
|
|
1315
|
+
* to handle clearing notifiers.
|
|
1316
|
+
*/
|
|
1317
|
+
public Reset(): void {
|
|
1318
|
+
|
|
1319
|
+
this.FlushTree();
|
|
1320
|
+
this.AttachModel();
|
|
1321
|
+
|
|
1322
|
+
this.full_rebuild_required = true;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* get a list of functions that require decorating with "_xlfn" on
|
|
1327
|
+
* export. the embed caller will pass this to the export worker.
|
|
1328
|
+
* since we manage functions, we can manage the list.
|
|
1329
|
+
*
|
|
1330
|
+
* UPDATE: to support our MC functions (which may need _xll decoration),
|
|
1331
|
+
* map to type and then overload as necessary
|
|
1332
|
+
*
|
|
1333
|
+
*/
|
|
1334
|
+
public DecoratedFunctionList(): Record<string, string> {
|
|
1335
|
+
// const list: string[] = [];
|
|
1336
|
+
const map: Record<string, string> = {};
|
|
1337
|
+
|
|
1338
|
+
const lib = this.library.List();
|
|
1339
|
+
for (const key of Object.keys(lib)) {
|
|
1340
|
+
const def = lib[key];
|
|
1341
|
+
if (def.xlfn) {
|
|
1342
|
+
// list.push(key);
|
|
1343
|
+
map[key] = '_xlfn';
|
|
1344
|
+
}
|
|
1345
|
+
else if (def.extension) {
|
|
1346
|
+
map[key] = '_xll';
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return map;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/** wrapper method ensures it always returns an Area (instance, not interface) */
|
|
1354
|
+
public ResolveArea(address: string|ICellAddress|IArea, active_sheet: Sheet): Area {
|
|
1355
|
+
const resolved = this.ResolveAddress(address, active_sheet);
|
|
1356
|
+
return IsCellAddress(resolved) ? new Area(resolved) : new Area(resolved.start, resolved.end);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* moved from embedded sheet. also modified to preserve ranges, so it
|
|
1361
|
+
* might return a range (area). if you are expecting the old behavior
|
|
1362
|
+
* you need to check (perhaps we could have a wrapper, or make it optional?)
|
|
1363
|
+
*
|
|
1364
|
+
* Q: why does this not go in grid? or model? (...)
|
|
1365
|
+
* Q: why are we not preserving absoute/relative? (...)
|
|
1366
|
+
*
|
|
1367
|
+
*/
|
|
1368
|
+
public ResolveAddress(address: string|ICellAddress|IArea, active_sheet: Sheet): ICellAddress|IArea {
|
|
1369
|
+
|
|
1370
|
+
if (typeof address === 'string') {
|
|
1371
|
+
const parse_result = this.parser.Parse(address);
|
|
1372
|
+
if (parse_result.expression && parse_result.expression.type === 'address') {
|
|
1373
|
+
this.ResolveSheetID(parse_result.expression, undefined, active_sheet);
|
|
1374
|
+
return {
|
|
1375
|
+
row: parse_result.expression.row,
|
|
1376
|
+
column: parse_result.expression.column,
|
|
1377
|
+
sheet_id: parse_result.expression.sheet_id,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
else if (parse_result.expression && parse_result.expression.type === 'range') {
|
|
1381
|
+
this.ResolveSheetID(parse_result.expression, undefined, active_sheet);
|
|
1382
|
+
return {
|
|
1383
|
+
start: {
|
|
1384
|
+
row: parse_result.expression.start.row,
|
|
1385
|
+
column: parse_result.expression.start.column,
|
|
1386
|
+
sheet_id: parse_result.expression.start.sheet_id,
|
|
1387
|
+
},
|
|
1388
|
+
end: {
|
|
1389
|
+
row: parse_result.expression.end.row,
|
|
1390
|
+
column: parse_result.expression.end.column,
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
else if (parse_result.expression && parse_result.expression.type === 'identifier') {
|
|
1395
|
+
|
|
1396
|
+
// is named range guaranteed to have a sheet ID? (I think yes...)
|
|
1397
|
+
|
|
1398
|
+
const named_range = this.model.named_ranges.Get(parse_result.expression.name);
|
|
1399
|
+
if (named_range) {
|
|
1400
|
+
return named_range;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
return { row: 0, column: 0 }; // default for string types -- broken
|
|
1405
|
+
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return address; // already range or address
|
|
1409
|
+
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** moved from embedded sheet */
|
|
1413
|
+
public Evaluate(expression: string, active_sheet?: Sheet, options: EvaluateOptions = {}) {
|
|
1414
|
+
|
|
1415
|
+
const current = this.parser.argument_separator;
|
|
1416
|
+
const r1c1_state = this.parser.flags.r1c1;
|
|
1417
|
+
|
|
1418
|
+
if (options.argument_separator) {
|
|
1419
|
+
if (options.argument_separator === ',') {
|
|
1420
|
+
this.parser.argument_separator = ArgumentSeparatorType.Comma;
|
|
1421
|
+
this.parser.decimal_mark = DecimalMarkType.Period;
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
|
|
1425
|
+
this.parser.decimal_mark = DecimalMarkType.Comma;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (options.r1c1) {
|
|
1430
|
+
this.parser.flags.r1c1 = options.r1c1;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const parse_result = this.parser.Parse(expression);
|
|
1434
|
+
|
|
1435
|
+
// reset
|
|
1436
|
+
|
|
1437
|
+
this.parser.argument_separator = current;
|
|
1438
|
+
this.parser.decimal_mark = (current === ArgumentSeparatorType.Comma) ? DecimalMarkType.Period : DecimalMarkType.Comma;
|
|
1439
|
+
this.parser.flags.r1c1 = r1c1_state;
|
|
1440
|
+
|
|
1441
|
+
// OK
|
|
1442
|
+
|
|
1443
|
+
if (parse_result && parse_result.expression ){
|
|
1444
|
+
|
|
1445
|
+
this.parser.Walk(parse_result.expression, (unit) => {
|
|
1446
|
+
if (unit.type === 'address' || unit.type === 'range') {
|
|
1447
|
+
|
|
1448
|
+
// don't allow offset references, even in R1C1
|
|
1449
|
+
if (unit.type === 'address') {
|
|
1450
|
+
if (unit.offset_column || unit.offset_row) {
|
|
1451
|
+
throw new Error(`Evaluate does not support offset references`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
if (unit.start.offset_column || unit.start.offset_row || unit.end.offset_column || unit.end.offset_row) {
|
|
1456
|
+
throw new Error(`Evaluate does not support offset references`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
this.ResolveSheetID(unit, undefined, active_sheet);
|
|
1461
|
+
}
|
|
1462
|
+
return true;
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
// console.info({expression: parse_result.expression})
|
|
1466
|
+
const result = this.CalculateExpression(parse_result.expression);
|
|
1467
|
+
|
|
1468
|
+
if (result.type === ValueType.array) {
|
|
1469
|
+
return result.value.map(row => row.map(value => value.value));
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
return result.value;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// or? (...)
|
|
1478
|
+
|
|
1479
|
+
if (parse_result.error) {
|
|
1480
|
+
throw new Error(parse_result.error);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
throw new Error('invalid expression');
|
|
1484
|
+
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* calculate an expression, optionally setting a fake cell address.
|
|
1489
|
+
* this may have weird side-effects.
|
|
1490
|
+
*/
|
|
1491
|
+
public CalculateExpression(
|
|
1492
|
+
expression: ExpressionUnit,
|
|
1493
|
+
address: ICellAddress = {row: -1, column: -1},
|
|
1494
|
+
preserve_flags = false): UnionValue {
|
|
1495
|
+
|
|
1496
|
+
return this.expression_calculator.Calculate(expression, address, preserve_flags).value; // dropping volatile flag
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* rebuild the graph, and set cells as clean. the vertices need internal
|
|
1501
|
+
* references to the calculated value, so that's set via the vertex method.
|
|
1502
|
+
*
|
|
1503
|
+
* we also need to manage the list of volatile cells, which is normally
|
|
1504
|
+
* built as a side-effect of calculation.
|
|
1505
|
+
*
|
|
1506
|
+
* UPDATE: optionally recalculate if there are volatile cells. that's used
|
|
1507
|
+
* for loading documents.
|
|
1508
|
+
*/
|
|
1509
|
+
public RebuildClean(recalculate_if_volatile = false): void {
|
|
1510
|
+
|
|
1511
|
+
this.full_rebuild_required = false; // unset
|
|
1512
|
+
|
|
1513
|
+
this.AttachModel();
|
|
1514
|
+
|
|
1515
|
+
this.RebuildGraph();
|
|
1516
|
+
|
|
1517
|
+
// add leaf vertices for annotations
|
|
1518
|
+
|
|
1519
|
+
this.UpdateAnnotations(); // all
|
|
1520
|
+
|
|
1521
|
+
// and notifiers
|
|
1522
|
+
|
|
1523
|
+
// this.UpdateNotifiers();
|
|
1524
|
+
|
|
1525
|
+
// there's a weird back-and-forth that happens here
|
|
1526
|
+
// (calculator -> graph -> calculator) to check for volatile
|
|
1527
|
+
// cells. it could probably be simplified.
|
|
1528
|
+
|
|
1529
|
+
this.InitializeGraph();
|
|
1530
|
+
|
|
1531
|
+
if (recalculate_if_volatile && this.volatile_list.length) {
|
|
1532
|
+
this.Recalculate();
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* remove duplicates from list, dropping absolute
|
|
1539
|
+
*/
|
|
1540
|
+
public FlattenCellList(list: ICellAddress[]): ICellAddress[] {
|
|
1541
|
+
|
|
1542
|
+
const map: {[index: string]: string} = {};
|
|
1543
|
+
const flattened: ICellAddress[] = [];
|
|
1544
|
+
|
|
1545
|
+
for (const entry of list) {
|
|
1546
|
+
const address = {
|
|
1547
|
+
column: entry.column,
|
|
1548
|
+
row: entry.row,
|
|
1549
|
+
sheet_id: entry.sheet_id,
|
|
1550
|
+
};
|
|
1551
|
+
const label = Area.CellAddressToLabel(address, true);
|
|
1552
|
+
if (map[label]) { continue; }
|
|
1553
|
+
map[label] = label;
|
|
1554
|
+
flattened.push(address);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
return flattened;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
/* * remove all notifiers * /
|
|
1562
|
+
public RemoveNotifiers(): void {
|
|
1563
|
+
for (const internal of this.notifiers) {
|
|
1564
|
+
if (internal.vertex) {
|
|
1565
|
+
internal.vertex.Reset();
|
|
1566
|
+
this.RemoveLeafVertex(internal.vertex);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
this.notifiers = [];
|
|
1570
|
+
}
|
|
1571
|
+
*/
|
|
1572
|
+
|
|
1573
|
+
/* *
|
|
1574
|
+
* remove specified notifier. you can pass the returned ID or the original
|
|
1575
|
+
* object used to create it.
|
|
1576
|
+
* /
|
|
1577
|
+
public RemoveNotifier(notifier: NotifierType|number): void {
|
|
1578
|
+
|
|
1579
|
+
let internal: InternalNotifierType|undefined;
|
|
1580
|
+
|
|
1581
|
+
this.notifiers = this.notifiers.filter(test => {
|
|
1582
|
+
if (test.id === notifier || test === notifier) {
|
|
1583
|
+
internal = test;
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
return true;
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
if (!internal) {
|
|
1590
|
+
// FIXME: error
|
|
1591
|
+
console.warn('invalid notifier');
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
|
|
1595
|
+
// remove vertex
|
|
1596
|
+
if (internal.vertex) {
|
|
1597
|
+
internal.vertex.Reset();
|
|
1598
|
+
this.RemoveLeafVertex(internal.vertex);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
}
|
|
1604
|
+
*/
|
|
1605
|
+
|
|
1606
|
+
/* *
|
|
1607
|
+
* update a notifier or notifiers, or the entire list (default).
|
|
1608
|
+
* /
|
|
1609
|
+
protected UpdateNotifiers(notifiers: InternalNotifierType|InternalNotifierType[] = this.notifiers): void {
|
|
1610
|
+
|
|
1611
|
+
if (!Array.isArray(notifiers)) {
|
|
1612
|
+
notifiers = [notifiers];
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
for (const notifier of notifiers) {
|
|
1616
|
+
|
|
1617
|
+
if (notifier.vertex) {
|
|
1618
|
+
notifier.vertex.Reset();
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
notifier.vertex = new LeafVertex();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// construct formula (inlining)
|
|
1625
|
+
|
|
1626
|
+
const string_reference = notifier.references.map(reference => {
|
|
1627
|
+
|
|
1628
|
+
// I don't want to go through strings here... OTOH if we build an
|
|
1629
|
+
// expression manually it's going to be fragile to changes in the
|
|
1630
|
+
// parser...
|
|
1631
|
+
|
|
1632
|
+
let sheet_name = '';
|
|
1633
|
+
let base: ICellAddress;
|
|
1634
|
+
let label = '';
|
|
1635
|
+
|
|
1636
|
+
if (reference.count === 1) {
|
|
1637
|
+
base = reference.start;
|
|
1638
|
+
label = Area.CellAddressToLabel(reference.start, false);
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
base = reference.start;
|
|
1642
|
+
label = Area.CellAddressToLabel(reference.start, false) + ':' +
|
|
1643
|
+
Area.CellAddressToLabel(reference.end, false);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
for (const sheet of this.model.sheets.list) {
|
|
1647
|
+
if (sheet.id === base.sheet_id) {
|
|
1648
|
+
sheet_name = sheet.name;
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (!sheet_name) {
|
|
1654
|
+
throw new Error('invalid sheet in reference');
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
if (QuotedSheetNameRegex.test(sheet_name)) {
|
|
1658
|
+
return `'${sheet_name}'!${label}`;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
return `${sheet_name}!${label}`;
|
|
1662
|
+
|
|
1663
|
+
}).join(',');
|
|
1664
|
+
|
|
1665
|
+
// the function (here "Notify") is never called. we're using a leaf
|
|
1666
|
+
// node, which bypasses the standard calculation system and only updates
|
|
1667
|
+
// a state reference when dirty. so here it's just an arbitrary string.
|
|
1668
|
+
|
|
1669
|
+
// still, we should use something that's not going to be used elsewhere
|
|
1670
|
+
// in the future...
|
|
1671
|
+
|
|
1672
|
+
const formula = `=Internal.Notify(${string_reference})`;
|
|
1673
|
+
// console.info('f', formula);
|
|
1674
|
+
|
|
1675
|
+
// we (theoretically) guarantee that all refeerences are qualified,
|
|
1676
|
+
// so we don't need a context (active sheet) for relative references.
|
|
1677
|
+
// we can just use model[0]
|
|
1678
|
+
|
|
1679
|
+
this.AddLeafVertex(notifier.vertex);
|
|
1680
|
+
this.UpdateLeafVertex(notifier.vertex, formula, this.model.sheets.list[0]);
|
|
1681
|
+
|
|
1682
|
+
// update state (gets reset?)
|
|
1683
|
+
|
|
1684
|
+
notifier.state = notifier.vertex.state_id;
|
|
1685
|
+
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
*/
|
|
1689
|
+
|
|
1690
|
+
/* *
|
|
1691
|
+
* new notification API (testing)
|
|
1692
|
+
* /
|
|
1693
|
+
public AddNotifier(references: RangeReference|RangeReference[], notifier: NotifierType, context: Sheet): number {
|
|
1694
|
+
|
|
1695
|
+
if (!Array.isArray(references)) {
|
|
1696
|
+
references = [references];
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// even if these are strings we want to properly resolve them so
|
|
1700
|
+
// we can store qualified references
|
|
1701
|
+
|
|
1702
|
+
const qualified: Area[] = references.map(reference => {
|
|
1703
|
+
|
|
1704
|
+
if (typeof reference === 'string') {
|
|
1705
|
+
return this.ResolveArea(reference, context).Clone();
|
|
1706
|
+
}
|
|
1707
|
+
if (IsCellAddress(reference)) {
|
|
1708
|
+
return new Area({
|
|
1709
|
+
...reference,
|
|
1710
|
+
sheet_id: reference.sheet_id || context.id,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
return new Area({
|
|
1715
|
+
...reference.start,
|
|
1716
|
+
sheet_id: reference.start.sheet_id || context.id,
|
|
1717
|
+
}, {
|
|
1718
|
+
...reference.end,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
const internal: InternalNotifierType = {
|
|
1724
|
+
id: this.notifier_id_source++,
|
|
1725
|
+
notifier,
|
|
1726
|
+
references: qualified,
|
|
1727
|
+
vertex: new LeafVertex(),
|
|
1728
|
+
state: 0,
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
// update
|
|
1732
|
+
this.UpdateNotifiers(internal);
|
|
1733
|
+
|
|
1734
|
+
// push to notifications
|
|
1735
|
+
this.notifiers.push(internal);
|
|
1736
|
+
|
|
1737
|
+
return internal.id;
|
|
1738
|
+
|
|
1739
|
+
}
|
|
1740
|
+
*/
|
|
1741
|
+
|
|
1742
|
+
public RemoveAnnotation(annotation: Annotation): void {
|
|
1743
|
+
const vertex = (annotation.temp.vertex as LeafVertex);
|
|
1744
|
+
if (!vertex) { return; }
|
|
1745
|
+
vertex.Reset();
|
|
1746
|
+
this.RemoveLeafVertex(vertex);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
public UpdateAnnotations(list?: Annotation|Annotation[], context?: Sheet): void {
|
|
1750
|
+
|
|
1751
|
+
if (!list) {
|
|
1752
|
+
|
|
1753
|
+
// update: since we don't have access to active_sheet,
|
|
1754
|
+
// just add all annotations. slightly less efficient
|
|
1755
|
+
// (perhaps) but better for handling multiple views.
|
|
1756
|
+
|
|
1757
|
+
for (const sheet of this.model.sheets.list) {
|
|
1758
|
+
this.UpdateAnnotations(sheet.annotations, sheet);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
return;
|
|
1762
|
+
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (!context) {
|
|
1766
|
+
throw new Error('invalid call to UpdateAnnotations with list but no sheet');
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (typeof list !== 'undefined' && !Array.isArray(list)) {
|
|
1770
|
+
list = [list];
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
for (const entry of list) {
|
|
1774
|
+
if (entry.formula) {
|
|
1775
|
+
if (!entry.temp.vertex) {
|
|
1776
|
+
entry.temp.vertex = new LeafVertex();
|
|
1777
|
+
}
|
|
1778
|
+
const vertex = entry.temp.vertex as LeafVertex;
|
|
1779
|
+
this.AddLeafVertex(vertex);
|
|
1780
|
+
this.UpdateLeafVertex(vertex, entry.formula, context);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* returns false if the sheet cannot be resolved, which probably
|
|
1788
|
+
* means the name changed (that's the case we are working on with
|
|
1789
|
+
* this fix).
|
|
1790
|
+
*/
|
|
1791
|
+
public ResolveSheetID(expr: UnitAddress|UnitRange, context?: ICellAddress, active_sheet?: Sheet): boolean {
|
|
1792
|
+
|
|
1793
|
+
const target = expr.type === 'address' ? expr : expr.start;
|
|
1794
|
+
|
|
1795
|
+
if (target.sheet_id) {
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (target.sheet) {
|
|
1800
|
+
const sheet = this.model.sheets.Find(target.sheet);
|
|
1801
|
+
if (sheet) {
|
|
1802
|
+
target.sheet_id = sheet.id;
|
|
1803
|
+
return true;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/*
|
|
1807
|
+
const lc = target.sheet.toLowerCase();
|
|
1808
|
+
for (const sheet of this.model.sheets.list) {
|
|
1809
|
+
if (sheet.name.toLowerCase() === lc) {
|
|
1810
|
+
target.sheet_id = sheet.id;
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
*/
|
|
1815
|
+
}
|
|
1816
|
+
else if (context?.sheet_id) {
|
|
1817
|
+
target.sheet_id = context.sheet_id;
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
else if (active_sheet?.id) {
|
|
1821
|
+
target.sheet_id = active_sheet.id;
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return false; // the error
|
|
1826
|
+
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// --- protected -------------------------------------------------------------
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* assuming the expression is an address, range, or named range, resolve
|
|
1833
|
+
* to an address/area. returns undefined if the expression can't be resolved.
|
|
1834
|
+
*/
|
|
1835
|
+
protected ResolveExpressionAddress(expr: ExpressionUnit, context?: ICellAddress): Area|undefined {
|
|
1836
|
+
|
|
1837
|
+
switch (expr.type) {
|
|
1838
|
+
case 'address':
|
|
1839
|
+
if (this.ResolveSheetID(expr, context)) {
|
|
1840
|
+
return new Area(expr);
|
|
1841
|
+
}
|
|
1842
|
+
break;
|
|
1843
|
+
|
|
1844
|
+
case 'range':
|
|
1845
|
+
if (this.ResolveSheetID(expr, context)) {
|
|
1846
|
+
return new Area(expr.start, expr.end);
|
|
1847
|
+
}
|
|
1848
|
+
break;
|
|
1849
|
+
|
|
1850
|
+
case 'identifier':
|
|
1851
|
+
{
|
|
1852
|
+
const named_range =
|
|
1853
|
+
this.model.named_ranges.Get(expr.name.toUpperCase());
|
|
1854
|
+
if (named_range) {
|
|
1855
|
+
return new Area(named_range.start, named_range.end);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return undefined;
|
|
1862
|
+
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
protected NamedRangeToAddressUnit(unit: UnitIdentifier): UnitAddress|UnitRange|undefined {
|
|
1866
|
+
|
|
1867
|
+
const normalized = unit.name.toUpperCase();
|
|
1868
|
+
const named_range = this.model.named_ranges.Get(normalized);
|
|
1869
|
+
if (named_range) {
|
|
1870
|
+
if (named_range.count === 1) {
|
|
1871
|
+
return this.ConstructAddressUnit(named_range.start, normalized, unit.id, unit.position);
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
return {
|
|
1875
|
+
type: 'range',
|
|
1876
|
+
start: this.ConstructAddressUnit(named_range.start, normalized, unit.id, unit.position),
|
|
1877
|
+
end: this.ConstructAddressUnit(named_range.end, normalized, unit.id, unit.position),
|
|
1878
|
+
label: normalized,
|
|
1879
|
+
id: unit.id,
|
|
1880
|
+
position: unit.position,
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
return undefined;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/** named range support */
|
|
1889
|
+
protected ConstructAddressUnit(address: ICellAddress, label: string, id: number, position: number): UnitAddress {
|
|
1890
|
+
return {
|
|
1891
|
+
type: 'address',
|
|
1892
|
+
row: address.row,
|
|
1893
|
+
column: address.column,
|
|
1894
|
+
sheet_id: address.sheet_id,
|
|
1895
|
+
label,
|
|
1896
|
+
id,
|
|
1897
|
+
position,
|
|
1898
|
+
} as UnitAddress;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* rebuild dependencies for a single expression (might be a cell, or an
|
|
1903
|
+
* annotation/leaf node). can recurse on elements, so the return value
|
|
1904
|
+
* is passed through. the first (outer) call can just leave it blank and
|
|
1905
|
+
* use the return value.
|
|
1906
|
+
*
|
|
1907
|
+
* we're adding the sheet name so that (in mc expression calculator) we
|
|
1908
|
+
* can turn address parameters into qualified labels. the normal routine
|
|
1909
|
+
* will just use the ID as the name, that's fine, as long as it's unique
|
|
1910
|
+
* (which it is).
|
|
1911
|
+
*
|
|
1912
|
+
* this might cause issues if we ever try to actually resolve from the
|
|
1913
|
+
* sheet name, though, so (...)
|
|
1914
|
+
*/
|
|
1915
|
+
protected RebuildDependencies(
|
|
1916
|
+
unit: ExpressionUnit,
|
|
1917
|
+
relative_sheet_id: number,
|
|
1918
|
+
relative_sheet_name: string,
|
|
1919
|
+
dependencies: DependencyList = {addresses: {}, ranges: {}},
|
|
1920
|
+
context_address: ICellAddress,
|
|
1921
|
+
): DependencyList {
|
|
1922
|
+
|
|
1923
|
+
if (!relative_sheet_name) {
|
|
1924
|
+
const sheet = this.model.sheets.Find(relative_sheet_id);
|
|
1925
|
+
if (sheet) {
|
|
1926
|
+
relative_sheet_name = sheet.name;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
switch (unit.type){
|
|
1931
|
+
|
|
1932
|
+
case 'literal':
|
|
1933
|
+
case 'missing':
|
|
1934
|
+
case 'operator':
|
|
1935
|
+
break;
|
|
1936
|
+
|
|
1937
|
+
case 'identifier':
|
|
1938
|
+
{
|
|
1939
|
+
// update to handle named expressions. just descend into
|
|
1940
|
+
// the expression as if it were inline.
|
|
1941
|
+
|
|
1942
|
+
const normalized = unit.name.toUpperCase();
|
|
1943
|
+
|
|
1944
|
+
if (this.model.named_expressions.has(normalized)) {
|
|
1945
|
+
const expr = this.model.named_expressions.get(normalized);
|
|
1946
|
+
if (expr) {
|
|
1947
|
+
this.RebuildDependencies(expr, relative_sheet_id, relative_sheet_name, dependencies, context_address);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
else {
|
|
1951
|
+
const resolved = this.NamedRangeToAddressUnit(unit);
|
|
1952
|
+
if (resolved) {
|
|
1953
|
+
if (resolved.type === 'address') {
|
|
1954
|
+
dependencies.addresses[resolved.label] = resolved;
|
|
1955
|
+
}
|
|
1956
|
+
else {
|
|
1957
|
+
dependencies.ranges[resolved.label] = resolved;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
break;
|
|
1963
|
+
|
|
1964
|
+
case 'structured-reference':
|
|
1965
|
+
|
|
1966
|
+
// when building the graph, resolve the reference to the table.
|
|
1967
|
+
// this is the same thing we do in expression-calculator, and
|
|
1968
|
+
// we rely on the same rules to ensure that the reference either
|
|
1969
|
+
// stays consitent, or gets rebuilt.
|
|
1970
|
+
|
|
1971
|
+
{
|
|
1972
|
+
const resolved = this.model.ResolveStructuredReference(unit, context_address);
|
|
1973
|
+
if (resolved) {
|
|
1974
|
+
if (resolved.type === 'address') {
|
|
1975
|
+
dependencies.addresses[resolved.sheet_id + '!' + resolved.label] = resolved;
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
dependencies.ranges[resolved.label] = resolved;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
const table = this.model.tables.get(unit.table.toLowerCase());
|
|
1984
|
+
if (table) {
|
|
1985
|
+
|
|
1986
|
+
// see ResolveStructuredReference in expression calculator
|
|
1987
|
+
|
|
1988
|
+
const row = context_address.row; // "this row"
|
|
1989
|
+
if (row < table.area.start.row || row > table.area.end.row) {
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const reference_column = unit.column.toLowerCase();
|
|
1994
|
+
let column = -1;
|
|
1995
|
+
|
|
1996
|
+
if (table.columns) { // FIXME: make this required
|
|
1997
|
+
for (let i = 0; i < table.columns.length; i++) {
|
|
1998
|
+
if (reference_column === table.columns[i]) {
|
|
1999
|
+
column = table.area.start.column + i;
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
if (column >= 0) {
|
|
2006
|
+
|
|
2007
|
+
// does using the original label here, instead of a sheet
|
|
2008
|
+
// address as label, mean we potentially have multiple
|
|
2009
|
+
// references to the same cell? probably...
|
|
2010
|
+
|
|
2011
|
+
const address: UnitAddress = {
|
|
2012
|
+
label: unit.label,
|
|
2013
|
+
type: 'address',
|
|
2014
|
+
row,
|
|
2015
|
+
column,
|
|
2016
|
+
sheet_id: table.area.start.sheet_id,
|
|
2017
|
+
id: unit.id,
|
|
2018
|
+
position: unit.position,
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
dependencies.addresses[address.sheet_id + '!' + address.label] = address;
|
|
2022
|
+
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
break;
|
|
2029
|
+
|
|
2030
|
+
case 'address':
|
|
2031
|
+
|
|
2032
|
+
if (!unit.sheet_id) {
|
|
2033
|
+
if (unit.sheet) {
|
|
2034
|
+
const sheet = this.model.sheets.Find(unit.sheet);
|
|
2035
|
+
if (sheet) {
|
|
2036
|
+
unit.sheet_id = sheet.id;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
unit.sheet_id = relative_sheet_id;
|
|
2041
|
+
unit.sheet = relative_sheet_name;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/*
|
|
2045
|
+
unit.sheet_id = unit.sheet ?
|
|
2046
|
+
(sheet_name_map[unit.sheet.toLowerCase()] || 0) :
|
|
2047
|
+
relative_sheet_id;
|
|
2048
|
+
if (!unit.sheet) { unit.sheet = relative_sheet_name; }
|
|
2049
|
+
*/
|
|
2050
|
+
|
|
2051
|
+
}
|
|
2052
|
+
if (!unit.sheet_id) {
|
|
2053
|
+
|
|
2054
|
+
// FIXME: we don't necessarily need to warn here, because we'll
|
|
2055
|
+
// get a warning when it tries to calculate. still this is helpful
|
|
2056
|
+
// for debugging.
|
|
2057
|
+
|
|
2058
|
+
console.warn('invalid address in range [9d]');
|
|
2059
|
+
}
|
|
2060
|
+
else {
|
|
2061
|
+
dependencies.addresses[unit.sheet_id + '!' + unit.label] = unit;
|
|
2062
|
+
}
|
|
2063
|
+
break; // this.AddressLabel(unit, offset);
|
|
2064
|
+
|
|
2065
|
+
case 'range':
|
|
2066
|
+
if (!unit.start.sheet_id) {
|
|
2067
|
+
if (unit.start.sheet) {
|
|
2068
|
+
const sheet = this.model.sheets.Find(unit.start.sheet);
|
|
2069
|
+
if (sheet) {
|
|
2070
|
+
unit.start.sheet_id = sheet.id;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
unit.start.sheet_id = relative_sheet_id;
|
|
2075
|
+
unit.start.sheet = relative_sheet_name;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/*
|
|
2079
|
+
unit.start.sheet_id = unit.start.sheet ?
|
|
2080
|
+
(sheet_name_map[unit.start.sheet.toLowerCase()] || 0) :
|
|
2081
|
+
relative_sheet_id;
|
|
2082
|
+
if (!unit.start.sheet) { unit.start.sheet = relative_sheet_name; }
|
|
2083
|
+
*/
|
|
2084
|
+
|
|
2085
|
+
}
|
|
2086
|
+
if (!unit.start.sheet_id) {
|
|
2087
|
+
|
|
2088
|
+
// see above in the address handler
|
|
2089
|
+
|
|
2090
|
+
console.warn('invalid sheet in range', unit);
|
|
2091
|
+
}
|
|
2092
|
+
else {
|
|
2093
|
+
dependencies.ranges[unit.start.sheet_id + '!' + unit.start.label + ':' + unit.end.label] = unit;
|
|
2094
|
+
}
|
|
2095
|
+
break;
|
|
2096
|
+
|
|
2097
|
+
case 'unary':
|
|
2098
|
+
this.RebuildDependencies(unit.operand, relative_sheet_id, relative_sheet_name, dependencies, context_address);//, sheet_name_map);
|
|
2099
|
+
break;
|
|
2100
|
+
|
|
2101
|
+
case 'binary':
|
|
2102
|
+
this.RebuildDependencies(unit.left, relative_sheet_id, relative_sheet_name, dependencies, context_address);//, sheet_name_map);
|
|
2103
|
+
this.RebuildDependencies(unit.right, relative_sheet_id, relative_sheet_name, dependencies, context_address);//, sheet_name_map);
|
|
2104
|
+
break;
|
|
2105
|
+
|
|
2106
|
+
case 'group':
|
|
2107
|
+
unit.elements.forEach((element) =>
|
|
2108
|
+
this.RebuildDependencies(element, relative_sheet_id, relative_sheet_name, dependencies, context_address));//, sheet_name_map));
|
|
2109
|
+
break;
|
|
2110
|
+
|
|
2111
|
+
case 'call':
|
|
2112
|
+
|
|
2113
|
+
// this is where we diverge. if there's a known function that has
|
|
2114
|
+
// an "address" parameter, we don't treat it as a dependency. this is
|
|
2115
|
+
// to support our weird MV syntax (weird here, but useful in Excel).
|
|
2116
|
+
|
|
2117
|
+
// UPDATE: this is broadly useful for some other functions, like OFFSET.
|
|
2118
|
+
{
|
|
2119
|
+
const args: ExpressionUnit[] = unit.args.slice(0);
|
|
2120
|
+
const func = this.library.Get(unit.name);
|
|
2121
|
+
if (func && func.arguments){
|
|
2122
|
+
func.arguments.forEach((descriptor, index) => {
|
|
2123
|
+
if (descriptor && descriptor.address) {
|
|
2124
|
+
|
|
2125
|
+
// we still want to fix sheet addresses, though, even if we're
|
|
2126
|
+
// not tracking the dependency. to do that, we can recurse with
|
|
2127
|
+
// a new (empty) dependency list, and just drop the new list
|
|
2128
|
+
|
|
2129
|
+
this.RebuildDependencies(args[index], relative_sheet_id, relative_sheet_name, undefined, context_address);//, sheet_name_map);
|
|
2130
|
+
|
|
2131
|
+
args[index] = { type: 'missing', id: -1 };
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
args.forEach((arg) => this.RebuildDependencies(arg, relative_sheet_id, relative_sheet_name, dependencies, context_address));//, sheet_name_map));
|
|
2136
|
+
|
|
2137
|
+
}
|
|
2138
|
+
break;
|
|
2139
|
+
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
return dependencies;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
protected UpdateLeafVertex(vertex: LeafVertex, formula: string, context: Sheet): void {
|
|
2146
|
+
|
|
2147
|
+
vertex.Reset();
|
|
2148
|
+
|
|
2149
|
+
const parse_result = this.parser.Parse(formula);
|
|
2150
|
+
if (parse_result.expression) {
|
|
2151
|
+
const dependencies =
|
|
2152
|
+
this.RebuildDependencies(
|
|
2153
|
+
parse_result.expression,
|
|
2154
|
+
// this.model.active_sheet.id,
|
|
2155
|
+
// this.model.active_sheet.name,
|
|
2156
|
+
context.id,
|
|
2157
|
+
context.name,
|
|
2158
|
+
undefined,
|
|
2159
|
+
{row: 0, column: 0}, // fake context
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
for (const key of Object.keys(dependencies.ranges)){
|
|
2163
|
+
const unit = dependencies.ranges[key];
|
|
2164
|
+
const range = new Area(unit.start, unit.end);
|
|
2165
|
+
|
|
2166
|
+
range.Iterate((address: ICellAddress) => {
|
|
2167
|
+
this.AddLeafVertexEdge(address, vertex);
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
/*
|
|
2171
|
+
for (const address of range) {
|
|
2172
|
+
this.AddLeafVertexEdge(address, vertex);
|
|
2173
|
+
}
|
|
2174
|
+
*/
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
for (const key of Object.keys(dependencies.addresses)){
|
|
2178
|
+
const address = dependencies.addresses[key];
|
|
2179
|
+
this.AddLeafVertexEdge(address, vertex);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
vertex.expression = parse_result.expression || {type: 'missing', id: -1};
|
|
2185
|
+
vertex.expression_error = !parse_result.valid;
|
|
2186
|
+
|
|
2187
|
+
// vertex.UpdateState();
|
|
2188
|
+
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/* *
|
|
2192
|
+
* we're passing model here to skip the test on each call
|
|
2193
|
+
*
|
|
2194
|
+
* @param unit
|
|
2195
|
+
* @param model
|
|
2196
|
+
* /
|
|
2197
|
+
protected ApplyMacroFunctionInternal(
|
|
2198
|
+
unit: ExpressionUnit,
|
|
2199
|
+
model: DataModel,
|
|
2200
|
+
name_stack: Array<{[index: string]: ExpressionUnit}>,
|
|
2201
|
+
): ExpressionUnit {
|
|
2202
|
+
|
|
2203
|
+
switch (unit.type) {
|
|
2204
|
+
|
|
2205
|
+
case 'identifier':
|
|
2206
|
+
if (name_stack[0]) {
|
|
2207
|
+
const value = name_stack[0][(unit.name || '').toUpperCase()];
|
|
2208
|
+
if (value) {
|
|
2209
|
+
return JSON.parse(JSON.stringify(value)) as ExpressionUnit;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
break;
|
|
2213
|
+
|
|
2214
|
+
case 'binary':
|
|
2215
|
+
unit.left = this.ApplyMacroFunctionInternal(unit.left, model, name_stack);
|
|
2216
|
+
unit.right = this.ApplyMacroFunctionInternal(unit.right, model, name_stack);
|
|
2217
|
+
break;
|
|
2218
|
+
|
|
2219
|
+
case 'unary':
|
|
2220
|
+
unit.operand = this.ApplyMacroFunctionInternal(unit.operand, model, name_stack);
|
|
2221
|
+
break;
|
|
2222
|
+
|
|
2223
|
+
case 'group':
|
|
2224
|
+
unit.elements = unit.elements.map(element => this.ApplyMacroFunctionInternal(element, model, name_stack));
|
|
2225
|
+
break;
|
|
2226
|
+
|
|
2227
|
+
case 'call':
|
|
2228
|
+
{
|
|
2229
|
+
// do this first, so we can pass through directly
|
|
2230
|
+
unit.args = unit.args.map(arg => this.ApplyMacroFunctionInternal(arg, model, name_stack));
|
|
2231
|
+
|
|
2232
|
+
const func = this.library.Get(unit.name);
|
|
2233
|
+
if (!func) {
|
|
2234
|
+
const macro = model.macro_functions[unit.name.toUpperCase()];
|
|
2235
|
+
if (macro && macro.expression) {
|
|
2236
|
+
|
|
2237
|
+
// clone
|
|
2238
|
+
const expression = JSON.parse(JSON.stringify(macro.expression));
|
|
2239
|
+
|
|
2240
|
+
const bound_names: {[index: string]: ExpressionUnit} = {};
|
|
2241
|
+
|
|
2242
|
+
if (macro.argument_names) {
|
|
2243
|
+
for (let i = 0; i < macro.argument_names.length; i++) {
|
|
2244
|
+
const name = macro.argument_names[i].toUpperCase();
|
|
2245
|
+
|
|
2246
|
+
// temp just pass in
|
|
2247
|
+
bound_names[name] = unit.args[i] ? unit.args[i] : {type: 'missing'} as UnitMissing;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// replace arguments
|
|
2252
|
+
name_stack.unshift(bound_names);
|
|
2253
|
+
const replacement = this.ApplyMacroFunctionInternal(expression, model, name_stack);
|
|
2254
|
+
name_stack.shift();
|
|
2255
|
+
return replacement;
|
|
2256
|
+
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
break;
|
|
2262
|
+
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
return unit;
|
|
2266
|
+
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
protected ApplyMacroFunctions(expression: ExpressionUnit): ExpressionUnit|undefined {
|
|
2270
|
+
|
|
2271
|
+
if (!this.model) { return; }
|
|
2272
|
+
|
|
2273
|
+
const count = Object.keys(this.model.macro_functions).length;
|
|
2274
|
+
if (!count) { return; }
|
|
2275
|
+
|
|
2276
|
+
return this.ApplyMacroFunctionInternal(expression, this.model, []);
|
|
2277
|
+
|
|
2278
|
+
}
|
|
2279
|
+
*/
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
*
|
|
2283
|
+
*/
|
|
2284
|
+
protected RebuildGraphCell(cell: Cell, address: ICellAddress2): void {
|
|
2285
|
+
|
|
2286
|
+
// console.info("RGC", cell, address);
|
|
2287
|
+
|
|
2288
|
+
// array head
|
|
2289
|
+
if (cell.area && cell.area.start.column === address.column && cell.area.start.row === address.row) {
|
|
2290
|
+
|
|
2291
|
+
const {start, end} = cell.area;
|
|
2292
|
+
|
|
2293
|
+
const sheet_id = start.sheet_id || address.sheet_id; // ... should always be ===
|
|
2294
|
+
if (!start.sheet_id) { start.sheet_id = sheet_id; }
|
|
2295
|
+
|
|
2296
|
+
for (let column = start.column; column <= end.column; column++) {
|
|
2297
|
+
for (let row = start.row; row <= end.row; row++) {
|
|
2298
|
+
this.ResetInbound({ column, row, sheet_id }, true, false); // set dirty, don't create
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
this.SetDirty(address); // implicitly creates vertex for array head (if it doesn't already exist)
|
|
2303
|
+
|
|
2304
|
+
// implicit vertices from array head -> array members. this is required
|
|
2305
|
+
// to correctly propagate dirtiness if a referenced cell changes state
|
|
2306
|
+
// from array -> !array and vice-versa
|
|
2307
|
+
|
|
2308
|
+
for (let column = start.column; column <= end.column; column++) {
|
|
2309
|
+
for (let row = start.row; row <= end.row; row++) {
|
|
2310
|
+
if (row === start.row && column === start.column) { continue; }
|
|
2311
|
+
|
|
2312
|
+
this.AddEdge(start, {...start, row, column});
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// formula?
|
|
2319
|
+
if (cell.type === ValueType.formula) {
|
|
2320
|
+
|
|
2321
|
+
this.ResetInbound(address, true); // NOTE: sets dirty AND creates vertex if it doesn't exist
|
|
2322
|
+
const parse_result = this.parser.Parse(cell.value as string);
|
|
2323
|
+
|
|
2324
|
+
// we have a couple of "magic" functions that can have loops
|
|
2325
|
+
// but shouldn't trigger circular references. we need to check
|
|
2326
|
+
// for those here...
|
|
2327
|
+
|
|
2328
|
+
if (parse_result.expression) {
|
|
2329
|
+
|
|
2330
|
+
// FIXME: move macro function parsing here; so that we don't
|
|
2331
|
+
// need special call semantics, and dependencies work as normal.
|
|
2332
|
+
|
|
2333
|
+
// NOTE: the problem with that is you have to deep-parse every function,
|
|
2334
|
+
// here, to look for macros. that might be OK, but the alternative is
|
|
2335
|
+
// just to calculate them on demand, which seems a lot more efficient
|
|
2336
|
+
|
|
2337
|
+
// TEMP removing old macro handling
|
|
2338
|
+
// const modified = this.ApplyMacroFunctions(parse_result.expression);
|
|
2339
|
+
// if (modified) { parse_result.expression = modified; }
|
|
2340
|
+
|
|
2341
|
+
// ...
|
|
2342
|
+
|
|
2343
|
+
if (parse_result.expression.type === 'call') {
|
|
2344
|
+
const func = this.library.Get(parse_result.expression.name);
|
|
2345
|
+
|
|
2346
|
+
// this is for sparklines and checkboxes atm
|
|
2347
|
+
|
|
2348
|
+
if (func && (func.render || func.click)) {
|
|
2349
|
+
cell.render_function = func.render;
|
|
2350
|
+
cell.click_function = func.click;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
const dependencies = this.RebuildDependencies(parse_result.expression, address.sheet_id, '', undefined, address); // cell.sheet_id);
|
|
2356
|
+
|
|
2357
|
+
for (const key of Object.keys(dependencies.ranges)) {
|
|
2358
|
+
const unit = dependencies.ranges[key];
|
|
2359
|
+
const range = new Area(unit.start, unit.end);
|
|
2360
|
+
|
|
2361
|
+
// testing out array vertices (vertices that represent ranges).
|
|
2362
|
+
// this is an effort to reduce the number of vertices in the graph,
|
|
2363
|
+
// especially since these are generally unecessary (except for
|
|
2364
|
+
// formula cells).
|
|
2365
|
+
|
|
2366
|
+
// if you want to drop this, go back to the non-array code below
|
|
2367
|
+
// and it should go back to the old way (but there will still be
|
|
2368
|
+
// some cruft in graph.ts, tests that will need to be removed).
|
|
2369
|
+
|
|
2370
|
+
// actually it's probably something that could be balanced based
|
|
2371
|
+
// on the number of constants vs the number of formulae in the
|
|
2372
|
+
// range. more (or all) constants, use a range. more/all formula,
|
|
2373
|
+
// iterate.
|
|
2374
|
+
|
|
2375
|
+
// --- array version -----------------------------------------------
|
|
2376
|
+
|
|
2377
|
+
/*
|
|
2378
|
+
const status = this.AddArrayVertexEdge(range, cell);
|
|
2379
|
+
|
|
2380
|
+
if (status !== GraphStatus.OK) {
|
|
2381
|
+
global_status = status;
|
|
2382
|
+
if (!initial_reference) initial_reference = { ...cell };
|
|
2383
|
+
}
|
|
2384
|
+
*/
|
|
2385
|
+
|
|
2386
|
+
// --- non-array version -------------------------------------------
|
|
2387
|
+
|
|
2388
|
+
/*
|
|
2389
|
+
range.Iterate((target: ICellAddress) => {
|
|
2390
|
+
this.AddEdge(target, address);
|
|
2391
|
+
});
|
|
2392
|
+
*/
|
|
2393
|
+
|
|
2394
|
+
// --- trying again... ---------------------------------------------
|
|
2395
|
+
|
|
2396
|
+
if (range.entire_row || range.entire_column) {
|
|
2397
|
+
this.AddArrayEdge(range, address);
|
|
2398
|
+
}
|
|
2399
|
+
else {
|
|
2400
|
+
range.Iterate((target: ICellAddress) => this.AddEdge(target, address));
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
// --- end ---------------------------------------------------------
|
|
2405
|
+
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
for (const key of Object.keys(dependencies.addresses)) {
|
|
2409
|
+
const dependency = dependencies.addresses[key];
|
|
2410
|
+
this.AddEdge(dependency, address);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
const vertex = this.GetVertex(address, true);
|
|
2416
|
+
|
|
2417
|
+
if (vertex) {
|
|
2418
|
+
vertex.expression = parse_result.expression || { type: 'missing', id: -1 };
|
|
2419
|
+
vertex.expression_error = !parse_result.valid;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
}
|
|
2423
|
+
else if (cell.value !== cell.calculated) {
|
|
2424
|
+
|
|
2425
|
+
// sets dirty and removes inbound edges (in case the cell
|
|
2426
|
+
// previously contained a formula and now it contains a constant).
|
|
2427
|
+
|
|
2428
|
+
this.ResetInbound(address, true, false); // NOTE: sets dirty
|
|
2429
|
+
}
|
|
2430
|
+
else if (cell.type === ValueType.undefined) {
|
|
2431
|
+
|
|
2432
|
+
// in the new framework, we get here on any cleared cell, but
|
|
2433
|
+
// the behavior is OK
|
|
2434
|
+
|
|
2435
|
+
// if we get here, it means that this cell was cleared but is not
|
|
2436
|
+
// 'empty'; in practice, that means it has a merge cell. reset inbound
|
|
2437
|
+
// and set dirty.
|
|
2438
|
+
|
|
2439
|
+
// is this unecessarily flagging a number of cells? (...)
|
|
2440
|
+
|
|
2441
|
+
this.ResetInbound(address, true, false, true);
|
|
2442
|
+
|
|
2443
|
+
// we should be able to remove this vertex altogether; watch
|
|
2444
|
+
// out for arrays here
|
|
2445
|
+
|
|
2446
|
+
// this.RemoveVertex(address); // implicit
|
|
2447
|
+
|
|
2448
|
+
}
|
|
2449
|
+
else {
|
|
2450
|
+
|
|
2451
|
+
// the reason you never get here is that the standard case is
|
|
2452
|
+
// value !== calculated. if you enter a constant, we flush
|
|
2453
|
+
// calculated first; so while the value doesn't change, it no
|
|
2454
|
+
// longer === calculated.
|
|
2455
|
+
|
|
2456
|
+
// actually we do get here in the case of an array head with
|
|
2457
|
+
// a constant value. so we should stop shouting about it.
|
|
2458
|
+
|
|
2459
|
+
// this is just a constant?
|
|
2460
|
+
// console.warn('UNHANDLED CASE', cell);
|
|
2461
|
+
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* rebuild the graph; parse expressions, build a dependency map,
|
|
2469
|
+
* initialize edges between nodes.
|
|
2470
|
+
*
|
|
2471
|
+
* FIXME: if we want to compose functions, we could do that here,
|
|
2472
|
+
* which might result in some savings [?]
|
|
2473
|
+
*/
|
|
2474
|
+
protected RebuildGraph(subset?: Area): void {
|
|
2475
|
+
|
|
2476
|
+
if (subset) {
|
|
2477
|
+
|
|
2478
|
+
if (!subset.start.sheet_id) {
|
|
2479
|
+
throw new Error('subset missing sheet id');
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// const cells = this.cells_map[subset.start.sheet_id];
|
|
2483
|
+
const cells = this.model.sheets.Find(subset.start.sheet_id)?.cells;
|
|
2484
|
+
|
|
2485
|
+
if (cells) {
|
|
2486
|
+
for (let row = subset.start.row; row <= subset.end.row; row++) {
|
|
2487
|
+
const row_array = cells.data[row];
|
|
2488
|
+
if (row_array) {
|
|
2489
|
+
for (let column = subset.start.column; column <= subset.end.column; column++) {
|
|
2490
|
+
const cell = row_array[column];
|
|
2491
|
+
if (cell) {
|
|
2492
|
+
this.RebuildGraphCell(cell, {row, column, sheet_id: subset.start.sheet_id});
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
for (const sheet of this.model.sheets.list || []) {
|
|
2502
|
+
const rows = sheet.cells.data.length;
|
|
2503
|
+
for (let row = 0; row < rows; row++) {
|
|
2504
|
+
const row_array = sheet.cells.data[row];
|
|
2505
|
+
if (row_array) {
|
|
2506
|
+
const columns = row_array.length;
|
|
2507
|
+
for (let column = 0; column < columns; column++) {
|
|
2508
|
+
const cell = row_array[column];
|
|
2509
|
+
if (cell) {
|
|
2510
|
+
this.RebuildGraphCell(cell, {row, column, sheet_id: sheet.id});
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2521
|
+
protected IsNativeOrTypedArray(val: unknown): boolean {
|
|
2522
|
+
return Array.isArray(val) || (val instanceof Float64Array) || (val instanceof Float32Array);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/**
|
|
2526
|
+
* check if a cell is volatile. normally this falls out of the calculation,
|
|
2527
|
+
* but if we build the graph and set values explicitly, we need to check.
|
|
2528
|
+
*/
|
|
2529
|
+
protected CheckVolatile(vertex: SpreadsheetVertex): boolean {
|
|
2530
|
+
if (!vertex.expression || vertex.expression_error) return false;
|
|
2531
|
+
|
|
2532
|
+
let volatile = false;
|
|
2533
|
+
|
|
2534
|
+
this.parser.Walk(vertex.expression, (unit: ExpressionUnit) => {
|
|
2535
|
+
if (unit.type === 'call') {
|
|
2536
|
+
const func = this.library.Get(unit.name);
|
|
2537
|
+
if (func && func.volatile) volatile = true;
|
|
2538
|
+
}
|
|
2539
|
+
return !volatile; // short circuit
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
return volatile;
|
|
2543
|
+
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
}
|