@trebco/treb 23.6.5 → 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.
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} +285 -269
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -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 +1227 -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 +5362 -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 +320 -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,2673 @@
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 {
23
+ ExpressionUnit,
24
+ UnitAddress,
25
+ UnitIdentifier,
26
+ UnitOperator,
27
+ UnitRange,
28
+ UnitArray,
29
+ UnitUnary,
30
+ DependencyList,
31
+ ParseResult,
32
+ ArgumentSeparatorType,
33
+ DecimalMarkType,
34
+ UnitLiteral,
35
+ UnitLiteralNumber,
36
+ ParserFlags,
37
+ UnitStructuredReference,
38
+ RenderOptions,
39
+ } from './parser-types';
40
+
41
+ interface PrecedenceList {
42
+ [index: string]: number;
43
+ }
44
+
45
+ /**
46
+ * regex determines if a sheet name requires quotes. centralizing
47
+ * this to simplify maintenance and reduce overlap/errors
48
+ */
49
+ export const QuotedSheetNameRegex = /[\s-+=<>!()]/;
50
+
51
+ /**
52
+ * similarly, illegal sheet name. we don't actually handle this in
53
+ * the parser, but it seems like a reasonable place to keep this
54
+ * definition.
55
+ */
56
+ export const IllegalSheetNameRegex = /['*\\]/;
57
+
58
+ const DOUBLE_QUOTE = 0x22; // '"'.charCodeAt(0);
59
+ const SINGLE_QUOTE = 0x27; // `'`.charCodeAt(0);
60
+
61
+ const NON_BREAKING_SPACE = 0xa0;
62
+ const SPACE = 0x20;
63
+ const TAB = 0x09;
64
+ const CR = 0x0a;
65
+ const LF = 0x0d;
66
+
67
+ const ZERO = 0x30;
68
+ const NINE = 0x39;
69
+ const PERIOD = 0x2e;
70
+
71
+ const PLUS = 0x2b;
72
+ const MINUS = 0x2d;
73
+
74
+ const OPEN_PAREN = 0x28;
75
+ const CLOSE_PAREN = 0x29;
76
+
77
+ const COMMA = 0x2c;
78
+ const PERCENT = 0x25;
79
+
80
+ const UNDERSCORE = 0x5f;
81
+ const DOLLAR_SIGN = 0x24;
82
+
83
+ const OPEN_BRACE = 0x7b;
84
+ const CLOSE_BRACE = 0x7d;
85
+
86
+ const OPEN_SQUARE_BRACKET = 0x5b;
87
+ const CLOSE_SQUARE_BRACKET = 0x5d;
88
+
89
+ const EXCLAMATION_MARK = 0x21;
90
+ // const COLON = 0x3a; // became an operator
91
+ const SEMICOLON = 0x3b;
92
+
93
+ const HASH = 0x23; // #
94
+ const AT = 0x40; // @
95
+
96
+ const UC_A = 0x41;
97
+ const LC_A = 0x61;
98
+ const UC_E = 0x45;
99
+ const LC_E = 0x65;
100
+ const UC_Z = 0x5a;
101
+ const LC_Z = 0x7a;
102
+
103
+ const LC_I = 0x69;
104
+ const LC_J = 0x6a;
105
+
106
+ const ACCENTED_RANGE_START = 192;
107
+ const ACCENTED_RANGE_END = 312;
108
+
109
+ /**
110
+ * precedence map
111
+ */
112
+ const binary_operators_precendence: PrecedenceList = {
113
+ '==': 6,
114
+ '!=': 6, // FIXME: we should not support these (legacy)
115
+ '<>': 6,
116
+ '=': 6, // these are the appropriate equality operators for SL
117
+ '<': 7,
118
+ '>': 7,
119
+ '<=': 7,
120
+ '>=': 7,
121
+ '+': 9,
122
+ '-': 9,
123
+ '&': 9,
124
+ '*': 10,
125
+ '/': 10,
126
+ '^': 11, // highest math op
127
+ ':': 13, // range operator
128
+ };
129
+
130
+ /**
131
+ * binary ops are sorted by length so we can compare long ops first
132
+ */
133
+ const binary_operators = Object.keys(binary_operators_precendence).sort(
134
+ (a, b) => b.length - a.length,
135
+ );
136
+
137
+ /**
138
+ * unary operators. atm we have no precedence issues, unary operators
139
+ * always have absolute precedence. (for numbers, these are properly part
140
+ * of the number, but consider `=-SUM(1,2)` -- this is an operator).
141
+ */
142
+ const unary_operators: PrecedenceList = { '-': 100, '+': 100 };
143
+
144
+ /**
145
+ * parser for spreadsheet language.
146
+ *
147
+ * FIXME: this is stateless, think about exporting a singleton.
148
+ *
149
+ * (there is internal state, but it's only used during a Parse() call,
150
+ * which runs synchronously). one benefit of using a singleton would be
151
+ * consistency in decimal mark, we'd only have to set once.
152
+ *
153
+ * FIXME: split rendering into a separate class? would be a little cleaner.
154
+ */
155
+ export class Parser {
156
+
157
+ /**
158
+ * argument separator. this can be changed prior to parsing/rendering.
159
+ * FIXME: use an accessor to ensure type, outside of ts?
160
+ */
161
+ public argument_separator = ArgumentSeparatorType.Comma;
162
+
163
+ /**
164
+ * decimal mark. this can be changed prior to parsing/rendering.
165
+ * FIXME: use an accessor to ensure type, outside of ts?
166
+ */
167
+ public decimal_mark = DecimalMarkType.Period;
168
+
169
+ /**
170
+ * unifying flags
171
+ */
172
+ public flags: Partial<ParserFlags> = {
173
+ spreadsheet_semantics: true,
174
+ dimensioned_quantities: false,
175
+ fractions: true,
176
+ };
177
+
178
+ protected r1c1_regex = /[rR]((?:\[[-+]{0,1}\d+\]|\d+))[cC]((?:\[[-+]{0,1}\d+\]|\d+))$/;
179
+
180
+ /**
181
+ * internal argument separator, as a number. this is set internally on
182
+ * parse call, following the argument_separator value.
183
+ */
184
+ protected argument_separator_char = COMMA;
185
+
186
+ /**
187
+ * internal decimal mark, as a number.
188
+ */
189
+ protected decimal_mark_char = PERIOD;
190
+
191
+ /**
192
+ * imaginary number value. this is "i", except for those EE weirdos who
193
+ * use "j". although I guess those guys put it in front, so it won't really
194
+ * work anyway... let's stick with "i" for now.
195
+ */
196
+ protected imaginary_char: 0x69|0x6A = LC_I;
197
+
198
+ /**
199
+ * imaginary number as text for matching
200
+ */
201
+ protected imaginary_number: 'i'|'j' = 'i';
202
+
203
+ /**
204
+ * internal counter for incrementing IDs
205
+ */
206
+ protected id_counter = 0;
207
+
208
+ protected expression = '';
209
+ protected data: number[] = [];
210
+ protected index = 0;
211
+ protected length = 0;
212
+
213
+ /** success flag */
214
+ protected valid = true;
215
+
216
+ /** rolling error state */
217
+ protected error_position: number | undefined;
218
+
219
+ /** rolling error state */
220
+ protected error: string | undefined;
221
+
222
+ protected dependencies: DependencyList = {
223
+ addresses: {},
224
+ ranges: {},
225
+ };
226
+
227
+ // referenced addresses -- used to merge ranges/addresses, although I'm
228
+ // not sure that's actually all that useful
229
+ protected address_refcount: { [index: string]: number } = {};
230
+
231
+ /**
232
+ * full list of referenced addresses and ranges. we're adding this
233
+ * to support highlighting, for which we need multiple instances
234
+ * of a single address. the original dep list was used for graph dependencies,
235
+ * so we compressed the list.
236
+ *
237
+ * FIXME: use a single list, i.e. something like
238
+ *
239
+ * address -> [instance, instance]
240
+ *
241
+ * because that's a big API change it's going to have to wait. for now,
242
+ * use a second list.
243
+ *
244
+ * UPDATE: adding (otherwise unused) tokens, which could be named ranges.
245
+ * in the future we may pass in a list of names at parse time, and resolve
246
+ * them; for now we are just listing names.
247
+ */
248
+ protected full_reference_list: Array<UnitAddress | UnitRange | UnitIdentifier | UnitStructuredReference> = [];
249
+
250
+ /**
251
+ * recursive tree walk.
252
+ *
253
+ * @param func function called on each node. for nodes that have children
254
+ * (operations, calls, groups) return false to skip the subtree, or true to
255
+ * traverse.
256
+ */
257
+ public Walk(unit: ExpressionUnit, func: (unit: ExpressionUnit) => boolean): void {
258
+ switch (unit.type) {
259
+ case 'address':
260
+ case 'missing':
261
+ case 'literal':
262
+ case 'complex':
263
+ case 'identifier':
264
+ case 'operator':
265
+ case 'structured-reference':
266
+ func(unit);
267
+ return;
268
+
269
+ case 'dimensioned':
270
+ if (func(unit)) {
271
+ this.Walk(unit.expression, func);
272
+ this.Walk(unit.unit, func);
273
+ }
274
+ return;
275
+
276
+ case 'range':
277
+ if (func(unit)) {
278
+ this.Walk(unit.start, func);
279
+ this.Walk(unit.end, func);
280
+ }
281
+ return;
282
+
283
+ case 'binary':
284
+ if (func(unit)) {
285
+ this.Walk(unit.left, func);
286
+ this.Walk(unit.right, func);
287
+ }
288
+ return;
289
+
290
+ case 'unary':
291
+ if (func(unit)) {
292
+ this.Walk(unit.operand, func);
293
+ }
294
+ return;
295
+
296
+ case 'group':
297
+ if (func(unit)) {
298
+ unit.elements.forEach((element) => this.Walk(element, func));
299
+ }
300
+ return;
301
+
302
+ case 'call':
303
+ if (func(unit)) {
304
+ unit.args.forEach((arg) => this.Walk(arg, func));
305
+ }
306
+ }
307
+ }
308
+
309
+ /** utility: transpose array */
310
+ public Transpose(arr: Array < Array <string|boolean|number|undefined> >): Array < Array <string|boolean|number|undefined> > {
311
+
312
+ const m = arr.length;
313
+ const transposed: Array < Array <string|boolean|number|undefined> > = [];
314
+ let n = 0;
315
+
316
+ for (let i = 0; i < m; i++){
317
+ if (Array.isArray(arr[i])) {
318
+ n = Math.max(n, arr[i].length);
319
+ }
320
+ }
321
+
322
+ for (let i = 0; i < n; i++) {
323
+ transposed[i] = [];
324
+ for (let j = 0; j < m; j++) {
325
+ transposed[i][j] = arr[j][i];
326
+ }
327
+ }
328
+
329
+ return transposed;
330
+ }
331
+
332
+ /**
333
+ * renders the passed expression as a string.
334
+ * @param unit base expression
335
+ * @param offset offset for addresses, used to offset relative addresses
336
+ * (and ranges). this is for copy-and-paste or move operations.
337
+ * @param missing string to represent missing values (can be '', for functions)
338
+ *
339
+ * FIXME: we're accumulating too many arguments. need to switch to an
340
+ * options object. do that after the structured reference stuff merges.
341
+ *
342
+ */
343
+ public Render(
344
+ unit: ExpressionUnit,
345
+ options: Partial<RenderOptions> = {}): string {
346
+
347
+ // defaults
348
+
349
+ const offset = options.offset || {rows: 0, columns: 0};
350
+ const missing = options.missing ?? '(missing)';
351
+
352
+ // the rest are optional
353
+
354
+ /*
355
+ offset: { rows: number; columns: number } = { rows: 0, columns: 0 },
356
+ missing = '(missing)',
357
+ convert_decimal?: DecimalMarkType,
358
+ convert_argument_separator?: ArgumentSeparatorType,
359
+ convert_imaginary_number?: 'i'|'j',
360
+ long_structured_references?: boolean,
361
+ table_name?: string,
362
+
363
+ ): string {
364
+ */
365
+
366
+ const {
367
+ convert_decimal,
368
+ convert_argument_separator,
369
+ convert_imaginary_number,
370
+ long_structured_references,
371
+ table_name
372
+ } = options;
373
+
374
+ // use default separator, unless we're explicitly converting.
375
+
376
+ let separator = this.argument_separator + ' ';
377
+ if (convert_argument_separator === ArgumentSeparatorType.Comma) {
378
+ separator = ', ';
379
+ }
380
+ else if (convert_argument_separator === ArgumentSeparatorType.Semicolon) {
381
+ separator = '; ';
382
+ }
383
+
384
+ let imaginary_character = this.imaginary_number;
385
+ if (convert_imaginary_number) {
386
+ imaginary_character = convert_imaginary_number;
387
+ }
388
+
389
+ // this is only used if we're converting.
390
+
391
+ const decimal = convert_decimal === DecimalMarkType.Comma ? ',' : '.';
392
+ const decimal_rex =
393
+ this.decimal_mark === DecimalMarkType.Comma ? /,/ : /\./;
394
+
395
+ // we need this for complex numbers, but I don't want to change the
396
+ // original at the moment, just in case. we can run through that later.
397
+
398
+ const decimal_rex_g =
399
+ this.decimal_mark === DecimalMarkType.Comma ? /,/g : /\./g;
400
+
401
+ switch (unit.type) {
402
+ case 'address':
403
+ return this.AddressLabel(unit, offset);
404
+
405
+ case 'range':
406
+ return (
407
+ this.AddressLabel(unit.start, offset) +
408
+ ':' +
409
+ this.AddressLabel(unit.end, offset)
410
+ );
411
+
412
+ case 'missing':
413
+ return missing;
414
+
415
+ case 'array':
416
+
417
+ // we have to transpose because we're column-major but the
418
+ // format is row-major
419
+
420
+ return '{' +
421
+ this.Transpose(unit.values).map((row) => row.map((value) => {
422
+ if (typeof value === 'string') {
423
+ return '"' + value + '"';
424
+ }
425
+ return value;
426
+ }).join(', ')).join('; ') + '}';
427
+
428
+ case 'binary':
429
+ return (
430
+ this.Render(unit.left, options) +
431
+ ' ' +
432
+ unit.operator +
433
+ ' ' +
434
+ this.Render(unit.right, options)
435
+ );
436
+
437
+ case 'unary':
438
+ return (
439
+ unit.operator +
440
+ this.Render(unit.operand, options)
441
+ );
442
+
443
+ case 'complex':
444
+
445
+ // formatting complex value (note for searching)
446
+ // this uses small regular "i"
447
+
448
+ // as with literals, we want to preserve the original text,
449
+ // which might have slight precision differences from what
450
+ // we would render.
451
+
452
+ if (unit.text) {
453
+ if (convert_decimal) {
454
+
455
+ // we don't support grouping numbers for complex, so there's
456
+ // no need to handle grouping
457
+
458
+ const text = unit.text;
459
+ return text.replace(decimal_rex_g, decimal);
460
+
461
+ }
462
+ else {
463
+ return unit.text;
464
+ }
465
+ }
466
+ else {
467
+
468
+ // if we don't have the original text for whatever reason, format
469
+ // and convert if necessary.
470
+
471
+ let imaginary_text = Math.abs(unit.imaginary).toString();
472
+ if (convert_decimal === DecimalMarkType.Comma || this.decimal_mark === DecimalMarkType.Comma) {
473
+ imaginary_text = imaginary_text.replace(/\./, ',');
474
+ }
475
+
476
+ if (unit.real) {
477
+ let real_text = unit.real.toString();
478
+ if (convert_decimal === DecimalMarkType.Comma || this.decimal_mark === DecimalMarkType.Comma) {
479
+ real_text = real_text.replace(/\./, ',');
480
+ }
481
+
482
+ const i = Math.abs(unit.imaginary);
483
+ return `${real_text}${unit.imaginary < 0 ? ' - ' : ' + '}${i === 1 ? '' : imaginary_text}i`;
484
+ }
485
+ else if (unit.imaginary === -1) {
486
+ return `-i`;
487
+ }
488
+ else if (unit.imaginary === 1) {
489
+ return `i`;
490
+ }
491
+ else {
492
+ return `${unit.imaginary < 0 ? '-' : ''}${imaginary_text}i`;
493
+ }
494
+
495
+ }
496
+
497
+ break;
498
+
499
+ case 'literal':
500
+ if (typeof unit.value === 'string') {
501
+
502
+ // escape any quotation marks in string
503
+ return '"' + unit.value.replace(/"/g, '""') + '"';
504
+ }
505
+ else if (convert_decimal && typeof unit.value === 'number') {
506
+ if (unit.text) {
507
+ // here we want to translate the literal typed-in value.
508
+ // users can type in a decimal point and possibly grouping.
509
+ // if we are converting from dot to comma, we need to make
510
+ // sure to remove any existing commas. for the time being
511
+ // we will just remove them.
512
+
513
+ // what about the alternate case? in that case, we're not allowing
514
+ // users to type in groupings (I think), so we can skip that part.
515
+
516
+ // ACTUALLY, we don't allow grouping at all. we normalize it
517
+ // if you type in a number. why? consider functions, grouping
518
+ // looks like parameter separation. so no.
519
+
520
+ let text = unit.text;
521
+ if (
522
+ convert_decimal === DecimalMarkType.Comma &&
523
+ this.decimal_mark === DecimalMarkType.Period
524
+ ) {
525
+ text = text.replace(/,/g, ''); // remove grouping
526
+ }
527
+ return text.replace(decimal_rex, decimal);
528
+ }
529
+ else {
530
+ // this always works because this function is guaranteed
531
+ // to return value in dot-decimal format without separators.
532
+
533
+ return unit.value.toString().replace(/\./, decimal);
534
+ }
535
+ }
536
+ else if (unit.text) return unit.text;
537
+ return unit.value.toString();
538
+
539
+ case 'identifier':
540
+ return unit.name;
541
+
542
+ case 'operator':
543
+ return '[' + unit.operator + ']'; // this should be invalid output
544
+
545
+ case 'group':
546
+ if (unit.explicit) {
547
+ return (
548
+ '(' +
549
+ unit.elements
550
+ .map((x) => this.Render(x, options)).join(separator) +
551
+ ')'
552
+ );
553
+ }
554
+ else {
555
+ return unit.elements
556
+ .map((x) => this.Render(x, options)).join(separator);
557
+ }
558
+
559
+ case 'call':
560
+ return (
561
+ unit.name +
562
+ '(' +
563
+ unit.args
564
+ .map((x) =>
565
+ this.Render(x, options)).join(separator) +
566
+ ')'
567
+ );
568
+
569
+ case 'dimensioned':
570
+ return this.Render(unit.expression) + ' ' + this.Render(unit.unit);
571
+
572
+ case 'structured-reference':
573
+
574
+ // not sure of the rules around one or two braces for the
575
+ // column name... certainly spaces means you need at least one
576
+
577
+ {
578
+ let column = unit.column;
579
+ if (/[^A-Za-z]/.test(column)) {
580
+ column = '[' + column + ']';
581
+ }
582
+
583
+ let table = unit.table;
584
+
585
+ // console.info("RENDER SR", unit, table_name, long_structured_references);
586
+
587
+ if (!table && long_structured_references && table_name) {
588
+ table = table_name;
589
+ }
590
+
591
+ switch (unit.scope) {
592
+ case 'all':
593
+ return `${table}[[#all],${column}]`;
594
+
595
+ case 'row':
596
+ if (long_structured_references) {
597
+ return `${table}[[#this row],${column}]`;
598
+ }
599
+ else {
600
+ return `${table}[@${column}]`;
601
+ }
602
+
603
+ case 'column':
604
+ return `${table}[${column}]`;
605
+
606
+ }
607
+
608
+ // this is here in case we add a new scope in the future,
609
+ // so we remember to handle this case
610
+
611
+ throw new Error('unhandled scope in structured reference');
612
+
613
+ }
614
+
615
+ }
616
+
617
+ return '??';
618
+ }
619
+
620
+ /**
621
+ * parses expression and returns the root of the parse tree, plus a
622
+ * list of dependencies (addresses and ranges) found in the expression.
623
+ *
624
+ * NOTE that in the new address parsing structure, we will overlap ranges
625
+ * and addresses (range corners). this is OK because ranges are mapped
626
+ * to individual address dependencies. it's just sloppy (FIXME: refcount?)
627
+ */
628
+ public Parse(expression: string): ParseResult {
629
+
630
+ // normalize
631
+ expression = expression.trim();
632
+
633
+ // remove leading =
634
+ if (expression[0] === '=') {
635
+ expression = expression.substr(1).trim();
636
+ }
637
+
638
+ this.expression = expression;
639
+ this.data = [];
640
+ this.length = expression.length;
641
+ this.index = 0;
642
+ this.valid = true;
643
+ this.error_position = undefined;
644
+ this.error = undefined;
645
+ this.dependencies.addresses = {};
646
+ this.dependencies.ranges = {};
647
+ this.address_refcount = {};
648
+ this.full_reference_list = [];
649
+
650
+ // reset ID
651
+ this.id_counter = 0;
652
+
653
+ // set separator
654
+ switch (this.argument_separator) {
655
+ case ArgumentSeparatorType.Semicolon:
656
+ this.argument_separator_char = SEMICOLON;
657
+ break;
658
+ default:
659
+ this.argument_separator_char = COMMA;
660
+ break;
661
+ }
662
+
663
+ // and decimal mark
664
+ switch (this.decimal_mark) {
665
+ case DecimalMarkType.Comma:
666
+ this.decimal_mark_char = COMMA;
667
+ break;
668
+ default:
669
+ this.decimal_mark_char = PERIOD;
670
+ break;
671
+ }
672
+
673
+ // NOTE on this function: charCodeAt returns UTF-16. codePointAt returns
674
+ // unicode. length returns UTF-16 length. any characters that are not
675
+ // representable as a single character in UTF-16 will be 'the first unit
676
+ // of a surrogate pair...' and so on.
677
+ //
678
+ // we want UTF-16, not unicode. for the parser itself, we are only really
679
+ // looking for ASCII, so it's not material. for anything else, if we
680
+ // construct strings from the original data we want to map the UTF-16,
681
+ // otherwise we will construct the string incorrectly. this applies to
682
+ // strings, function names, and anything else.
683
+ //
684
+ // which is all a long way of saying, don't be tempted to replace this
685
+ // with codePointAt.
686
+
687
+ for (let i = 0; i < this.length; i++) {
688
+ this.data[i] = expression.charCodeAt(i);
689
+ }
690
+
691
+ const expr = this.ParseGeneric();
692
+
693
+ // last pass: convert any remaining imaginary values to complex values.
694
+ // FIXME: could do this elsewhere? not sure we should be adding yet
695
+ // another loop...
696
+
697
+ // (moving)
698
+
699
+ // remove extraneous addresses
700
+
701
+ // NOTE: we still may have duplicates that have different absolute/relative
702
+ // modifiers, e.g. C3 and $C$3 (and $C3 and C$3). not sure what we should
703
+ // do about that, since some consumers may consider these different -- we
704
+ // need to establish a contract about this
705
+
706
+ const addresses: { [index: string]: UnitAddress } = {};
707
+ for (const key of Object.keys(this.dependencies.addresses)) {
708
+ if (this.address_refcount[key]) {
709
+ addresses[key] = this.dependencies.addresses[key];
710
+ }
711
+ }
712
+ this.dependencies.addresses = addresses;
713
+
714
+ return {
715
+ expression: expr || undefined,
716
+ valid: this.valid,
717
+ error: this.error,
718
+ error_position: this.error_position,
719
+ dependencies: this.dependencies,
720
+ separator: this.argument_separator,
721
+ decimal_mark: this.decimal_mark,
722
+ full_reference_list: this.full_reference_list.slice(0),
723
+ };
724
+ }
725
+
726
+ /** generates column label ("A") from column index (0-based) */
727
+ protected ColumnLabel(column: number): string {
728
+ if (column === Infinity) { return ''; }
729
+ let s = String.fromCharCode(65 + (column % 26));
730
+ while (column > 25) {
731
+ column = Math.floor(column / 26) - 1;
732
+ s = String.fromCharCode(65 + (column % 26)) + s;
733
+ }
734
+ return s;
735
+ }
736
+
737
+ /** generates address label ("C3") from address (0-based) */
738
+ protected AddressLabel(
739
+ address: UnitAddress,
740
+ offset: { rows: number; columns: number },
741
+ ): string {
742
+ let column = address.column;
743
+ if (!address.absolute_column && address.column !== Infinity) column += offset.columns;
744
+
745
+ let row = address.row;
746
+ if (!address.absolute_row && address.row !== Infinity) row += offset.rows;
747
+
748
+ if (row < 0 || column < 0 || (row === Infinity && column === Infinity)) return '#REF';
749
+
750
+ let label = '';
751
+ if (address.sheet) {
752
+
753
+ /*
754
+ if (address.sheet === '__REF') {
755
+ return '#REF'; // magic
756
+ }
757
+ */
758
+
759
+ label = (QuotedSheetNameRegex.test(address.sheet) ?
760
+ '\'' + address.sheet + '\'' : address.sheet) + '!';
761
+ }
762
+
763
+ if (row === Infinity) {
764
+ return label +
765
+ (address.absolute_column ? '$' : '') +
766
+ this.ColumnLabel(column);
767
+ }
768
+
769
+ if (column === Infinity) {
770
+ return label +
771
+ (address.absolute_row ? '$' : '') +
772
+ (row + 1)
773
+ }
774
+
775
+ return (
776
+ label +
777
+ (address.absolute_column ? '$' : '') +
778
+ this.ColumnLabel(column) +
779
+ (address.absolute_row ? '$' : '') +
780
+ (row + 1)
781
+ );
782
+ }
783
+
784
+ /**
785
+ * base parse routine; may recurse inside parens (either as grouped
786
+ * operations or in function arguments).
787
+ *
788
+ * @param exit exit on specific characters
789
+ */
790
+ protected ParseGeneric(exit: number[] = [0]): ExpressionUnit | null {
791
+ let stream: ExpressionUnit[] = [];
792
+
793
+ for (; this.index < this.length;) {
794
+ const unit = this.ParseNext(stream.length === 0);
795
+ if (typeof unit === 'number') {
796
+
797
+ if (exit.some((test) => unit === test)) {
798
+ break;
799
+ }
800
+ else if (unit === OPEN_PAREN) {
801
+
802
+ // note that function calls are handled elsewhere,
803
+ // so we only have to worry about grouping. parse
804
+ // up to the closing paren...
805
+
806
+ this.index++; // open paren
807
+ const group = this.ParseGeneric([CLOSE_PAREN]);
808
+ this.index++; // close paren
809
+
810
+ // and wrap up in a group element to prevent reordering.
811
+ // flag indicates that this is a user grouping, not ours
812
+
813
+ // skip nulls
814
+
815
+ if (group) {
816
+ stream.push({
817
+ type: 'group',
818
+ id: this.id_counter++,
819
+ elements: [group],
820
+ explicit: true,
821
+ });
822
+ }
823
+ }
824
+ else {
825
+ // this can probably move to PNext? except for the test
826
+ // on looking for a binary operator? (...)
827
+
828
+ const operator = this.ConsumeOperator();
829
+ if (operator) {
830
+ stream.push(operator);
831
+ }
832
+ else {
833
+ this.error = `unexpected character [1]: ${String.fromCharCode(unit)}, 0x${unit.toString(16)}`;
834
+ this.valid = false;
835
+ this.index++;
836
+ }
837
+ }
838
+ }
839
+ else {
840
+ stream.push(unit);
841
+ }
842
+ }
843
+
844
+ // why do we build ranges after doing reordering? since ranges
845
+ // have the highest precedence (after complex numbers), why not
846
+ // just run through them now? also we could merge the complex
847
+ // composition (or not, since that's optional)
848
+
849
+ // ...
850
+
851
+ // OK, doing that now (testing). a side benefit is that this solves
852
+ // one of the problems we had with complex numbers, mismatching naked
853
+ // column identifiers like I:J. if we do ranges first we will not run
854
+ // into that problem.
855
+
856
+ if (stream.length) {
857
+
858
+ stream = this.BinaryToRange2(stream);
859
+
860
+ // FIXME: fractions should perhaps move, not sure about the proper
861
+ // ordering...
862
+
863
+ if (this.flags.fractions) {
864
+
865
+ // the specific pattern we are looking for for a fraction is
866
+ //
867
+ // literal (integer)
868
+ // literal (integer)
869
+ // operator (/)
870
+ // literal (integer)
871
+ //
872
+
873
+ // NOTE: excel actually translates these functions after you
874
+ // enter them to remove the fractions. not sure why, but it's
875
+ // possible that exporting them to something else (lotus?) wouldn't
876
+ // work. we can export them to excel, however, so maybe we can just
877
+ // leave as-is.
878
+
879
+ const rebuilt: ExpressionUnit[] = [];
880
+ const IsInteger = (test: ExpressionUnit) => {
881
+ return (test.type === 'literal')
882
+ && ((typeof test.value) === 'number')
883
+ && ((test.value as number) % 1 === 0); // bad typescript
884
+ };
885
+
886
+ let i = 0;
887
+ for (; i < stream.length - 3; i++) {
888
+ if (IsInteger(stream[i])
889
+ && IsInteger(stream[i + 1])
890
+ && (stream[i + 2].type === 'operator' && (stream[i+2] as UnitOperator).operator === '/')
891
+ && IsInteger(stream[i + 3])) {
892
+
893
+ const a = stream[i] as UnitLiteralNumber;
894
+ const b = stream[i + 1] as UnitLiteralNumber;
895
+ const c = stream[i + 3] as UnitLiteralNumber;
896
+ const f = ((a.value < 0) ? -1 : 1) * (b.value / c.value);
897
+
898
+ i += 3;
899
+ rebuilt.push({
900
+ id: stream[i].id,
901
+ type: 'literal',
902
+ text: this.expression.substring(a.position, c.position + 1),
903
+ value: a.value + f,
904
+ position: a.position,
905
+ })
906
+ }
907
+ else {
908
+ rebuilt.push(stream[i]);
909
+ }
910
+ }
911
+ for (; i < stream.length; i++){
912
+ rebuilt.push(stream[i]);
913
+ }
914
+
915
+ stream = rebuilt;
916
+
917
+ }
918
+
919
+
920
+ // so we're moving complex handling to post-reordering, to support
921
+ // precedence properly. there's still one thing we have to do here,
922
+ // though: handle those cases of naked imaginary values "i". these
923
+ // will be text identifiers, because they don't look like anything
924
+ // else. the previous routine will have pulled out column ranges like
925
+ // I:I so we don't have to worry about that anymore.
926
+
927
+ stream = stream.map(test => {
928
+ if (test.type === 'identifier' && test.name === this.imaginary_number) {
929
+
930
+ return {
931
+ type: 'complex',
932
+ real: 0,
933
+ imaginary: 1,
934
+ position: test.position,
935
+ text: test.name,
936
+ id: this.id_counter++,
937
+ };
938
+
939
+ }
940
+ return test;
941
+ });
942
+
943
+ if (this.flags.dimensioned_quantities) {
944
+
945
+ // support dimensioned quantities. we need to think a little about what
946
+ // should and should not be supported here -- definitely a literal
947
+ // followed by an identifier; definitely not two identifiers in a row;
948
+ // (really?) definitely not expressions followed by identifiers...
949
+ //
950
+ // what about
951
+ // group: (3+2)mm [yes]
952
+ // call: sin(3)mm [yes]
953
+ // name?: Xmm [...]
954
+ //
955
+ // what about space?
956
+ // 10 fluid ounces
957
+ // 10 fl oz
958
+ //
959
+
960
+ const rebuilt: ExpressionUnit[] = [];
961
+ let unit: ExpressionUnit | undefined;
962
+
963
+ for (let i = 0; i < stream.length; i++) {
964
+ //for (const entry of stream) {
965
+ const entry = stream[i];
966
+
967
+ if (!unit) {
968
+ unit = entry;
969
+ }
970
+ else if (entry.type === 'identifier' && (unit.type === 'literal' || unit.type === 'group' || unit.type === 'call')) {
971
+
972
+ // check for multi-word unit (unit has spaces)
973
+
974
+ const identifier = entry as UnitIdentifier;
975
+ while (stream[i + 1]?.type === 'identifier') {
976
+ identifier.name += (' ' + (stream[++i] as UnitIdentifier).name);
977
+ }
978
+
979
+ rebuilt.push({
980
+ type: 'dimensioned',
981
+ expression: unit,
982
+ unit: entry as UnitIdentifier,
983
+ id: this.id_counter++,
984
+ });
985
+ unit = undefined; // consume
986
+ }
987
+ else {
988
+ rebuilt.push(unit);
989
+ unit = entry;
990
+ }
991
+ }
992
+
993
+ // trailer
994
+
995
+ if (unit) {
996
+ rebuilt.push(unit);
997
+ }
998
+
999
+ stream = rebuilt;
1000
+
1001
+ }
1002
+
1003
+ }
1004
+
1005
+ // console.info("STREAM\n", stream, "\n\n");
1006
+
1007
+ if (stream.length === 0) return null;
1008
+ if (stream.length === 1) return stream[0];
1009
+
1010
+ // fix ordering of binary operations based on precedence; also
1011
+ // convert and validate ranges
1012
+
1013
+ // return this.BinaryToRange(this.ArrangeUnits(stream));
1014
+ // return this.ArrangeUnits(stream);
1015
+ return this.BinaryToComplex(this.ArrangeUnits(stream));
1016
+ }
1017
+
1018
+ /**
1019
+ * helper function, @see BinaryToRange
1020
+ * @param unit
1021
+ * @returns
1022
+ */
1023
+ protected UnitToAddress(unit: UnitLiteral|UnitIdentifier): UnitAddress|undefined {
1024
+
1025
+ // console.info("U2", unit);
1026
+
1027
+ // for literals, only numbers are valid
1028
+ if (unit.type === 'literal') {
1029
+ if (typeof unit.value === 'number' && unit.value > 0 && !/\./.test(unit.text||'')) {
1030
+ return {
1031
+ type: 'address',
1032
+ position: unit.position,
1033
+ label: unit.value.toString(),
1034
+ row: unit.value - 1,
1035
+ id: this.id_counter++,
1036
+ column: Infinity,
1037
+ };
1038
+ }
1039
+ }
1040
+ else {
1041
+
1042
+ // UPDATE: sheet names... we may actually need a subparser for this?
1043
+ // or can we do it with a regex? (...)
1044
+
1045
+ let sheet: string|undefined;
1046
+ let name = unit.name;
1047
+
1048
+ const tokens = name.split('!');
1049
+ if (tokens.length > 1) {
1050
+ sheet = tokens.slice(0, tokens.length - 1).join('!');
1051
+ name = name.substr(sheet.length + 1);
1052
+ if (sheet[0] === '\'') {
1053
+ if (sheet.length > 1 && sheet[sheet.length - 1] === '\'') {
1054
+ sheet = sheet.substr(1, sheet.length - 2);
1055
+ }
1056
+ else {
1057
+ // console.info('mismatched single quote');
1058
+ return undefined;
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ const absolute = name[0] === '$';
1064
+ name = (absolute ? name.substr(1) : name).toUpperCase();
1065
+ const as_number = Number(name);
1066
+
1067
+ // if it looks like a number, consider it a number and then be strict
1068
+ if (!isNaN(as_number)) {
1069
+ if (as_number > 0 && as_number !== Infinity && !/\./.test(name)) {
1070
+ return {
1071
+ type: 'address',
1072
+ position: unit.position,
1073
+ absolute_row: absolute,
1074
+ label: unit.name,
1075
+ row: as_number - 1,
1076
+ id: this.id_counter++,
1077
+ column: Infinity,
1078
+ sheet,
1079
+ };
1080
+ }
1081
+ }
1082
+ else if (/[A-Z]{1,3}/.test(name)) {
1083
+
1084
+ let column = -1; // clever
1085
+
1086
+ for (let i = 0; i < name.length; i++) {
1087
+ const char = name[i].charCodeAt(0);
1088
+ column = 26 * (1 + column) + (char - UC_A);
1089
+ }
1090
+
1091
+ return {
1092
+ type: 'address',
1093
+ position: unit.position,
1094
+ absolute_column: absolute,
1095
+ label: unit.name,
1096
+ column,
1097
+ id: this.id_counter++,
1098
+ row: Infinity,
1099
+ sheet,
1100
+ }
1101
+
1102
+ }
1103
+
1104
+ }
1105
+
1106
+ return undefined;
1107
+ }
1108
+
1109
+ /**
1110
+ * rewrite of binary to range. this version operates on the initial stream,
1111
+ * which should be OK because range has the highest precedence so we would
1112
+ * never reorder a range.
1113
+ *
1114
+ * ACTUALLY this will break in the case of
1115
+ *
1116
+ * -15:16
1117
+ *
1118
+ * (I think that's the only case). we can fix that though. this should
1119
+ * not impact the case of `2-15:16`, because in that case the - will look
1120
+ * like an operator and not part of the number. the same goes for a leading
1121
+ * `+` which will get dropped implicitly but has no effect (we might want
1122
+ * to preserve it for consistency though).
1123
+ *
1124
+ * NOTE: that error existed in the old version, too, and this way is perhaps
1125
+ * better for fixing it. we should merge this into main.
1126
+ *
1127
+ *
1128
+ * old version comments:
1129
+ * ---
1130
+ *
1131
+ * converts binary operations with a colon operator to ranges. this also
1132
+ * validates that there are no colon operations with non-address operands
1133
+ * (which is why it's called after precendence reordering; colon has the
1134
+ * highest preference). recursive only over binary ops AND unary ops.
1135
+ *
1136
+ * NOTE: there are other legal arguments to a colon operator. specifically:
1137
+ *
1138
+ * (1) two numbers, in either order
1139
+ *
1140
+ * 15:16
1141
+ * 16:16
1142
+ * 16:15
1143
+ *
1144
+ * (2) with one or both optionally having a $
1145
+ *
1146
+ * 15:$16
1147
+ * $16:$16
1148
+ *
1149
+ * (3) two column identifiers, in either order
1150
+ *
1151
+ * A:F
1152
+ * B:A
1153
+ *
1154
+ * (4) and the same with $
1155
+ *
1156
+ * $A:F
1157
+ * $A:$F
1158
+ *
1159
+ * because none of these are legal in any other context, we leave the
1160
+ * default treatment of them UNLESS they are arguments to the colon
1161
+ * operator, in which case we will grab them. that does mean we parse
1162
+ * them twice, but (...)
1163
+ *
1164
+ * FIXME: will need some updated to rendering these, we don't have any
1165
+ * handler for rendering infinity
1166
+ */
1167
+ protected BinaryToRange2(stream: ExpressionUnit[]): ExpressionUnit[] {
1168
+ const result: ExpressionUnit[] = [];
1169
+
1170
+ for (let i = 0; i < stream.length; i++) {
1171
+
1172
+ const a = stream[i];
1173
+ const b = stream[i + 1];
1174
+ const c = stream[i + 2];
1175
+
1176
+ let range: UnitRange|undefined;
1177
+ let label = '';
1178
+
1179
+ let negative: UnitOperator|undefined; // this is a fix for the error case `-14:15`, see below
1180
+
1181
+ if (a && b && c && b.type === 'operator' && b.operator === ':') {
1182
+
1183
+ if (a.type === 'address' && c.type === 'address') {
1184
+
1185
+ // construct a label using the full text. there's a possibility,
1186
+ // I suppose, that there are spaces (this should probably not be
1187
+ // legal). this is a canonical label, though (generated)
1188
+
1189
+ // it might be better to let this slip, or treat it as an error
1190
+ // and force a correction... not sure (TODO/FIXME)
1191
+
1192
+ const start_index = a.position + a.label.length;
1193
+ const end_index = c.position;
1194
+
1195
+ range = {
1196
+ type: 'range',
1197
+ id: this.id_counter++,
1198
+ position: a.position,
1199
+ start: a,
1200
+ end: c,
1201
+ label:
1202
+ a.label +
1203
+ this.expression.substring(start_index, end_index) +
1204
+ c.label,
1205
+ };
1206
+
1207
+ label = range.start.label + ':' + range.end.label;
1208
+
1209
+ this.address_refcount[range.start.label]--;
1210
+ this.address_refcount[range.end.label]--;
1211
+
1212
+ // remove entries from the list for start, stop
1213
+ const positions = [a.position, c.position];
1214
+ this.full_reference_list = this.full_reference_list.filter((test) => {
1215
+ return (
1216
+ test.position !== positions[0] && test.position !== positions[1]
1217
+ );
1218
+ });
1219
+
1220
+ }
1221
+ else if ((a.type === 'literal' || a.type === 'identifier')
1222
+ && (c.type === 'literal' || c.type === 'identifier')) {
1223
+
1224
+ // see if we can plausibly interpret both of these as rows or columns
1225
+
1226
+ // this is a fix for the case of `-14:15`, which is kind of a rare
1227
+ // case but could happen. in that case we need to invert the first number,
1228
+ // so it parses as an address properly, and also insert a "-" which
1229
+ // should be treated as a unary operator.
1230
+
1231
+ // if this happens, the first part must look like a negative number,
1232
+ // e.g. -10, so there are no leading spaces or intervening spaces
1233
+ // between the - and the value. therefore...
1234
+
1235
+ let left = this.UnitToAddress(a);
1236
+ if (!left && a.type === 'literal' && typeof a.value === 'number' && a.value < 0) {
1237
+ const test = {
1238
+ ...a,
1239
+ text: (a.text || '').replace(/^-/, ''), // <- ...sign always in position 0
1240
+ position: a.position + 1, // <- ...advance 1
1241
+ value: -a.value, // <- ...invert value
1242
+ };
1243
+ left = this.UnitToAddress(test);
1244
+
1245
+ if (left) {
1246
+
1247
+ // if that worked, we need to insert an operator into the
1248
+ // stream to reflect the - sign. we use the original position.
1249
+
1250
+ negative = {
1251
+ type: 'operator',
1252
+ operator: '-',
1253
+ position: a.position,
1254
+ id: this.id_counter++,
1255
+ }
1256
+ }
1257
+
1258
+ }
1259
+
1260
+ const right = this.UnitToAddress(c);
1261
+
1262
+ // and they need to match
1263
+
1264
+ if (left && right
1265
+ && ((left.column === Infinity && right.column === Infinity)
1266
+ || (left.row === Infinity && right.row === Infinity))) {
1267
+
1268
+ label = left.label + ':' + right.label;
1269
+
1270
+ // we don't support out-of-order ranges, so we should correct.
1271
+ // they just won't work otherwise. (TODO/FIXME)
1272
+
1273
+ range = {
1274
+ type: 'range',
1275
+ id: this.id_counter++,
1276
+ position: left.position,
1277
+ start: left,
1278
+ end: right,
1279
+ label,
1280
+ };
1281
+
1282
+ }
1283
+ }
1284
+
1285
+ }
1286
+
1287
+ if (range) {
1288
+
1289
+ if (negative) {
1290
+ result.push(negative);
1291
+ }
1292
+
1293
+ result.push(range);
1294
+ this.dependencies.ranges[label] = range;
1295
+ this.full_reference_list.push(range);
1296
+
1297
+ // skip
1298
+ i += 2;
1299
+ }
1300
+ else {
1301
+ result.push(a);
1302
+ }
1303
+
1304
+ }
1305
+
1306
+ return result;
1307
+ }
1308
+
1309
+ /**
1310
+ * we've now come full circle. we started with handling ranges as
1311
+ * binary operators; then we added complex composition as a first-pass
1312
+ * function; then we moved ranges to a first-pass function; and now we're
1313
+ * moving complex composition to a lower-level restructuring of binary
1314
+ * operations.
1315
+ *
1316
+ * that allows better precedence handling for (potentially) ambiguous
1317
+ * constructions like =B3 * 2 + 3i. we do have parens, so.
1318
+ *
1319
+ * @param unit
1320
+ * @returns
1321
+ */
1322
+ protected BinaryToComplex(unit: ExpressionUnit): ExpressionUnit {
1323
+
1324
+ if (unit.type === 'binary'){
1325
+ if ((unit.operator === '+' || unit.operator === '-')
1326
+ && unit.left.type === 'literal'
1327
+ && typeof unit.left.value === 'number'
1328
+ && unit.right.type === 'complex' // 'imaginary') {
1329
+ && !unit.right.composited ){
1330
+
1331
+ // ok, compose
1332
+ // console.info("WANT TO COMPOSE", unit);
1333
+
1334
+ let text = '';
1335
+
1336
+ text = this.expression.substring(unit.left.position, unit.right.position + (unit.right.text?.length || 0));
1337
+
1338
+ let imaginary_value = unit.right.imaginary;
1339
+
1340
+ if (unit.operator === '-') {
1341
+ imaginary_value = -imaginary_value;
1342
+ }
1343
+
1344
+ return {
1345
+ type: 'complex',
1346
+ position: unit.left.position,
1347
+ text: text,
1348
+ id: this.id_counter++,
1349
+ imaginary: imaginary_value,
1350
+ real: unit.left.value,
1351
+ composited: true,
1352
+ };
1353
+
1354
+ }
1355
+ else {
1356
+ unit.left = this.BinaryToComplex(unit.left);
1357
+ unit.right = this.BinaryToComplex(unit.right);
1358
+ }
1359
+ }
1360
+ else if (unit.type === 'unary' &&
1361
+ (unit.operator === '-' || unit.operator === '+') &&
1362
+ unit.operand.type === 'complex' &&
1363
+ unit.operand.text === this.imaginary_number ) {
1364
+
1365
+ // sigh... patch fix for very special case of "-i"
1366
+ // actually: why do I care about this? we could let whomever is using
1367
+ // the result deal with this particular case... although it's more
1368
+ // properly our responsibility if we are parsing complex numbers.
1369
+
1370
+ // we only have to worry about mischaracterizing the range label,
1371
+ // e.g. "-i:j", but we should have already handled that in a prior pass.
1372
+
1373
+ return {
1374
+ ...unit.operand,
1375
+ position: unit.position,
1376
+ text: this.expression.substring(unit.position, unit.operand.position + (unit.operand.text || '').length),
1377
+ imaginary: unit.operand.imaginary * (unit.operator === '-' ? -1 : 1),
1378
+ };
1379
+
1380
+ }
1381
+
1382
+ return unit;
1383
+
1384
+ }
1385
+
1386
+
1387
+ /**
1388
+ * converts binary operations with a colon operator to ranges. this also
1389
+ * validates that there are no colon operations with non-address operands
1390
+ * (which is why it's called after precendence reordering; colon has the
1391
+ * highest preference). recursive only over binary ops AND unary ops.
1392
+ *
1393
+ * NOTE: there are other legal arguments to a colon operator. specifically:
1394
+ *
1395
+ * (1) two numbers, in either order
1396
+ *
1397
+ * 15:16
1398
+ * 16:16
1399
+ * 16:15
1400
+ *
1401
+ * (2) with one or both optionally having a $
1402
+ *
1403
+ * 15:$16
1404
+ * $16:$16
1405
+ *
1406
+ * (3) two column identifiers, in either order
1407
+ *
1408
+ * A:F
1409
+ * B:A
1410
+ *
1411
+ * (4) and the same with $
1412
+ *
1413
+ * $A:F
1414
+ * $A:$F
1415
+ *
1416
+ * because none of these are legal in any other context, we leave the
1417
+ * default treatment of them UNLESS they are arguments to the colon
1418
+ * operator, in which case we will grab them. that does mean we parse
1419
+ * them twice, but (...)
1420
+ *
1421
+ * FIXME: will need some updated to rendering these, we don't have any
1422
+ * handler for rendering infinity
1423
+ */
1424
+ protected BinaryToRangeX(unit: ExpressionUnit): ExpressionUnit {
1425
+ if (unit.type === 'binary') {
1426
+ if (unit.operator === ':') {
1427
+
1428
+ let range: UnitRange|undefined;
1429
+ let label = '';
1430
+
1431
+ if (unit.left.type === 'address' && unit.right.type === 'address') {
1432
+ // construct a label using the full text. there's a possibility,
1433
+ // I suppose, that there are spaces (this should probably not be
1434
+ // legal). this is a canonical label, though (generated)
1435
+
1436
+ // it might be better to let this slip, or treat it as an error
1437
+ // and force a correction... not sure (TODO/FIXME)
1438
+
1439
+ const start_index = unit.left.position + unit.left.label.length;
1440
+ const end_index = unit.right.position;
1441
+
1442
+ range = {
1443
+ type: 'range',
1444
+ id: this.id_counter++,
1445
+ position: unit.left.position,
1446
+ start: unit.left,
1447
+ end: unit.right,
1448
+ label:
1449
+ unit.left.label +
1450
+ this.expression.substring(start_index, end_index) +
1451
+ unit.right.label,
1452
+ };
1453
+
1454
+ label = range.start.label + ':' + range.end.label;
1455
+
1456
+ this.address_refcount[range.start.label]--;
1457
+ this.address_refcount[range.end.label]--;
1458
+
1459
+ // remove entries from the list for start, stop
1460
+ const positions = [unit.left.position, unit.right.position];
1461
+ this.full_reference_list = this.full_reference_list.filter((test) => {
1462
+ return (
1463
+ test.position !== positions[0] && test.position !== positions[1]
1464
+ );
1465
+ });
1466
+
1467
+ }
1468
+ else if ((unit.left.type === 'literal' || unit.left.type === 'identifier')
1469
+ && (unit.right.type === 'literal' || unit.right.type === 'identifier')) {
1470
+
1471
+ // see if we can plausibly interpret both of these as rows or columns
1472
+
1473
+ const left = this.UnitToAddress(unit.left);
1474
+ const right = this.UnitToAddress(unit.right);
1475
+
1476
+ // and they need to match
1477
+
1478
+ if (left && right
1479
+ && ((left.column === Infinity && right.column === Infinity)
1480
+ || (left.row === Infinity && right.row === Infinity))) {
1481
+
1482
+ label = left.label + ':' + right.label;
1483
+
1484
+ // we don't support out-of-order ranges, so we should correct.
1485
+ // they just won't work otherwise. (TODO/FIXME)
1486
+
1487
+ range = {
1488
+ type: 'range',
1489
+ id: this.id_counter++,
1490
+ position: unit.left.position,
1491
+ start: left,
1492
+ end: right,
1493
+ label,
1494
+ };
1495
+
1496
+ }
1497
+
1498
+ }
1499
+
1500
+ /*
1501
+ else if ( unit.left.type === 'literal'
1502
+ && unit.right.type === 'literal'
1503
+ && typeof unit.left.value === 'number'
1504
+ && typeof unit.right.value === 'number') {
1505
+
1506
+ // technically we don't want to support any number that has
1507
+ // a decimal place, but I'm not sure we have a useful way of
1508
+ // measuring that... could look at the original text?
1509
+
1510
+ if (unit.left.value > 0
1511
+ && unit.right.value > 0
1512
+ && !/\./.test(unit.left.text||'')
1513
+ && !/\./.test(unit.right.text||'')
1514
+ ) {
1515
+
1516
+ label = unit.left.value.toString() + ':' + unit.right.value.toString();
1517
+
1518
+ console.info('m2:', label);
1519
+
1520
+ const left: UnitAddress = {
1521
+ type: 'address',
1522
+ position: unit.left.position,
1523
+ label: unit.left.value.toString(),
1524
+ row: unit.left.value - 1,
1525
+ id: this.id_counter++,
1526
+ column: Infinity,
1527
+ };
1528
+
1529
+ const right: UnitAddress = {
1530
+ type: 'address',
1531
+ position: unit.right.position,
1532
+ label: unit.right.value.toString(),
1533
+ row: unit.right.value - 1,
1534
+ id: this.id_counter++,
1535
+ column: Infinity,
1536
+ };
1537
+
1538
+ range = {
1539
+ type: 'range',
1540
+ id: this.id_counter++,
1541
+ position: unit.left.position,
1542
+ start: left,
1543
+ end: right,
1544
+ label,
1545
+ };
1546
+
1547
+ }
1548
+
1549
+ }
1550
+ */
1551
+
1552
+ if (range) {
1553
+
1554
+ this.dependencies.ranges[label] = range;
1555
+
1556
+ // and add the range
1557
+ this.full_reference_list.push(range);
1558
+
1559
+ return range;
1560
+
1561
+ }
1562
+ else {
1563
+ this.error = `unexpected character: :`;
1564
+ this.valid = false;
1565
+ // console.info('xx', unit);
1566
+ }
1567
+
1568
+ }
1569
+
1570
+ // recurse
1571
+
1572
+ unit.left = this.BinaryToRangeX(unit.left);
1573
+ unit.right = this.BinaryToRangeX(unit.right);
1574
+ }
1575
+
1576
+ // this should no longer be required, because we explicitly check
1577
+ // when we construct the unary operations...
1578
+
1579
+ // else if (unit.type === 'unary') {
1580
+ // unit.operand = this.BinaryToRange(unit.operand);
1581
+ // }
1582
+
1583
+ return unit;
1584
+ }
1585
+
1586
+ /**
1587
+ * reorders operations for precendence
1588
+ */
1589
+ protected ArrangeUnits(stream: ExpressionUnit[]): ExpressionUnit {
1590
+ // probably should not happen
1591
+ if (stream.length === 0) return { type: 'missing', id: this.id_counter++ };
1592
+
1593
+ // this is probably already covered
1594
+ if (stream.length === 1) return stream[0];
1595
+
1596
+ const stack: ExpressionUnit[] = [];
1597
+
1598
+ // work left-to-right (implied precendence), unless there
1599
+ // is actual precendence. spreadsheet language only supports
1600
+ // binary operators, so we always expect unit - operator - unit
1601
+ //
1602
+ // UPDATE: that's incorrect. SL supports unary + and - operators.
1603
+ // which makes this more complicated.
1604
+ //
1605
+ // we explicitly support unfinished expressions for the first pass
1606
+ // to build dependencies, but if they're invalid the resulting
1607
+ // parse tree isn't expected to be correct. in that case we
1608
+ // generally will pass back a bag of parts, with a flag set.
1609
+
1610
+ for (let index = 0; index < stream.length; index++) {
1611
+ let element = stream[index];
1612
+
1613
+ // given that we need to support unary operators, the logic needs
1614
+ // to be a little different. operators are OK at any position, provided
1615
+ // we can construct either a unary or binary operation.
1616
+
1617
+ if (element.type === 'operator') {
1618
+ if (stack.length === 0 || stack[stack.length - 1].type === 'operator') {
1619
+ // valid if unary operator and we can construct a unary operation.
1620
+ // in this case we do it with recursion.
1621
+
1622
+ if (unary_operators[element.operator]) {
1623
+
1624
+ // MARK X
1625
+
1626
+ // const right = this.BinaryToRange(
1627
+ // this.ArrangeUnits(stream.slice(index + 1)),
1628
+ //);
1629
+
1630
+ // const right = this.ArrangeUnits(stream.slice(index + 1));
1631
+ const right = this.BinaryToComplex(this.ArrangeUnits(stream.slice(index + 1)));
1632
+
1633
+ // this ensures we return the highest-level group, even if we recurse
1634
+ if (!this.valid) {
1635
+ return {
1636
+ type: 'group',
1637
+ id: this.id_counter++,
1638
+ elements: stream,
1639
+ explicit: false,
1640
+ };
1641
+ }
1642
+
1643
+ // if it succeeded, then we need to apply the unary operator to
1644
+ // the result, or if it's a binary operation, to the left-hand side
1645
+ // (because we have precedence) -- unless it's a range [this is now
1646
+ // handled above]
1647
+
1648
+ if (right.type === 'binary') {
1649
+ right.left = {
1650
+ type: 'unary',
1651
+ id: this.id_counter++,
1652
+ operator: element.operator,
1653
+ operand: right.left,
1654
+ position: element.position,
1655
+ } as UnitUnary;
1656
+ element = right;
1657
+ }
1658
+ else {
1659
+ // create a unary operation which will replace the element
1660
+ element = {
1661
+ type: 'unary',
1662
+ id: this.id_counter++,
1663
+ operator: element.operator,
1664
+ operand: right,
1665
+ position: element.position,
1666
+ } as UnitUnary;
1667
+ }
1668
+
1669
+ // end loop after this pass, because the recurse consumes everything else
1670
+ index = stream.length;
1671
+ }
1672
+ else {
1673
+ this.error = `unexpected character [2]: ${element.operator}`;
1674
+ this.error_position = element.position;
1675
+ this.valid = false;
1676
+ return {
1677
+ type: 'group',
1678
+ id: this.id_counter++,
1679
+ elements: stream,
1680
+ explicit: false,
1681
+ };
1682
+ }
1683
+ }
1684
+ else {
1685
+ stack.push(element);
1686
+ continue;
1687
+ }
1688
+ }
1689
+
1690
+ if (stack.length < 2) {
1691
+ stack.push(element);
1692
+ }
1693
+ else if (stack[stack.length - 1].type === 'operator') {
1694
+ const left = stack[stack.length - 2];
1695
+ const operator_unit = stack[stack.length - 1] as UnitOperator;
1696
+ const operator = operator_unit.operator;
1697
+
1698
+ // assume we can construct it as follows: [A op B]
1699
+
1700
+ const operation: ExpressionUnit = {
1701
+ type: 'binary',
1702
+ id: this.id_counter++,
1703
+ left,
1704
+ operator,
1705
+ position: operator_unit.position,
1706
+ right: element,
1707
+ };
1708
+
1709
+ // we have to reorder if left (A) is a binary operation, and the
1710
+ // precedence of the new operator is higher. note that we will
1711
+ // deal with range operations later, for now just worry about
1712
+ // operator precedence
1713
+
1714
+ if (
1715
+ left.type === 'binary' &&
1716
+ binary_operators_precendence[operator] >
1717
+ binary_operators_precendence[left.operator]
1718
+ ) {
1719
+ // so we have [[A op1 B] op2 C], and we need to re-order this into [A op1 [B op2 C]].
1720
+
1721
+ operation.left = left.left; // <- A
1722
+ operation.operator = left.operator; // <- op1
1723
+ operation.position = left.position;
1724
+ operation.right = {
1725
+ type: 'binary',
1726
+ id: this.id_counter++,
1727
+ left: left.right, // <- B
1728
+ right: element, // <- C
1729
+ operator, // <- op2
1730
+ position: operator_unit.position,
1731
+ };
1732
+ }
1733
+
1734
+ stack.splice(-2, 2, operation);
1735
+ }
1736
+ else {
1737
+ this.error = `multiple expressions`;
1738
+ this.error_position = (element as any).position;
1739
+ this.valid = false;
1740
+ return {
1741
+ type: 'group',
1742
+ id: this.id_counter++,
1743
+ elements: stream,
1744
+ explicit: false,
1745
+ };
1746
+ }
1747
+ }
1748
+
1749
+ return stack[0];
1750
+ }
1751
+
1752
+ /**
1753
+ * parses literals and tokens from the stream, ignoring whitespace,
1754
+ * and stopping on unexpected tokens (generally operators or parens).
1755
+ *
1756
+ * @param naked treat -/+ as signs (part of numbers) rather than operators.
1757
+ */
1758
+ protected ParseNext(naked = true): ExpressionUnit | number {
1759
+
1760
+ this.ConsumeWhiteSpace();
1761
+
1762
+ const char = this.data[this.index];
1763
+ if (char === DOUBLE_QUOTE) {
1764
+ return {
1765
+ type: 'literal',
1766
+ id: this.id_counter++,
1767
+ position: this.index,
1768
+ value: this.ConsumeString(),
1769
+ };
1770
+ }
1771
+ else if ((char >= ZERO && char <= NINE) || char === this.decimal_mark_char) {
1772
+ return this.ConsumeNumber();
1773
+ }
1774
+ else if (char === OPEN_BRACE) {
1775
+ return this.ConsumeArray();
1776
+ }
1777
+ else if (naked && (char === MINUS || char === PLUS)) {
1778
+
1779
+ // there's a case where you type '=-func()', which should support
1780
+ // '=+func()' as well, both of which are naked operators and not numbers.
1781
+ // the only way to figure this out is to check for a second number char.
1782
+
1783
+ // this is turning into lookahead, which we did not want to do...
1784
+
1785
+ const check = this.data[this.index + 1];
1786
+ if (
1787
+ (check >= ZERO && check <= NINE) ||
1788
+ check === this.decimal_mark_char
1789
+ ) {
1790
+ return this.ConsumeNumber();
1791
+ }
1792
+ }
1793
+ else if (
1794
+ (char >= UC_A && char <= UC_Z) ||
1795
+ (char >= LC_A && char <= LC_Z) ||
1796
+ char === UNDERSCORE ||
1797
+ char === HASH || // new: only allowed in position 1, always an error
1798
+ char === SINGLE_QUOTE ||
1799
+ char === DOLLAR_SIGN ||
1800
+
1801
+ // we used to not allow square brackets to start tokens, because
1802
+ // we only supported them for relative R1C1 references -- hence you'd
1803
+ // need the R first. but we now allow them for "structured references".
1804
+
1805
+ char === OPEN_SQUARE_BRACKET ||
1806
+
1807
+ (char >= ACCENTED_RANGE_START && char <= ACCENTED_RANGE_END) // adding accented characters, needs some testing
1808
+ ) {
1809
+
1810
+ return this.ConsumeToken(char);
1811
+ }
1812
+
1813
+ // else throw(new Error('Unexpected character: ' + char));
1814
+ return char;
1815
+ }
1816
+
1817
+ protected ConsumeArray(): ExpressionUnit {
1818
+
1819
+ const expression: UnitArray = {
1820
+ type: 'array',
1821
+ id: this.id_counter++,
1822
+ values: [],
1823
+ position: this.index,
1824
+ };
1825
+
1826
+ this.index++;
1827
+
1828
+ let row = 0;
1829
+ let column = 0;
1830
+
1831
+ while (this.index < this.length) {
1832
+ const item = this.ParseNext();
1833
+ const start_position = this.index;
1834
+
1835
+ if (typeof item === 'number') {
1836
+ this.index++;
1837
+ switch (item) {
1838
+
1839
+ case SEMICOLON:
1840
+ //column = 0;
1841
+ //row++;
1842
+ column++;
1843
+ row = 0;
1844
+ break;
1845
+
1846
+ case COMMA:
1847
+ //column++;
1848
+ row++;
1849
+ break;
1850
+
1851
+ case CLOSE_BRACE:
1852
+ return expression;
1853
+
1854
+ default:
1855
+ if (this.valid) {
1856
+ this.error = `invalid character in array literal`;
1857
+ this.error_position = start_position;
1858
+ this.valid = false;
1859
+ }
1860
+ break;
1861
+ }
1862
+ }
1863
+ else {
1864
+ switch (item.type) {
1865
+ case 'literal':
1866
+ if (!expression.values[row]) { expression.values[row] = []; }
1867
+ expression.values[row][column] = item.value;
1868
+ break;
1869
+ default:
1870
+ if (this.valid) {
1871
+ this.error = `invalid value in array literal`;
1872
+ this.error_position = start_position;
1873
+ this.valid = false;
1874
+ }
1875
+ break;
1876
+ }
1877
+ }
1878
+ }
1879
+
1880
+ return expression;
1881
+
1882
+ }
1883
+
1884
+ protected ConsumeOperator(): ExpressionUnit | null {
1885
+ for (const operator of binary_operators) {
1886
+ if (this.expression.substr(this.index, operator.length) === operator) {
1887
+ const position = this.index;
1888
+ this.index += operator.length;
1889
+ return {
1890
+ type: 'operator',
1891
+ id: this.id_counter++,
1892
+ operator,
1893
+ position,
1894
+ };
1895
+ }
1896
+ }
1897
+ return null;
1898
+ }
1899
+
1900
+ /** consume function arguments, which can be of any type */
1901
+ protected ConsumeArguments(): ExpressionUnit[] {
1902
+ this.index++; // open paren
1903
+
1904
+ let argument_index = 0;
1905
+ const args: ExpressionUnit[] = [];
1906
+
1907
+ for (; this.index < this.length;) {
1908
+ const unit = this.ParseGeneric([
1909
+ this.argument_separator_char,
1910
+ CLOSE_PAREN,
1911
+ ]);
1912
+ if (null !== unit) args.push(unit);
1913
+
1914
+ // why did parsing stop?
1915
+ const char = this.data[this.index];
1916
+
1917
+ if (char === this.argument_separator_char) {
1918
+ this.index++;
1919
+ argument_index++;
1920
+ for (let i = args.length; i < argument_index; i++) {
1921
+ args.push({ type: 'missing', id: this.id_counter++ });
1922
+ }
1923
+ }
1924
+ else if (char === CLOSE_PAREN) {
1925
+ this.index++;
1926
+ return args;
1927
+ }
1928
+ // else console.info('UNEXPECTED (CA)', char);
1929
+ }
1930
+
1931
+ return args;
1932
+ }
1933
+
1934
+ /**
1935
+ * consume token. also checks for function call, because parens
1936
+ * have a different meaning (grouping/precedence) when they appear
1937
+ * not immediately after a token.
1938
+ *
1939
+ * regarding periods: as long as there's no intervening whitespace
1940
+ * or operator, period should be a valid token character. tokens
1941
+ * cannot start with a period.
1942
+ *
1943
+ * NOTE: that's true irrespective of decimal mark type.
1944
+ *
1945
+ * you can have tokens (addresses) with single quotes; these are used
1946
+ * to escape sheet names with spaces (which is a bad idea, but hey). this
1947
+ * should only be legal if the token starts with a single quote, and only
1948
+ * for one (closing) quote.
1949
+ *
1950
+ * R1C1 relative notation uses square brackets, like =R2C[-1] or =R[-1]C[-2].
1951
+ * that's pretty easy to see. there's also regular R1C1, like =R1C1.
1952
+ *
1953
+ * "structured references" use square brackets. they can start with
1954
+ * square brackets -- in that case the table source is implicit (has to
1955
+ * be in the table). otherwise they look like =TableName[@ColumnName]. that
1956
+ * @ is optional and (I think) means don't spill.
1957
+ *
1958
+ */
1959
+ protected ConsumeToken(initial_char: number): ExpressionUnit {
1960
+
1961
+ const token: number[] = [initial_char];
1962
+ const position = this.index;
1963
+
1964
+ let single_quote = (initial_char === SINGLE_QUOTE);
1965
+ let square_bracket = 0; // now balancing // false; // this one can't be initial
1966
+
1967
+ // this is a set-once flag for square brackets; it can
1968
+ // short-circuit the check for structured references.
1969
+ let braces = false;
1970
+
1971
+ // also watch first char
1972
+ if (initial_char === OPEN_SQUARE_BRACKET) {
1973
+ square_bracket = 1;
1974
+ braces = true;
1975
+ }
1976
+
1977
+ for (++this.index; this.index < this.length; this.index++) {
1978
+ const char = this.data[this.index];
1979
+ if (
1980
+ (char >= UC_A && char <= UC_Z) ||
1981
+ (char >= LC_A && char <= LC_Z) ||
1982
+ (char >= ACCENTED_RANGE_START && char <= ACCENTED_RANGE_END) ||
1983
+ char === UNDERSCORE ||
1984
+ char === DOLLAR_SIGN ||
1985
+ char === PERIOD ||
1986
+ char === EXCLAMATION_MARK ||
1987
+ single_quote || // ((char === SINGLE_QUOTE || char === SPACE) && single_quote) ||
1988
+ (char >= ZERO && char <= NINE) // tokens can't start with a number, but this loop starts at index 1
1989
+
1990
+ // we now allow square brackets for structured references;
1991
+ // minus is still only allowed in R1C1 references, so keep
1992
+ // that restriction
1993
+
1994
+ || char === OPEN_SQUARE_BRACKET
1995
+ || (square_bracket > 0 && char === CLOSE_SQUARE_BRACKET)
1996
+ || (char === MINUS && this.flags.r1c1 && (square_bracket === 1))
1997
+
1998
+ // the @ sign can appear after the first square bracket...
1999
+ // but only immediately?
2000
+
2001
+ || (square_bracket > 0 && char === AT && this.data[this.index - 1] === OPEN_SQUARE_BRACKET)
2002
+
2003
+ // comma can appear in the first level. this is maybe an older
2004
+ // syntax? it looks like `Table2[[#this row],[region]]
2005
+
2006
+ || (square_bracket === 1 && (char === COMMA || char === SPACE))
2007
+
2008
+ // structured references allow basically any character, if
2009
+ // it's in the SECOND bracket. not sure what's up with that.
2010
+
2011
+ || (square_bracket > 1)
2012
+
2013
+ // I think that's all the rules for structured references.
2014
+
2015
+ /*
2016
+
2017
+ || (this.flags.r1c1 && (
2018
+ char === OPEN_SQUARE_BRACKET ||
2019
+ char === CLOSE_SQUARE_BRACKET ||
2020
+ (char === MINUS && square_bracket)
2021
+ ))
2022
+ */
2023
+
2024
+
2025
+
2026
+
2027
+ ) {
2028
+ token.push(char);
2029
+
2030
+ if (char === OPEN_SQUARE_BRACKET) {
2031
+ // square_bracket = true;
2032
+ square_bracket++;
2033
+ braces = true;
2034
+ }
2035
+ if (char === CLOSE_SQUARE_BRACKET) {
2036
+ // square_bracket = false;
2037
+ square_bracket--;
2038
+ }
2039
+
2040
+ if (char === SINGLE_QUOTE) {
2041
+ single_quote = false; // one only
2042
+ }
2043
+ }
2044
+
2045
+ else break;
2046
+ }
2047
+
2048
+ const str = token.map((num) => String.fromCharCode(num)).join('');
2049
+
2050
+ // special handling: unbalanced single quote (probably sheet name),
2051
+ // this is an error
2052
+
2053
+ if (single_quote) { // unbalanced
2054
+
2055
+ this.error = `unbalanced single quote`;
2056
+ this.error_position = position;
2057
+ this.valid = false;
2058
+
2059
+ return {
2060
+ type: 'identifier',
2061
+ id: this.id_counter++,
2062
+ name: str,
2063
+ position,
2064
+ } as UnitIdentifier;
2065
+
2066
+ }
2067
+
2068
+ // check unbalanced square bracket as well, could be a runaway structured
2069
+ // reference
2070
+
2071
+ if (square_bracket) {
2072
+
2073
+ this.error = `unbalanced square bracket`;
2074
+ this.error_position = position;
2075
+ this.valid = false;
2076
+
2077
+ return {
2078
+ type: 'identifier',
2079
+ id: this.id_counter++,
2080
+ name: str,
2081
+ position,
2082
+ } as UnitIdentifier;
2083
+
2084
+ }
2085
+
2086
+ // special handling
2087
+
2088
+ if (str.toLowerCase() === 'true') {
2089
+ return {
2090
+ type: 'literal',
2091
+ id: this.id_counter++,
2092
+ value: true,
2093
+ position,
2094
+ };
2095
+ }
2096
+ if (str.toLowerCase() === 'false') {
2097
+ return {
2098
+ type: 'literal',
2099
+ id: this.id_counter++,
2100
+ value: false,
2101
+ position,
2102
+ };
2103
+ }
2104
+
2105
+ // function takes precendence over address? I guess so
2106
+
2107
+ this.ConsumeWhiteSpace();
2108
+
2109
+ const next_char = this.data[this.index];
2110
+ if (next_char === OPEN_PAREN) {
2111
+ const args = this.ConsumeArguments();
2112
+ return {
2113
+ type: 'call',
2114
+ id: this.id_counter++,
2115
+ name: str,
2116
+ args,
2117
+ position,
2118
+ };
2119
+ }
2120
+
2121
+ if (this.flags.spreadsheet_semantics) {
2122
+
2123
+ // check for address. in the case of a range, we'll see an address, the
2124
+ // range operator, and a second address. that will be turned into a range
2125
+ // later.
2126
+
2127
+ const address = this.ConsumeAddress(str, position);
2128
+ if (address) return address;
2129
+
2130
+ // check for structured reference, if we had square brackets
2131
+
2132
+ if (braces) {
2133
+ const structured = this.ConsumeStructuredReference(str, position);
2134
+ if (structured) {
2135
+ return structured;
2136
+ }
2137
+ }
2138
+
2139
+ }
2140
+
2141
+
2142
+
2143
+ const identifier: UnitIdentifier = {
2144
+ type: 'identifier',
2145
+ id: this.id_counter++,
2146
+ name: str,
2147
+ position,
2148
+ };
2149
+
2150
+ this.full_reference_list.push(identifier);
2151
+
2152
+ return identifier;
2153
+ }
2154
+
2155
+ /**
2156
+ * like ConsumeAddress, look for a structured reference.
2157
+ */
2158
+ protected ConsumeStructuredReference(token: string, position: number): UnitStructuredReference|undefined {
2159
+
2160
+ // structured references look something like
2161
+ //
2162
+ // [@Column1]
2163
+ // [@[Column with spaces]]
2164
+ // [[#This Row],[Column2]]
2165
+ //
2166
+ // @ means the same as [#This Row]. there are probably other things
2167
+ // that use the # syntax, but I haven't seen them yet.
2168
+ //
2169
+ // some observations: case is not matched for the "this row" text.
2170
+ // I think that's true of column names as well, but that's not relevant
2171
+ // at this stage. whitespace around that comma is ignored. I _think_
2172
+ // whitespace around column names is also ignored, but spaces within
2173
+ // a column name are OK, at least within the second set of brackets.
2174
+
2175
+ const index = position;
2176
+ const token_length = token.length;
2177
+
2178
+ const label = token;
2179
+
2180
+ let table = '';
2181
+ let i = 0;
2182
+
2183
+ for (; i < token_length; i++) {
2184
+ if (token[i] === '[') {
2185
+ token = token.substring(i);
2186
+ break;
2187
+ }
2188
+ table += token[i];
2189
+ }
2190
+
2191
+ // after the table, must start and end with brackets
2192
+
2193
+ if (token[0] !== '[' || token[token.length - 1] !== ']') {
2194
+ return undefined;
2195
+ }
2196
+
2197
+ token = token.substring(1, token.length - 1);
2198
+ const parts = token.split(',').map(part => part.trim());
2199
+
2200
+ let scope: 'row'|'all'|'column' = 'column';
2201
+
2202
+ // let this_row = false;
2203
+ let column = '';
2204
+
2205
+ if (parts.length > 2) {
2206
+ return undefined; // ??
2207
+ }
2208
+ else if (parts.length === 2) {
2209
+ if (/\[#this row\]/i.test(parts[0])) {
2210
+ scope = 'row';
2211
+ }
2212
+ else if (/\[#all\]/i.test(parts[0])) {
2213
+ scope = 'all';
2214
+ }
2215
+ column = parts[1];
2216
+ }
2217
+ else {
2218
+ column = parts[0];
2219
+ if (column[0] === '@') {
2220
+ scope = 'row';
2221
+ column = column.substring(1, column.length);
2222
+ }
2223
+ }
2224
+
2225
+ if (column[0] === '[' && column[column.length - 1] === ']') {
2226
+ column = column.substring(1, column.length - 1);
2227
+ }
2228
+
2229
+ const reference: UnitStructuredReference = {
2230
+ type: 'structured-reference',
2231
+ id: this.id_counter++,
2232
+ label,
2233
+ position,
2234
+ scope,
2235
+ column,
2236
+ table,
2237
+ };
2238
+
2239
+ // console.info(reference);
2240
+
2241
+ this.full_reference_list.push(reference);
2242
+
2243
+ return reference;
2244
+
2245
+ }
2246
+
2247
+ /**
2248
+ * consumes address. this is outside of the normal parse flow;
2249
+ * we already have a token, here we're checking if it's an address.
2250
+ *
2251
+ * this used to check for ranges as well, but we now treat ranges as
2252
+ * an operation on two addresses; that supports whitespace between the
2253
+ * tokens.
2254
+ *
2255
+ * FIXME: that means we can now inline the column/row routines, since
2256
+ * they are not called more than once
2257
+ */
2258
+ protected ConsumeAddress(
2259
+ token: string,
2260
+ position: number,
2261
+ ): UnitAddress | null {
2262
+ const index = position;
2263
+ const token_length = token.length;
2264
+
2265
+ // FIXME: should mark this (!) when it hits, rather than search
2266
+
2267
+ // UPDATE: ! is legal in sheet names, although it needs to be quoted.
2268
+
2269
+ let sheet: string | undefined;
2270
+ const tokens = token.split('!');
2271
+
2272
+ if (tokens.length > 1) {
2273
+ sheet = tokens.slice(0, tokens.length - 1).join('!');
2274
+ position += sheet.length + 1;
2275
+ }
2276
+
2277
+ // handle first
2278
+
2279
+ if (this.flags.r1c1) {
2280
+
2281
+ const match = tokens[tokens.length - 1].match(this.r1c1_regex);
2282
+ if (match) {
2283
+
2284
+ const r1c1: UnitAddress = {
2285
+ type: 'address',
2286
+ id: this.id_counter++,
2287
+ label: token, // TODO
2288
+ row: 0,
2289
+ column: 0,
2290
+ // absolute_row: false, // TODO: is this supported?
2291
+ // absolute_column: false, // TODO: is this supported?
2292
+ position: index,
2293
+ sheet,
2294
+ r1c1: true,
2295
+ };
2296
+
2297
+ if (match[1][0] === '[') { // relative
2298
+ r1c1.offset_row = true;
2299
+ r1c1.row = Number(match[1].substring(1, match[1].length - 1));
2300
+ }
2301
+ else { // absolute
2302
+ r1c1.row = Number(match[1]) - 1; // R1C1 is 1-based
2303
+ }
2304
+
2305
+ if (match[2][0] === '[') { // relative
2306
+ r1c1.offset_column = true;
2307
+ r1c1.column = Number(match[2].substring(1, match[2].length - 1));
2308
+ }
2309
+ else { // absolute
2310
+ r1c1.column = Number(match[2]) - 1; // R1C1 is 1-based
2311
+ }
2312
+
2313
+ return r1c1;
2314
+
2315
+ }
2316
+ }
2317
+
2318
+ // FIXME: can inline
2319
+
2320
+ const c = this.ConsumeAddressColumn(position);
2321
+ if (!c) return null;
2322
+ position = c.position;
2323
+
2324
+ const r = this.ConsumeAddressRow(position);
2325
+ if (!r) return null;
2326
+ position = r.position;
2327
+
2328
+ const label = sheet ?
2329
+ sheet + token.substr(sheet.length, position - index).toUpperCase() :
2330
+ token.substr(0, position - index).toUpperCase();
2331
+
2332
+ if (sheet && sheet[0] === '\'') {
2333
+ sheet = sheet.substr(1, sheet.length - 2);
2334
+ }
2335
+
2336
+ const addr: UnitAddress = {
2337
+ type: 'address',
2338
+ id: this.id_counter++,
2339
+ label, // : token.substr(0, position - index).toUpperCase(),
2340
+ row: r.row,
2341
+ column: c.column,
2342
+ absolute_row: r.absolute,
2343
+ absolute_column: c.absolute,
2344
+ position: index,
2345
+ sheet,
2346
+ };
2347
+
2348
+ // if that's not the complete token, then it's invalid
2349
+
2350
+ if (token_length !== position - index) return null;
2351
+
2352
+ // store ref, increment count
2353
+
2354
+ this.dependencies.addresses[addr.label] = addr;
2355
+ this.address_refcount[addr.label] =
2356
+ (this.address_refcount[addr.label] || 0) + 1;
2357
+
2358
+ // add to new address list. use the actual object (not a clone or copy);
2359
+ // we update the list later, and we may want to remove it (if it turns
2360
+ // out it's part of a range)
2361
+
2362
+ this.full_reference_list.push(addr);
2363
+
2364
+ return addr;
2365
+ }
2366
+
2367
+ /**
2368
+ * consumes a row, possibly absolute ($). returns the numeric row
2369
+ * (0-based) and metadata
2370
+ */
2371
+ protected ConsumeAddressRow(position: number):
2372
+ {
2373
+ absolute: boolean;
2374
+ row: number;
2375
+ position: number;
2376
+ }|false {
2377
+
2378
+ const absolute = this.data[position] === DOLLAR_SIGN;
2379
+ if (absolute) position++;
2380
+
2381
+ const start = position;
2382
+ let value = 0;
2383
+
2384
+ for (; ; position++) {
2385
+ const char = this.data[position];
2386
+ if (char >= ZERO && char <= NINE) {
2387
+ value *= 10;
2388
+ value += char - ZERO;
2389
+ }
2390
+ else break;
2391
+ }
2392
+
2393
+ if (start === position) return false;
2394
+ return { absolute, row: value - 1, position };
2395
+ }
2396
+
2397
+ /**
2398
+ * consumes a column, possibly absolute ($). returns the numeric
2399
+ * column (0-based) and metadata
2400
+ */
2401
+ protected ConsumeAddressColumn(position: number):
2402
+ {
2403
+ absolute: boolean;
2404
+ column: number;
2405
+ position: number;
2406
+ }|false {
2407
+
2408
+ let column = -1; // clever
2409
+ let length = 0; // max 3 chars for column
2410
+
2411
+ const absolute = this.data[position] === DOLLAR_SIGN;
2412
+ if (absolute) position++;
2413
+
2414
+ for (; ; position++, length++) {
2415
+ if (length >= 4) return false; // max 3 chars for column
2416
+
2417
+ const char = this.data[position];
2418
+ if (char >= UC_A && char <= UC_Z) {
2419
+ column = 26 * (1 + column) + (char - UC_A);
2420
+ }
2421
+ else if (char >= LC_A && char <= LC_Z) {
2422
+ column = 26 * (1 + column) + (char - LC_A);
2423
+ }
2424
+ else break;
2425
+ }
2426
+
2427
+ if (column < 0) return false;
2428
+ return { absolute, column, position };
2429
+ }
2430
+
2431
+ /**
2432
+ * consumes number. supported formats (WIP):
2433
+ *
2434
+ * -3
2435
+ * +3
2436
+ * 100.9
2437
+ * 10.0%
2438
+ * 1e-2.2
2439
+ *
2440
+ * ~1,333,123.22~
2441
+ *
2442
+ * UPDATE: commas (separators) are not acceptable in numbers passed
2443
+ * in formulae, can't distinguish between them and function argument
2444
+ * separators.
2445
+ *
2446
+ * regarding the above, a couple of rules:
2447
+ *
2448
+ * 1. +/- is only legal in position 0 or immediately after e/E
2449
+ * 2. only one decimal point is allowed.
2450
+ * 3. any number of separators, in any position, are legal, but
2451
+ * only before the decimal point.
2452
+ * 4. only one % is allowed, and only in the last position
2453
+ *
2454
+ * NOTE: this is probably going to break on unfinished strings that
2455
+ * end in - or +... if they're not treated as operators...
2456
+ *
2457
+ * FIXME: find test cases for that so we can fix it
2458
+ *
2459
+ * UPDATE: exporting original text string for preservation/insertion.
2460
+ * this function now returns a tuple of [value, text].
2461
+ *
2462
+ * UPDATE: we now (at least in a branch) consume complex numbers. the last
2463
+ * element of the return array is a boolean which is set if the value is an
2464
+ * imaginary number. when parsing, we will only see the imaginary part;
2465
+ * we'll use a separate step to put complex numbers together.
2466
+ *
2467
+ *
2468
+ */
2469
+ protected ConsumeNumber(): ExpressionUnit { // [number, string, boolean] {
2470
+
2471
+ const starting_position = this.index;
2472
+
2473
+ // for exponential notation
2474
+ let exponent = 0;
2475
+ let negative_exponent = false;
2476
+
2477
+ // general
2478
+ let negative = false;
2479
+ let integer = 0;
2480
+ let decimal = 0;
2481
+ let fraction = 0;
2482
+
2483
+ let state: 'integer' | 'fraction' | 'exponent' = 'integer';
2484
+ let position = 0;
2485
+
2486
+ let imaginary = false;
2487
+
2488
+ const start_index = this.index;
2489
+
2490
+ for (; this.index < this.length; this.index++, position++) {
2491
+ const char = this.data[this.index];
2492
+
2493
+ if (char === this.decimal_mark_char) {
2494
+ if (state === 'integer') state = 'fraction';
2495
+ else break; // end of token; not consuming
2496
+ }
2497
+ else if (char === PERCENT) {
2498
+ // FIXME: disallow combination of exponential and percent notation
2499
+
2500
+ integer /= 100; // this is a dumb way to do this
2501
+ fraction /= 100;
2502
+
2503
+ this.index++; // we are consuming
2504
+ break; // end of token
2505
+ }
2506
+ else if (char === PLUS || char === MINUS) {
2507
+ // NOTE: handling of positive/negative exponent in exponential
2508
+ // notation is handled separately, see below
2509
+
2510
+ if (position === 0) {
2511
+ if (char === MINUS) negative = true;
2512
+ }
2513
+ else break; // end of token -- not consuming
2514
+ }
2515
+ else if (char === UC_E || char === LC_E) {
2516
+ if (state === 'integer' || state === 'fraction') {
2517
+ state = 'exponent';
2518
+ if (this.index < this.length - 1) {
2519
+ if (this.data[this.index + 1] === PLUS) this.index++;
2520
+ else if (this.data[this.index + 1] === MINUS) {
2521
+ this.index++;
2522
+ negative_exponent = true;
2523
+ }
2524
+ }
2525
+ }
2526
+ else break; // not sure what this is, then
2527
+ }
2528
+ else if (char === this.imaginary_char) {
2529
+
2530
+ // FIXME: this should only be set if it's exactly '8i' and not '8in',
2531
+ // since we want to use that for dimensioned quantities. what's legit
2532
+ // after the i and what is not? let's exclude anything in the "word"
2533
+ // range...
2534
+
2535
+ // peek
2536
+ const peek = this.data[this.index + 1];
2537
+ if ((peek >= UC_A && peek <= UC_Z) ||
2538
+ (peek >= LC_A && peek <= LC_Z) ||
2539
+ (peek >= ACCENTED_RANGE_START && peek <= ACCENTED_RANGE_END) ||
2540
+ peek === UNDERSCORE) {
2541
+
2542
+ break; // start of an identifier
2543
+ }
2544
+
2545
+ // actually we could use our dimension logic instead of this... turn
2546
+ // this off when using dimensioned quantities and move it in there?
2547
+
2548
+ if (state === 'integer' || state === 'fraction') {
2549
+ this.index++; // consume
2550
+ imaginary = true;
2551
+ break; // end of token
2552
+ }
2553
+ }
2554
+ else if (char >= ZERO && char <= NINE) {
2555
+ switch (state) {
2556
+ case 'integer':
2557
+ integer = integer * 10 + (char - ZERO);
2558
+ break;
2559
+ case 'fraction':
2560
+ fraction = fraction * 10 + (char - ZERO);
2561
+ decimal++;
2562
+ break;
2563
+ case 'exponent':
2564
+ exponent = exponent * 10 + (char - ZERO);
2565
+ break;
2566
+ }
2567
+ }
2568
+ else break;
2569
+ }
2570
+
2571
+ // NOTE: multiplying returns fp noise, but dividing does not? need
2572
+ // to check more browsers... maybe we should store the value in some
2573
+ // other form? (that's a larger TODO)
2574
+
2575
+ // let value = integer + fraction * Math.pow(10, -decimal);
2576
+ let value = integer + fraction / (Math.pow(10, decimal)); // <- this is cleaner?
2577
+
2578
+ if (state === 'exponent') {
2579
+ value = value * Math.pow(10, (negative_exponent ? -1 : 1) * exponent);
2580
+ }
2581
+
2582
+ // const text = this.expression.substring(start_index, this.index) || '';
2583
+ // return [negative ? -value : value, text, imaginary];
2584
+
2585
+ if (imaginary) {
2586
+ return {
2587
+ type: 'complex',
2588
+ id: this.id_counter++,
2589
+ position: starting_position,
2590
+ imaginary: negative ? -value : value,
2591
+ real: 0,
2592
+ text: this.expression.substring(start_index, this.index) || '',
2593
+ };
2594
+
2595
+ }
2596
+ else {
2597
+ return {
2598
+ type: 'literal',
2599
+ id: this.id_counter++,
2600
+ position: starting_position,
2601
+ value: negative ? -value : value,
2602
+ text: this.expression.substring(start_index, this.index) || '',
2603
+ };
2604
+
2605
+ }
2606
+
2607
+ /*
2608
+ return {
2609
+ type: imaginary ? 'imaginary' : 'literal',
2610
+ id: this.id_counter++,
2611
+ position: starting_position,
2612
+ value: negative ? -value : value,
2613
+ text: this.expression.substring(start_index, this.index) || '',
2614
+ };
2615
+ */
2616
+
2617
+ }
2618
+
2619
+ /**
2620
+ * in spreadsheet language ONLY double-quoted strings are legal. there
2621
+ * are no escape characters, and a backslash is a legal character. to
2622
+ * embed a quotation mark, use "" (double-double quote); that's an escaped
2623
+ * double-quote.
2624
+ */
2625
+ protected ConsumeString(): string {
2626
+ this.index++; // open quote
2627
+ const str: number[] = [];
2628
+
2629
+ for (; this.index < this.length; this.index++) {
2630
+ const char = this.data[this.index];
2631
+ if (char === DOUBLE_QUOTE) {
2632
+ // always do this: either it's part of the string (and
2633
+ // we want to skip the next one), or it's the end of the
2634
+ // string and we want to close the literal.
2635
+
2636
+ this.index++;
2637
+
2638
+ // check for an escaped double-quote; otherwise close the string
2639
+ // note (1) we already incremented, so check the current value,
2640
+ // and (2) it will increment again on the loop pass so it will
2641
+ // drop the extra one. I note these because this was confusing to
2642
+ // write.
2643
+
2644
+ if (
2645
+ this.index >= this.length ||
2646
+ this.data[this.index] !== DOUBLE_QUOTE
2647
+ ) {
2648
+ break;
2649
+ }
2650
+ }
2651
+ str.push(char);
2652
+ }
2653
+
2654
+ return str.map((char) => String.fromCharCode(char)).join('');
2655
+ }
2656
+
2657
+ /** run through any intervening whitespace */
2658
+ protected ConsumeWhiteSpace(): void {
2659
+ for (; this.index < this.length;) {
2660
+ const char = this.data[this.index];
2661
+ if (
2662
+ char === SPACE ||
2663
+ char === TAB ||
2664
+ char === CR ||
2665
+ char === LF ||
2666
+ char === NON_BREAKING_SPACE
2667
+ ) {
2668
+ this.index++;
2669
+ }
2670
+ else return;
2671
+ }
2672
+ }
2673
+ }