@trebco/treb 23.6.5 → 25.0.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +323 -271
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +49 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1228 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5358 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +298 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,723 @@
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 { NumberFormatSection } from './number_format_section';
23
+ import { TextPartFlag, TextPart } from 'treb-base-types';
24
+ // import { NumberFormat } from './format';
25
+
26
+ const ASTERISK = 0x2A; // TODO
27
+ const UNDERSCORE = 0x5F; // TODO
28
+
29
+ const QUESTION_MARK = 0x3F;
30
+ const ZERO = 0x30;
31
+ const PERIOD = 0x2E;
32
+ // const SPACE = 0x20;
33
+ const COMMA = 0x2C;
34
+ const PERCENT = 0x25;
35
+ const DOUBLE_QUOTE = 0x22;
36
+ const NUMBER_SIGN = 0x23;
37
+ const SEMICOLON = 0x3B;
38
+ const BACKSLASH = 0x5C;
39
+ // const FORWARDSLASH = 0x2F;
40
+ const AT = 0x40;
41
+ const LEFT_BRACE = 0x5B;
42
+ const RIGHT_BRACE = 0x5D;
43
+
44
+ const UPPERCASE_E = 0x45;
45
+ const LOWERCASE_E = 0x65;
46
+
47
+ const UPPERCASE_H = 0x48;
48
+ const LOWERCASE_H = 0x68;
49
+ const UPPERCASE_M = 0x4D;
50
+ const LOWERCASE_M = 0x6D;
51
+ const UPPERCASE_S = 0x53;
52
+ const LOWERCASE_S = 0x73;
53
+ const UPPERCASE_D = 0x44;
54
+ const LOWERCASE_D = 0x64;
55
+ const UPPERCASE_Y = 0x59;
56
+ const LOWERCASE_Y = 0x79;
57
+ const UPPERCASE_A = 0x41;
58
+ const LOWERCASE_A = 0x61;
59
+
60
+ enum NumberPart {
61
+ Integer = 0,
62
+ Decimal = 1,
63
+ }
64
+
65
+ export class FormatParser {
66
+
67
+ protected static date_pattern = false;
68
+ protected static pattern = '';
69
+ protected static char_index = 0;
70
+ protected static characters: number[] = [];
71
+ protected static sections: NumberFormatSection[] = [];
72
+ protected static current_section: NumberFormatSection = new NumberFormatSection();
73
+ protected static preserve_formatting_characters = false; // true;
74
+
75
+ // FIXME: localization
76
+
77
+ protected static decimal_mark = PERIOD;
78
+ protected static group_separator = COMMA;
79
+
80
+ /**
81
+ * parser is static (essentially a singleton). state is ephemeral.
82
+ *
83
+ * it's a little hard to unify parsing for dates and numbers.
84
+ * luckily we don't have to parse that often; only when a format
85
+ * is created. so we will do some extra work here.
86
+ */
87
+ public static Parse(pattern: string): NumberFormatSection[] {
88
+
89
+ // local
90
+ this.pattern = pattern;
91
+
92
+ // convert to numbers
93
+ this.characters = pattern.split('').map((char) => char.charCodeAt(0));
94
+
95
+ // pointer
96
+ this.char_index = 0;
97
+
98
+ // allocate initial section
99
+ this.current_section = new NumberFormatSection();
100
+ this.sections = [this.current_section];
101
+
102
+ // check if it's a date, if so we can move on
103
+ if (this.ParseDatePattern()) {
104
+ return this.sections;
105
+ }
106
+
107
+ // not a date; reset and try again
108
+ this.char_index = 0;
109
+ this.current_section = new NumberFormatSection();
110
+ this.sections = [this.current_section];
111
+
112
+ // parse
113
+ while (this.char_index < this.characters.length) {
114
+ this.ConsumeChar();
115
+ }
116
+
117
+ // result
118
+ return this.sections;
119
+
120
+ }
121
+
122
+ protected static ConsumeString(): string {
123
+ let text = '';
124
+ if (this.preserve_formatting_characters) {
125
+ text += this.pattern[this.char_index]; // "
126
+ }
127
+ for (++this.char_index; this.char_index < this.characters.length; this.char_index++) {
128
+ const char = this.characters[this.char_index];
129
+ switch (char) {
130
+ case BACKSLASH: // escape character
131
+ if (this.preserve_formatting_characters) {
132
+ text += this.pattern[this.char_index];
133
+ }
134
+ if ((this.char_index + 1) < this.characters.length) {
135
+ text += this.pattern[++this.char_index];
136
+ }
137
+ break;
138
+ case DOUBLE_QUOTE:
139
+ if (this.preserve_formatting_characters) {
140
+ text += this.pattern[this.char_index]; // "
141
+ }
142
+ this.char_index++;
143
+ return text;
144
+ default:
145
+ text += this.pattern[this.char_index];
146
+ break;
147
+ }
148
+ }
149
+ throw new Error('unterminated string');
150
+ }
151
+
152
+ protected static ConsumeFormatting(): string {
153
+ let text = '';
154
+ for (++this.char_index; this.char_index < this.characters.length; this.char_index++) {
155
+ const char = this.characters[this.char_index];
156
+ switch (char) {
157
+ case BACKSLASH:
158
+ throw new Error('invalid escape character in formatting block');
159
+
160
+ case RIGHT_BRACE:
161
+ this.char_index++;
162
+ return text;
163
+
164
+ default:
165
+ text += this.pattern[this.char_index];
166
+ break;
167
+ }
168
+ }
169
+ throw new Error('unterminated format');
170
+ }
171
+
172
+ /**
173
+ * pre-scan for fractional format, check for legal/illegal chars.
174
+ * fraction format has an optional integer, spaces, then the fractional
175
+ * part.
176
+ *
177
+ * except for the denominator, all characters are represented as # or ?,
178
+ * but formats seem to be a little forgiving (not sure we have to be).
179
+ * essentially, should look something like
180
+ * ```
181
+ * # ##/##
182
+ * ? ??/??
183
+ * #/32
184
+ * #/64
185
+ * # #/16
186
+ * ```
187
+ */
188
+ protected static ScanFractionFormat(): boolean {
189
+
190
+ const fraction_regex = /^([#?]+ +){0,1}([#?]+)\/([#?0-9]+)(?:$|[^#?0-9])/;
191
+ const text = this.pattern.substr(this.char_index);
192
+
193
+ const match = text.match(fraction_regex);
194
+ if (!match) {
195
+ return false;
196
+ }
197
+
198
+ const len = (match[1] || '').length + match[2].length + match[3].length + 1;
199
+
200
+ // flag
201
+ this.current_section.fraction_format = true;
202
+
203
+ // has integer section
204
+ this.current_section.fraction_integer = !!match[1];
205
+
206
+ // fixed denominator
207
+ const fixed_denominator = Number(match[3]);
208
+ if (!isNaN(fixed_denominator)) {
209
+ this.current_section.fraction_denominator = fixed_denominator;
210
+ }
211
+
212
+ // we do this regardless; it's used when collapsing values to zero
213
+ this.current_section.decimal_max_digits = this.current_section.fraction_denominator_digits = match[3].length;
214
+
215
+ this.char_index += len;
216
+ this.current_section.has_number_format = true;
217
+
218
+ return true;
219
+ }
220
+
221
+ /**
222
+ * number format proper contains only the following characters:
223
+ * +-0#.,
224
+ * anything else will be ignored
225
+ *
226
+ * [UPDATE] fractional number formats can contain spaces and
227
+ * the / character (in fact they would have to contain that).
228
+ *
229
+ */
230
+ protected static ConsumeNumberFormat(): void {
231
+
232
+ let number_part = NumberPart.Integer;
233
+
234
+ for (this.char_index; this.char_index < this.characters.length; this.char_index++) {
235
+ const char = this.characters[this.char_index];
236
+ switch (char) {
237
+
238
+ case this.group_separator:
239
+ {
240
+ // the behavior of this token is different at the end of the number
241
+ // format. in that case, each comma represents 'scale by 1000'. so
242
+ // we need to do lookahead... but we only one character?
243
+
244
+ let lookahead_digit = false;
245
+ for (let i = this.char_index + 1; !lookahead_digit && i < this.characters.length; i++) {
246
+ const next_char = this.characters[i];
247
+ if (next_char === this.decimal_mark
248
+ || next_char === NUMBER_SIGN
249
+ || next_char === ZERO) {
250
+ lookahead_digit = true;
251
+ }
252
+ else if (next_char !== COMMA) { break; }
253
+ }
254
+ if (lookahead_digit) {
255
+ if (number_part === NumberPart.Decimal) {
256
+ throw new Error('invalid grouping in decimal part');
257
+ }
258
+ this.current_section.grouping = true;
259
+ }
260
+ else {
261
+ this.current_section.scaling = (this.current_section.scaling || 1) * 1000;
262
+ }
263
+ }
264
+ break;
265
+
266
+ case this.decimal_mark:
267
+ if (number_part === NumberPart.Decimal) {
268
+ throw new Error('too many decimal marks');
269
+ }
270
+ number_part = NumberPart.Decimal;
271
+ break;
272
+
273
+ case NUMBER_SIGN:
274
+
275
+ // spacing. allowing for some junk, we treat these as required
276
+ // if they're inside of zeros (after in the case of integer, before
277
+ // in the case of decimal)
278
+
279
+ if (number_part === NumberPart.Decimal) {
280
+ this.current_section.decimal_max_digits++;
281
+ }
282
+ else if (this.current_section.integer_min_digits) {
283
+ this.current_section.integer_min_digits++;
284
+ }
285
+
286
+ break;
287
+
288
+ case ZERO:
289
+
290
+ // required digit.
291
+
292
+ if (number_part === NumberPart.Decimal) {
293
+ this.current_section.decimal_max_digits++;
294
+ this.current_section.decimal_min_digits = this.current_section.decimal_max_digits;
295
+ }
296
+ else {
297
+ this.current_section.integer_min_digits++;
298
+ }
299
+ break;
300
+
301
+ default:
302
+
303
+ // non-number format character; we're done?
304
+
305
+ return;
306
+
307
+ }
308
+ }
309
+
310
+ }
311
+
312
+ protected static AppendCharAsText(advance_pointer = true): void {
313
+ if (this.current_section.has_number_format) {
314
+ this.current_section.suffix[this.current_section.suffix.length - 1].text += this.pattern[this.char_index];
315
+ }
316
+ else {
317
+ this.current_section.prefix[this.current_section.prefix.length - 1].text += this.pattern[this.char_index];
318
+ }
319
+ if (advance_pointer) {
320
+ this.char_index++;
321
+ }
322
+ }
323
+
324
+ protected static AppendString(text: string): void {
325
+ if (this.current_section.has_number_format) {
326
+ this.current_section.suffix[this.current_section.suffix.length - 1].text += text;
327
+ }
328
+ else {
329
+ this.current_section.prefix[this.current_section.prefix.length - 1].text += text;
330
+ }
331
+ }
332
+
333
+ protected static AppendTextPart(part: TextPart): void {
334
+ if (this.current_section.has_number_format) {
335
+ this.current_section.suffix.push(part);
336
+ this.current_section.suffix.push({ text: '' });
337
+ }
338
+ else {
339
+ this.current_section.prefix.push(part);
340
+ this.current_section.prefix.push({ text: '' });
341
+ }
342
+ }
343
+
344
+ protected static ConsumeChar(): void {
345
+
346
+ const char = this.characters[this.char_index];
347
+
348
+ // check for fraction format. this can't happen in a string section,
349
+ // and if there's already a number format then treat it as text (garbage).
350
+
351
+ if (char === QUESTION_MARK || char === NUMBER_SIGN) {
352
+ if (!this.current_section.has_number_format &&
353
+ !this.current_section.string_format &&
354
+ this.ScanFractionFormat()) {
355
+ return;
356
+ }
357
+ }
358
+
359
+ switch (char) {
360
+ case SEMICOLON:
361
+
362
+ // FIXME: there's a concept of an "empty" section, which is
363
+ // zero-length text between semicolons (or before the first
364
+ // semicolon). we should treat those as cloned or synthentic.
365
+
366
+ // actually, is that legal for the first section? possibly not.
367
+
368
+ this.char_index++; // discard
369
+ this.current_section = new NumberFormatSection();
370
+ if (this.sections.length === 3) this.current_section.string_format = true;
371
+ this.sections.push(this.current_section);
372
+ break;
373
+
374
+ case AT:
375
+
376
+ this.char_index++;
377
+ this.AppendTextPart({
378
+ text: '@', flag: TextPartFlag.literal,
379
+ });
380
+ this.current_section.string_format = true; // force
381
+ break;
382
+
383
+ case ZERO:
384
+ case NUMBER_SIGN:
385
+ case PERIOD:
386
+ case COMMA:
387
+
388
+ // only one actual format. anything else is treated as text.
389
+ // also skip for string format (#4)
390
+
391
+ if (!this.current_section.has_number_format && !this.current_section.string_format) {
392
+ this.ConsumeNumberFormat();
393
+ this.current_section.has_number_format = true;
394
+ }
395
+ else {
396
+ this.AppendCharAsText();
397
+ }
398
+ break;
399
+
400
+ case LEFT_BRACE:
401
+ this.AppendTextPart({ text: this.ConsumeFormatting(), flag: TextPartFlag.formatting });
402
+ break;
403
+
404
+ case DOUBLE_QUOTE:
405
+ this.AppendString(this.ConsumeString());
406
+ break;
407
+
408
+ case QUESTION_MARK: // this is like _0
409
+ if (this.preserve_formatting_characters) {
410
+ this.AppendCharAsText();
411
+ }
412
+ else {
413
+ this.AppendTextPart({
414
+ text: '0',
415
+ flag: TextPartFlag.hidden,
416
+ });
417
+ this.char_index++;
418
+ }
419
+ break;
420
+
421
+ case UNDERSCORE:
422
+ if (this.preserve_formatting_characters) {
423
+ this.AppendCharAsText();
424
+ }
425
+ else {
426
+ if (++this.char_index >= this.characters.length) {
427
+ throw new Error('invalid pad character at end');
428
+ }
429
+ this.AppendTextPart({
430
+ text: this.pattern[this.char_index++],
431
+ flag: TextPartFlag.hidden,
432
+ });
433
+ }
434
+ break;
435
+
436
+ case ASTERISK:
437
+ if (this.current_section.has_asterisk) {
438
+ throw new Error(`we don't support multiple asterisks`);
439
+ }
440
+ if (this.preserve_formatting_characters) {
441
+ this.AppendCharAsText();
442
+ }
443
+ else {
444
+ if (++this.char_index >= this.characters.length) {
445
+ throw new Error('invalid pad character at end');
446
+ }
447
+ this.AppendTextPart({
448
+ text: this.pattern[this.char_index++],
449
+ flag: TextPartFlag.padded,
450
+ });
451
+ this.current_section.has_asterisk = true;
452
+ }
453
+ break;
454
+
455
+ case LOWERCASE_E:
456
+ case UPPERCASE_E:
457
+
458
+ if (this.current_section.percent ||
459
+ this.current_section.exponential ||
460
+ this.current_section.string_format) {
461
+ this.AppendCharAsText();
462
+ }
463
+ else {
464
+ this.current_section.exponential = true;
465
+ this.char_index++;
466
+ }
467
+ break;
468
+
469
+ case PERCENT:
470
+
471
+ if (!this.current_section.exponential && !this.current_section.string_format) {
472
+ this.current_section.percent = true;
473
+ }
474
+ this.AppendCharAsText();
475
+ break;
476
+
477
+ case BACKSLASH:
478
+ if (this.preserve_formatting_characters) {
479
+ this.AppendCharAsText(false);
480
+ }
481
+ if (++this.char_index >= this.characters.length) {
482
+ throw new Error('invalid escape character at end');
483
+ }
484
+ this.AppendCharAsText();
485
+ break;
486
+
487
+ default:
488
+ this.AppendCharAsText();
489
+
490
+ }
491
+ }
492
+
493
+ /**
494
+ * we treat it as a date pattern if there's an unquoted date/time letter
495
+ * (one of [hmsdyHMSDY]). technically mixing date formats and number
496
+ * formats (#0) is illegal. we will just drop into number formats for those.
497
+ */
498
+ protected static ParseDatePattern(): boolean {
499
+ this.date_pattern = true;
500
+ while (this.date_pattern && this.char_index < this.pattern.length) {
501
+ this.DatePatternConsumeChar();
502
+ }
503
+
504
+ // one more check: there has to be a date format part in there
505
+ if (this.date_pattern) {
506
+ this.date_pattern = false;
507
+ for (const section of this.sections) {
508
+ for (const part of section.prefix) {
509
+ // tslint:disable-next-line: no-bitwise
510
+ if (part.flag && (part.flag & (TextPartFlag.date_component | TextPartFlag.date_component_minutes))) {
511
+ this.date_pattern = true;
512
+ }
513
+ }
514
+ }
515
+ }
516
+
517
+ // if it _is_ a date pattern, set the section flag.
518
+ if (this.date_pattern) {
519
+ this.sections[0].date_format = true;
520
+
521
+ // check for minutes, and set the flag (actually state in the text
522
+ // part). in date formats mm means months _unless_ it is preceded
523
+ // by an hh or followed by an ss.
524
+
525
+ this.sections[0].prefix.forEach((item, index) => {
526
+ if (item.flag === TextPartFlag.date_component && (item.text === 'mm' || item.text === 'm')) {
527
+ if (index) {
528
+ for (let i = index - 1; i; i--) {
529
+ const test = this.sections[0].prefix[i];
530
+ if (test.flag === TextPartFlag.date_component) {
531
+ if (/h/i.test(test.text)) {
532
+ item.flag = TextPartFlag.date_component_minutes;
533
+ item.text = item.text.toLowerCase(); // normalize
534
+ }
535
+ break;
536
+ }
537
+ }
538
+ }
539
+ if (index < this.sections[0].prefix.length - 1) {
540
+ for (let i = index + 1; i < this.sections[0].prefix.length; i++) {
541
+ const test = this.sections[0].prefix[i];
542
+ if (test.flag === TextPartFlag.date_component) {
543
+ if (/s/i.test(test.text)) {
544
+ item.flag = TextPartFlag.date_component_minutes;
545
+ item.text = item.text.toLowerCase(); // normalize
546
+ }
547
+ break;
548
+ }
549
+ }
550
+ }
551
+ }
552
+ });
553
+
554
+ }
555
+ return this.date_pattern;
556
+ }
557
+
558
+ /**
559
+ * date parts are repeated sequences (e.g. ddd). we allow
560
+ * fractional seconds with ss.00.
561
+ */
562
+ protected static ConsumeDatePart(): TextPart {
563
+ const initial_char = this.pattern[this.char_index++];
564
+ const normalized = initial_char.toLowerCase();
565
+
566
+ const part: TextPart = {
567
+ text: initial_char,
568
+ flag: TextPartFlag.date_component,
569
+ };
570
+
571
+ while (this.pattern[this.char_index] && (this.pattern[this.char_index].toLowerCase() === normalized)) {
572
+ part.text += (this.pattern[this.char_index++]);
573
+ }
574
+
575
+ // partial seconds
576
+
577
+ if (normalized === 's' && this.pattern[this.char_index] === '.') {
578
+ part.text += (this.pattern[this.char_index++]);
579
+ while (this.pattern[this.char_index] === '0') {
580
+ part.text += (this.pattern[this.char_index++]);
581
+ }
582
+ }
583
+
584
+ return part;
585
+ }
586
+
587
+ /**
588
+ * special patterns for am/pm in date formats
589
+ */
590
+ protected static ConsumeAMPM(): TextPart | undefined {
591
+
592
+ let test = this.pattern.substr(this.char_index, 5);
593
+ if (test === 'am/pm' || test === 'AM/PM') {
594
+ this.char_index += 5;
595
+ this.sections[0].twelve_hour = true;
596
+ return { text: test, flag: TextPartFlag.date_component };
597
+ }
598
+
599
+ test = this.pattern.substr(this.char_index, 3);
600
+ if (test === 'a/p' || test === 'A/P') {
601
+ this.char_index += 3;
602
+ this.sections[0].twelve_hour = true;
603
+ return { text: test, flag: TextPartFlag.date_component };
604
+ }
605
+
606
+ return undefined;
607
+ }
608
+
609
+ protected static DatePatternConsumeChar(): void {
610
+
611
+ const char = this.characters[this.char_index];
612
+
613
+ switch (char) {
614
+ case SEMICOLON:
615
+
616
+ // only one section allowed for dates (not sure why). just ignore
617
+ // everything after the semicolon, but don't invalidate the pattern.
618
+ this.char_index = this.characters.length; // end
619
+ break;
620
+
621
+ case ZERO:
622
+ case NUMBER_SIGN:
623
+ case LOWERCASE_E:
624
+ case UPPERCASE_E:
625
+ case PERCENT:
626
+ case AT:
627
+ // case PERIOD:
628
+ // case COMMA:
629
+
630
+ // this is not a date format.
631
+ this.date_pattern = false;
632
+ break;
633
+
634
+ case UPPERCASE_H:
635
+ case LOWERCASE_H:
636
+ case UPPERCASE_M:
637
+ case LOWERCASE_M:
638
+ case UPPERCASE_S:
639
+ case LOWERCASE_S:
640
+ case UPPERCASE_D:
641
+ case LOWERCASE_D:
642
+ case UPPERCASE_Y:
643
+ case LOWERCASE_Y:
644
+ this.AppendTextPart(this.ConsumeDatePart());
645
+ break;
646
+
647
+ case UPPERCASE_A:
648
+ case LOWERCASE_A:
649
+ {
650
+ const ampm = this.ConsumeAMPM();
651
+ if (ampm) this.AppendTextPart(ampm);
652
+ else this.AppendCharAsText();
653
+ }
654
+ break;
655
+
656
+ case DOUBLE_QUOTE:
657
+ this.AppendString(this.ConsumeString());
658
+ break;
659
+
660
+ case QUESTION_MARK: // this is like _0
661
+ if (this.preserve_formatting_characters) {
662
+ this.AppendCharAsText();
663
+ }
664
+ else {
665
+ this.AppendTextPart({
666
+ text: '0',
667
+ flag: TextPartFlag.hidden,
668
+ });
669
+ this.char_index++;
670
+ }
671
+ break;
672
+
673
+ case UNDERSCORE:
674
+ if (this.preserve_formatting_characters) {
675
+ this.AppendCharAsText();
676
+ }
677
+ else {
678
+ if (++this.char_index >= this.characters.length) {
679
+ throw new Error('invalid pad character at end');
680
+ }
681
+ this.AppendTextPart({
682
+ text: this.pattern[this.char_index++],
683
+ flag: TextPartFlag.hidden,
684
+ });
685
+ }
686
+ break;
687
+
688
+ case ASTERISK:
689
+ if (this.current_section.has_asterisk) {
690
+ throw new Error(`we don't support multiple asterisks`);
691
+ }
692
+ if (this.preserve_formatting_characters) {
693
+ this.AppendCharAsText();
694
+ }
695
+ else {
696
+ if (++this.char_index >= this.characters.length) {
697
+ throw new Error('invalid pad character at end');
698
+ }
699
+ this.AppendTextPart({
700
+ text: this.pattern[this.char_index++],
701
+ flag: TextPartFlag.padded,
702
+ });
703
+ this.current_section.has_asterisk = true;
704
+ }
705
+ break;
706
+
707
+ case BACKSLASH:
708
+ if (this.preserve_formatting_characters) {
709
+ this.AppendCharAsText(false);
710
+ }
711
+ if (++this.char_index >= this.characters.length) {
712
+ throw new Error('invalid escape character at end');
713
+ }
714
+ this.AppendCharAsText();
715
+ break;
716
+
717
+ default:
718
+ this.AppendCharAsText();
719
+
720
+ }
721
+ }
722
+
723
+ }