@trebco/treb 23.6.5 → 25.0.0-rc2

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.
Files changed (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +323 -271
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +49 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1228 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5358 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +298 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. 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
+ }