@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.
- package/.eslintignore +8 -0
- package/.eslintrc.js +164 -0
- package/README-shadow-DOM.md +88 -0
- package/README.md +37 -130
- package/api-config.json +29 -0
- package/api-generator/api-generator-types.ts +82 -0
- package/api-generator/api-generator.ts +1172 -0
- package/api-generator/package.json +3 -0
- package/build/treb-spreadsheet.mjs +14 -0
- package/{treb.d.ts → build/treb.d.ts} +285 -269
- package/esbuild-custom-element.mjs +336 -0
- package/esbuild.js +305 -0
- package/package.json +43 -14
- package/treb-base-types/package.json +5 -0
- package/treb-base-types/src/api_types.ts +36 -0
- package/treb-base-types/src/area.ts +583 -0
- package/treb-base-types/src/basic_types.ts +45 -0
- package/treb-base-types/src/cell.ts +612 -0
- package/treb-base-types/src/cells.ts +1066 -0
- package/treb-base-types/src/color.ts +124 -0
- package/treb-base-types/src/import.ts +71 -0
- package/treb-base-types/src/index-standalone.ts +29 -0
- package/treb-base-types/src/index.ts +42 -0
- package/treb-base-types/src/layout.ts +47 -0
- package/treb-base-types/src/localization.ts +187 -0
- package/treb-base-types/src/rectangle.ts +145 -0
- package/treb-base-types/src/render_text.ts +72 -0
- package/treb-base-types/src/style.ts +545 -0
- package/treb-base-types/src/table.ts +109 -0
- package/treb-base-types/src/text_part.ts +54 -0
- package/treb-base-types/src/theme.ts +608 -0
- package/treb-base-types/src/union.ts +152 -0
- package/treb-base-types/src/value-type.ts +164 -0
- package/treb-base-types/style/resizable.css +59 -0
- package/treb-calculator/modern.tsconfig.json +11 -0
- package/treb-calculator/package.json +5 -0
- package/treb-calculator/src/calculator.ts +2546 -0
- package/treb-calculator/src/complex-math.ts +558 -0
- package/treb-calculator/src/dag/array-vertex.ts +198 -0
- package/treb-calculator/src/dag/graph.ts +951 -0
- package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
- package/treb-calculator/src/dag/vertex.ts +352 -0
- package/treb-calculator/src/descriptors.ts +162 -0
- package/treb-calculator/src/expression-calculator.ts +1069 -0
- package/treb-calculator/src/function-error.ts +103 -0
- package/treb-calculator/src/function-library.ts +103 -0
- package/treb-calculator/src/functions/base-functions.ts +1214 -0
- package/treb-calculator/src/functions/checkbox.ts +164 -0
- package/treb-calculator/src/functions/complex-functions.ts +253 -0
- package/treb-calculator/src/functions/finance-functions.ts +399 -0
- package/treb-calculator/src/functions/information-functions.ts +102 -0
- package/treb-calculator/src/functions/matrix-functions.ts +182 -0
- package/treb-calculator/src/functions/sparkline.ts +335 -0
- package/treb-calculator/src/functions/statistics-functions.ts +350 -0
- package/treb-calculator/src/functions/text-functions.ts +298 -0
- package/treb-calculator/src/index.ts +27 -0
- package/treb-calculator/src/notifier-types.ts +59 -0
- package/treb-calculator/src/primitives.ts +428 -0
- package/treb-calculator/src/utilities.ts +305 -0
- package/treb-charts/package.json +5 -0
- package/treb-charts/src/chart-functions.ts +156 -0
- package/treb-charts/src/chart-types.ts +230 -0
- package/treb-charts/src/chart.ts +1288 -0
- package/treb-charts/src/index.ts +24 -0
- package/treb-charts/src/main.ts +37 -0
- package/treb-charts/src/rectangle.ts +52 -0
- package/treb-charts/src/renderer.ts +1841 -0
- package/treb-charts/src/util.ts +122 -0
- package/treb-charts/style/charts.scss +221 -0
- package/treb-charts/style/old-charts.scss +250 -0
- package/treb-embed/markup/layout.html +137 -0
- package/treb-embed/markup/toolbar.html +175 -0
- package/treb-embed/modern.tsconfig.json +25 -0
- package/treb-embed/src/custom-element/content-types.d.ts +18 -0
- package/treb-embed/src/custom-element/global.d.ts +11 -0
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
- package/treb-embed/src/custom-element/treb-global.ts +44 -0
- package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
- package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
- package/treb-embed/src/index.ts +16 -0
- package/treb-embed/src/language-model.ts +41 -0
- package/treb-embed/src/options.ts +320 -0
- package/treb-embed/src/progress-dialog.ts +228 -0
- package/treb-embed/src/selection-state.ts +16 -0
- package/treb-embed/src/spinner.ts +42 -0
- package/treb-embed/src/toolbar-message.ts +96 -0
- package/treb-embed/src/types.ts +167 -0
- package/treb-embed/style/autocomplete.scss +103 -0
- package/treb-embed/style/dark-theme.scss +114 -0
- package/treb-embed/style/defaults.scss +36 -0
- package/treb-embed/style/dialog.scss +181 -0
- package/treb-embed/style/dropdown-select.scss +101 -0
- package/treb-embed/style/formula-bar.scss +193 -0
- package/treb-embed/style/grid.scss +374 -0
- package/treb-embed/style/layout.scss +424 -0
- package/treb-embed/style/mouse-mask.scss +67 -0
- package/treb-embed/style/note.scss +92 -0
- package/treb-embed/style/overlay-editor.scss +102 -0
- package/treb-embed/style/spinner.scss +92 -0
- package/treb-embed/style/tab-bar.scss +228 -0
- package/treb-embed/style/table.scss +80 -0
- package/treb-embed/style/theme-defaults.scss +444 -0
- package/treb-embed/style/toolbar.scss +416 -0
- package/treb-embed/style/tooltip.scss +68 -0
- package/treb-embed/style/treb-icons.scss +130 -0
- package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
- package/treb-embed/style/z-index.scss +43 -0
- package/treb-export/docs/charts.md +68 -0
- package/treb-export/modern.tsconfig.json +19 -0
- package/treb-export/package.json +4 -0
- package/treb-export/src/address-type.ts +77 -0
- package/treb-export/src/base-template.ts +22 -0
- package/treb-export/src/column-width.ts +85 -0
- package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
- package/treb-export/src/drawing2/chart2.ts +282 -0
- package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
- package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
- package/treb-export/src/drawing2/drawing2.ts +355 -0
- package/treb-export/src/drawing2/embedded-image.ts +71 -0
- package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
- package/treb-export/src/export-worker/export-worker.ts +99 -0
- package/treb-export/src/export-worker/index-modern.ts +22 -0
- package/treb-export/src/export2.ts +2204 -0
- package/treb-export/src/import2.ts +882 -0
- package/treb-export/src/relationship.ts +36 -0
- package/treb-export/src/shared-strings2.ts +128 -0
- package/treb-export/src/template-2.ts +22 -0
- package/treb-export/src/unescape_xml.ts +47 -0
- package/treb-export/src/workbook-sheet2.ts +182 -0
- package/treb-export/src/workbook-style2.ts +1285 -0
- package/treb-export/src/workbook-theme2.ts +88 -0
- package/treb-export/src/workbook2.ts +491 -0
- package/treb-export/src/xml-utils.ts +201 -0
- package/treb-export/template/base/[Content_Types].xml +2 -0
- package/treb-export/template/base/_rels/.rels +2 -0
- package/treb-export/template/base/docProps/app.xml +2 -0
- package/treb-export/template/base/docProps/core.xml +12 -0
- package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
- package/treb-export/template/base/xl/sharedStrings.xml +2 -0
- package/treb-export/template/base/xl/styles.xml +2 -0
- package/treb-export/template/base/xl/theme/theme1.xml +2 -0
- package/treb-export/template/base/xl/workbook.xml +2 -0
- package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
- package/treb-export/template/base.xlsx +0 -0
- package/treb-format/package.json +8 -0
- package/treb-format/src/format.test.ts +213 -0
- package/treb-format/src/format.ts +942 -0
- package/treb-format/src/format_cache.ts +199 -0
- package/treb-format/src/format_parser.ts +723 -0
- package/treb-format/src/index.ts +25 -0
- package/treb-format/src/number_format_section.ts +100 -0
- package/treb-format/src/value_parser.ts +337 -0
- package/treb-grid/package.json +5 -0
- package/treb-grid/src/editors/autocomplete.ts +394 -0
- package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
- package/treb-grid/src/editors/formula_bar.ts +473 -0
- package/treb-grid/src/editors/formula_editor_base.ts +910 -0
- package/treb-grid/src/editors/overlay_editor.ts +511 -0
- package/treb-grid/src/index.ts +37 -0
- package/treb-grid/src/layout/base_layout.ts +2618 -0
- package/treb-grid/src/layout/grid_layout.ts +299 -0
- package/treb-grid/src/layout/rectangle_cache.ts +86 -0
- package/treb-grid/src/render/selection-renderer.ts +414 -0
- package/treb-grid/src/render/svg_header_overlay.ts +93 -0
- package/treb-grid/src/render/svg_selection_block.ts +187 -0
- package/treb-grid/src/render/tile_renderer.ts +2122 -0
- package/treb-grid/src/types/annotation.ts +216 -0
- package/treb-grid/src/types/border_constants.ts +34 -0
- package/treb-grid/src/types/clipboard_data.ts +31 -0
- package/treb-grid/src/types/data_model.ts +334 -0
- package/treb-grid/src/types/drag_mask.ts +81 -0
- package/treb-grid/src/types/grid.ts +7743 -0
- package/treb-grid/src/types/grid_base.ts +3644 -0
- package/treb-grid/src/types/grid_command.ts +470 -0
- package/treb-grid/src/types/grid_events.ts +124 -0
- package/treb-grid/src/types/grid_options.ts +97 -0
- package/treb-grid/src/types/grid_selection.ts +60 -0
- package/treb-grid/src/types/named_range.ts +369 -0
- package/treb-grid/src/types/scale-control.ts +202 -0
- package/treb-grid/src/types/serialize_options.ts +72 -0
- package/treb-grid/src/types/set_range_options.ts +52 -0
- package/treb-grid/src/types/sheet.ts +3099 -0
- package/treb-grid/src/types/sheet_types.ts +95 -0
- package/treb-grid/src/types/tab_bar.ts +464 -0
- package/treb-grid/src/types/tile.ts +59 -0
- package/treb-grid/src/types/update_flags.ts +75 -0
- package/treb-grid/src/util/dom_utilities.ts +44 -0
- package/treb-grid/src/util/fontmetrics2.ts +179 -0
- package/treb-grid/src/util/ua.ts +104 -0
- package/treb-logo.svg +18 -0
- package/treb-parser/package.json +5 -0
- package/treb-parser/src/csv-parser.ts +122 -0
- package/treb-parser/src/index.ts +25 -0
- package/treb-parser/src/md-parser.ts +526 -0
- package/treb-parser/src/parser-types.ts +397 -0
- package/treb-parser/src/parser.test.ts +298 -0
- package/treb-parser/src/parser.ts +2673 -0
- package/treb-utils/package.json +5 -0
- package/treb-utils/src/dispatch.ts +57 -0
- package/treb-utils/src/event_source.ts +147 -0
- package/treb-utils/src/ievent_source.ts +33 -0
- package/treb-utils/src/index.ts +31 -0
- package/treb-utils/src/measurement.ts +174 -0
- package/treb-utils/src/resizable.ts +160 -0
- package/treb-utils/src/scale.ts +137 -0
- package/treb-utils/src/serialize_html.ts +124 -0
- package/treb-utils/src/template.ts +70 -0
- package/treb-utils/src/validate_uri.ts +61 -0
- package/tsconfig.json +10 -0
- package/tsproject.json +30 -0
- package/util/license-plugin-esbuild.js +86 -0
- package/util/list-css-vars.sh +46 -0
- package/README-esm.md +0 -37
- package/treb-bundle.css +0 -2
- package/treb-bundle.mjs +0 -15
|
@@ -0,0 +1,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
|
+
}
|