@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,942 @@
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 { FormatParser } from './format_parser';
23
+ import { NumberFormatSection } from './number_format_section';
24
+ import {
25
+ Localization, TextPartFlag, TextPart, Complex, DimensionedQuantity, CellValue, IsDimensionedQuantity,
26
+ } from 'treb-base-types';
27
+
28
+ //
29
+ // excel time is explicitly universal, so we need all dates in and out
30
+ // to be UTC. we can't use local time because of daylight savings (which
31
+ // excel ignores)
32
+ //
33
+ // the actual epoch is "January 0" -- I suppose that === Dec 31?
34
+ //
35
+ // const base_date = -2209075200000; // new Date('1899-12-31 00:00:00 Z').getTime();
36
+
37
+ //
38
+ // excel time is 1 == 1 day, so relative to js time (millis), we need
39
+ // to scale by 1000 * 60 * 60 * 24
40
+ //
41
+ // const date_scale = 86400000;
42
+
43
+ //
44
+ // one last thing -- Excel incorrectly treats 1900 as a leap year. this was
45
+ // for compatibility with Lotus 1-2-3, which handled it incorrectly. we will
46
+ // join the party and treat it incorrectly as well.
47
+ //
48
+ // ref:
49
+ // https://docs.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year
50
+ //
51
+ // what about backwards?
52
+ //
53
+ // OK, I can answer that now: Excel just doesn't handle dates before 1900
54
+ // at all. can't parse them; can't handle negative numbers as dates.
55
+
56
+ /** convert cell value -> date, using the rules above */
57
+ export const LotusDate = (value: number): Date => {
58
+ if (value >= 60) value--; // March 1, 1900
59
+ return new Date(-2209075200000 + 86400000 * value);
60
+ };
61
+
62
+ /** convert date (as number, utc millis) -> lotus date value */
63
+ export const UnlotusDate = (value: number, local = true): number => {
64
+
65
+ // if the passed value is local, we need to convert it to UTC
66
+
67
+ if (local) {
68
+
69
+ const local_date = new Date(value);
70
+ const utc_date = new Date();
71
+
72
+ utc_date.setUTCMilliseconds(local_date.getUTCMilliseconds());
73
+ utc_date.setUTCSeconds(local_date.getUTCSeconds());
74
+ utc_date.setUTCMinutes(local_date.getUTCMinutes());
75
+ utc_date.setUTCHours(local_date.getHours());
76
+ utc_date.setUTCDate(local_date.getDate());
77
+ utc_date.setUTCMonth(local_date.getMonth());
78
+ utc_date.setUTCFullYear(local_date.getFullYear());
79
+
80
+ value = utc_date.getTime();
81
+
82
+ }
83
+
84
+ value = (value + 2209075200000) / 86400000;
85
+ if (value >= 60) { value++; }
86
+
87
+ return value;
88
+
89
+ };
90
+
91
+ /**
92
+ * unifying date format and number format (really just bolting dates
93
+ * on the side). dates have only a single section, constant pattern, and
94
+ * are immutable.
95
+ */
96
+ export class NumberFormat {
97
+
98
+ public static grouping_regexp = /\d{1,3}(?=(\d{3})+(?!\d))/g;
99
+
100
+ public static fraction_limits = [9, 99, 999, 9999];
101
+
102
+ /**
103
+ * this is now exposed so it can be changed, for rendering; some options are
104
+ *
105
+ * "i" - regular i, and the default
106
+ * "𝑖" - mathematical italic small i", U+1D456
107
+ * " 𝑖" - the same, with a leading hair space (U+200A)
108
+ */
109
+ public static imaginary_character = '𝑖'; // 'i';
110
+
111
+ /**
112
+ * also for complex rendering, the minus sign. there's a unicode
113
+ * symbol U+2212 which (at least in calibri) is wider than the regular minus
114
+ * sign/hyphen. I like this but it looks a bit odd if negative numbers are
115
+ * rendered using the other one.
116
+ *
117
+ * "-" - hyphen
118
+ * "−" - minus
119
+ */
120
+ public static minus_character = '-'; // hyphen
121
+ // public static minus_character = '−'; // minus
122
+
123
+ /** for the "General" format, a magic decimal point */
124
+ public magic_decimal = false;
125
+
126
+ /**
127
+ * (testing) transformer. this is not rendered or persisted, like magic
128
+ * decimal it needs to be applied in code. ATM this is only applied in
129
+ * formatting DQ, but it might turn out to be more universal...
130
+ *
131
+ * NOTE that atm this transforms value back into the same type; we don't
132
+ * cross types (at least for now). perhaps we should support that? that
133
+ * might mean switching in here and removing the "special" format calls
134
+ * for complex and DQ.
135
+ */
136
+ public transform_value?: (value: CellValue) => CellValue;
137
+
138
+ // tslint:disable-next-line:variable-name
139
+ protected _pattern = '';
140
+ protected sections: NumberFormatSection[];
141
+ protected decimal_zero_regexp: Array<RegExp | undefined> = [];
142
+
143
+ // this is a flag for string representation
144
+ protected cloned: boolean[] = [];
145
+
146
+ // NumberFormat.decimal_mark = Localization.decimal_separator;
147
+ // if (NumberFormat.decimal_mark === ',') NumberFormat.grouping_separator = ' ';
148
+
149
+ // public static decimal_mark: '.'|',' = Localization.decimal_separator;
150
+ // public static grouping_separator = (Localization.decimal_separator === '.') ? ',' : ' ';
151
+
152
+ public get pattern(): string {
153
+ return this._pattern;
154
+ }
155
+
156
+ /** flag indicates if this is a date format */
157
+ public get date_format(): boolean {
158
+ return this.sections[0] && this.sections[0].date_format;
159
+ }
160
+
161
+ constructor(pattern: string) {
162
+ this._pattern = pattern;
163
+ this.sections = FormatParser.Parse(pattern);
164
+
165
+ // nothing?
166
+
167
+ if (!this.sections.length) this.sections = [];
168
+
169
+ // check zero. we were previously assuming this stepped, but we
170
+ // now support gaps in format sections (although not at 0?)
171
+
172
+ if (!this.sections[0]) {
173
+ this.sections[0] = new NumberFormatSection(); // pretty sure this cannot happen atm
174
+ }
175
+
176
+ // do we have a negative section? if not, use the positive
177
+ // section and prepend a - sign.
178
+
179
+ if (!this.sections[1]) {
180
+ this.sections[1] = { ...this.sections[0] };
181
+ this.sections[1].prefix = JSON.parse(JSON.stringify(this.sections[1].prefix));
182
+ this.sections[1].suffix = JSON.parse(JSON.stringify(this.sections[1].suffix));
183
+ this.sections[1].prefix.push({ text: '-' }); // at end of prefix, before number
184
+ this.cloned[1] = true;
185
+ }
186
+
187
+ // do we have a zero section? if not, clone the positive section.
188
+
189
+ if (!this.sections[2]) {
190
+ this.sections[2] = { ...this.sections[0] };
191
+ this.cloned[2] = true;
192
+ }
193
+
194
+ // string section, default just reflects the string. we could perhaps
195
+ // skip this and just have default behavior if there's no section, which
196
+ // might simplify rendering
197
+
198
+ // UPDATE, special case: unless a string section is explicitly
199
+ // provided, we use a default '@' section (it's implicit). however,
200
+ // if there's a literatal '@' in the first section, we want to
201
+ // propogate that to all empty sections, including the string section.
202
+
203
+ // note that we should not support literal AND numeric sections in
204
+ // the same block... it will fail silently here... [FIXME: at least warn]
205
+
206
+ if (!this.sections[3]) {
207
+ for (const part of this.sections[0].prefix) {
208
+ if (part.flag === TextPartFlag.literal) {
209
+ this.sections[3] = { ...this.sections[0] };
210
+ this.sections[3].string_format = true;
211
+ this.cloned[3] = true;
212
+ break;
213
+ }
214
+ }
215
+ }
216
+
217
+ /*
218
+ if (!this.sections[3]) {
219
+ this.sections[3] = new NumberFormatSection();
220
+ this.sections[3].string_format = true;
221
+ this.sections[3].prefix = [{ text: '@', flag: TextPartFlag.literal }];
222
+
223
+ // obviously not cloned, but we want the behavior. FIXME: change flag name
224
+ this.cloned[3] = true;
225
+ }
226
+ */
227
+
228
+ this.decimal_zero_regexp = this.sections.map((section) => {
229
+ if (section.decimal_max_digits > section.decimal_min_digits) {
230
+ return new RegExp(`0{1,${section.decimal_max_digits - section.decimal_min_digits}}(?:$|e)`);
231
+ }
232
+ return undefined;
233
+ });
234
+
235
+ }
236
+
237
+ /**
238
+ * render text parts to string
239
+ * FIXME: move
240
+ */
241
+ public static FormatPartsAsText(parts: TextPart[], text_width = 0): string {
242
+
243
+ let padded = -1;
244
+
245
+ const formatted = parts.map((part, index) => {
246
+ switch (part.flag) {
247
+ case TextPartFlag.padded:
248
+ padded = index;
249
+ return part.text;
250
+
251
+ case TextPartFlag.hidden:
252
+ return part.text.replace(/./g, ' ');
253
+
254
+ case TextPartFlag.formatting:
255
+ return '';
256
+
257
+ default:
258
+ return part.text;
259
+ }
260
+ });
261
+
262
+ if (padded >= 0 && text_width) {
263
+ const total_length = formatted.reduce((a, str, index) => (index === padded) ? a : a + str.length, 0);
264
+ let tmp = '';
265
+ for (let i = 0; i < text_width - total_length; i++) {
266
+ tmp += formatted[padded];
267
+ }
268
+ formatted[padded] = tmp;
269
+ }
270
+
271
+ return formatted.join('');
272
+
273
+ }
274
+
275
+ /** for decimal only, set an explicit number of digits */
276
+ public SetDecimal(digits: number): void {
277
+ for (const section of this.sections) {
278
+ if (!section.fraction_format) {
279
+ section.decimal_min_digits = digits;
280
+ section.decimal_max_digits = digits;
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * mutate
287
+ * UPDATE: for fractional formats, increase the denominator digits
288
+ * (doing something weird with fixed denominators...)
289
+ */
290
+ public IncreaseDecimal(): void {
291
+ this.sections.forEach((section) => {
292
+ if (section.fraction_format) {
293
+ if (!section.fraction_denominator) {
294
+ section.fraction_denominator_digits = Math.min(section.fraction_denominator_digits + 1, 4);
295
+ }
296
+ }
297
+ else {
298
+ section.decimal_min_digits++;
299
+ section.decimal_max_digits = section.decimal_min_digits;
300
+ }
301
+ });
302
+ }
303
+
304
+ /**
305
+ * mutate
306
+ * UPDATE: for fractional formats, decrease the denominator digits
307
+ * (doing something weird with fixed denominators...)
308
+ */
309
+ public DecreaseDecimal(): void {
310
+ this.sections.forEach((section) => {
311
+ if (section.fraction_format) {
312
+ if (!section.fraction_denominator) {
313
+ section.fraction_denominator_digits = Math.max(section.fraction_denominator_digits - 1, 1);
314
+ }
315
+ }
316
+ else {
317
+ section.decimal_min_digits = Math.max(0, section.decimal_min_digits - 1);
318
+ section.decimal_max_digits = section.decimal_min_digits;
319
+ }
320
+ });
321
+ }
322
+
323
+ /** mutate */
324
+ public AddGrouping(): void {
325
+ this.sections.forEach((section) => {
326
+ section.grouping = true;
327
+ });
328
+ }
329
+
330
+ /** mutate */
331
+ public RemoveGrouping(): void {
332
+ this.sections.forEach((section) => {
333
+ section.grouping = false;
334
+ });
335
+ }
336
+
337
+ /** mutate */
338
+ public ToggleGrouping(): void {
339
+ // set all to ! the value of the first one
340
+ const grouping = !this.sections[0].grouping;
341
+ this.sections.forEach((section) => {
342
+ section.grouping = grouping;
343
+ });
344
+ }
345
+
346
+ /**
347
+ * generates a string representation. we use this because we are (now)
348
+ * allowing mutation of formats; therefore we need to serialize them back
349
+ * to the basic format.
350
+ */
351
+ public toString(): string {
352
+
353
+ if (this.sections[0].date_format) {
354
+ return this._pattern; // immutable
355
+ }
356
+
357
+ return this.sections.filter((section, i) => {
358
+ return !this.cloned[i];
359
+ }).map((section) => {
360
+
361
+ let nf = '';
362
+ let i = 0;
363
+
364
+ if (section.fraction_format) {
365
+ if (section.fraction_integer) {
366
+ nf += '? ';
367
+ }
368
+ let pattern = '';
369
+ for (let j = 0; j < section.fraction_denominator_digits; j++) {
370
+ pattern += '#';
371
+ }
372
+ nf += pattern;
373
+ nf += '/';
374
+ if (section.fraction_denominator) {
375
+ nf += section.fraction_denominator;
376
+ }
377
+ else {
378
+ nf += pattern;
379
+ }
380
+ }
381
+ else if (section.has_number_format) {
382
+ for (i = 0; i < section.integer_min_digits; i++) {
383
+ nf += '0';
384
+ }
385
+ if (section.grouping) {
386
+ if (nf.length < 4) nf = ('####' + nf).slice(-4);
387
+ nf = nf.replace(/[\d#]{1,3}(?=([\d#]{3})+(?![\d#]))/g, '$&' + ','); // Localization.grouping_separator);
388
+ }
389
+ if (section.decimal_max_digits || section.decimal_min_digits) {
390
+ nf += '.'; // Localization.decimal_separator;
391
+ for (i = 0; i < section.decimal_min_digits; i++) { nf += '0'; }
392
+ for (; i < section.decimal_max_digits; i++) { nf += '#'; }
393
+ }
394
+ if (section.scaling) {
395
+ const count = Math.log10(section.scaling) / 3;
396
+ for (i = 0; i < count; i++) { nf += ','; }
397
+ }
398
+ if (section.exponential) {
399
+ nf += 'e';
400
+ }
401
+ }
402
+
403
+ return section.prefix.map((part) => {
404
+ if (part.flag === TextPartFlag.hidden) {
405
+ return part.text === '0' ? '?' : '_' + part.text;
406
+ }
407
+ else if (part.flag === TextPartFlag.padded) {
408
+ return '*' + part.text;
409
+ }
410
+ else if (part.flag === TextPartFlag.formatting) {
411
+ return '[' + part.text + ']';
412
+ }
413
+ return part.text;
414
+ }).join('') + nf +
415
+ section.suffix.map((part) => {
416
+ if (part.flag === TextPartFlag.hidden) {
417
+ return part.text === '0' ? '?' : '_' + part.text;
418
+ }
419
+ else if (part.flag === TextPartFlag.padded) { return '*' + part.text; }
420
+ return part.text;
421
+ }).join('');
422
+
423
+ }).join(';');
424
+
425
+ }
426
+
427
+ /** also temporary? why not switch in here? */
428
+ public FormatDimensionedQuantity(value: DimensionedQuantity): TextPart[]|string {
429
+
430
+ if (this.transform_value) {
431
+ const result = this.transform_value(value);
432
+ if (IsDimensionedQuantity(result)) {
433
+ value = result;
434
+ }
435
+ else if (typeof result === 'string') {
436
+
437
+ // so this is new, but we want string semantics here; rendering
438
+ // is different because strings can wrap
439
+
440
+ return result;
441
+ }
442
+ else {
443
+
444
+ // could be a complex (not likely now, but things change), in which
445
+ // case this is not the right method -- we need a method that checks
446
+ // and switches.
447
+
448
+ return this.FormatParts(result);
449
+ }
450
+ }
451
+
452
+ const parts: TextPart[] = this.FormatParts(value.value || 0);
453
+
454
+ // anything fancy we want to do in here...
455
+
456
+ if (value.unit) {
457
+ parts.push({text: ' '}, {
458
+ text: value.unit
459
+ });
460
+ }
461
+
462
+ return parts;
463
+ }
464
+
465
+ /**
466
+ * temporary
467
+ *
468
+ * FIXME: merge with FormatParts, use a test to check if it's complex?
469
+ * OTOH that adds a test to every format which is probably wasteful...
470
+ * although we can check for 'number' first
471
+ *
472
+ */
473
+ public FormatComplex(value: Complex): TextPart[] {
474
+
475
+ // formatting complex value (note for searching)
476
+
477
+ // this needs some work. some thoughts:
478
+ //
479
+ // (1) allow fractions and decimals, but not percent or exponential notation
480
+ // (2) change default for General to have fewer decimal places
481
+ // (3) use the section's integer specification to decide on whether to
482
+ // write "1i" or just "i"
483
+ // (4) drop either real or imaginary part (but not both) if it renders as
484
+ // all zeros
485
+ // (5) change default fraction to #/## (actually we should do that always)
486
+
487
+
488
+ // check if the imaginary format will render as 0.00i -- we want to
489
+ // handle this differently.
490
+
491
+ let imaginary_format: TextPart[] = [];
492
+ let real_format: TextPart[] = [];
493
+
494
+ let drop_imaginary_coefficient = false;
495
+
496
+ let has_imaginary_value = !!value.imaginary;
497
+ if (has_imaginary_value) {
498
+ imaginary_format = this.FormatParts(value.imaginary);
499
+ has_imaginary_value = imaginary_format.some(element => /[1-9]/.test(element.text));
500
+
501
+ // special case: if the integer is not required and the value is
502
+ // either "1" or "-1", drop the integer. do this for integer length
503
+ // <= 1, because you want 0, i, 2i, 3i, &c.
504
+
505
+ if (imaginary_format.length === 1 &&
506
+ this.sections[0].integer_min_digits <= 1 &&
507
+ imaginary_format[0].text === '1' ) {
508
+ imaginary_format[0].text = '';
509
+ drop_imaginary_coefficient = true;
510
+ }
511
+ else if (imaginary_format.length === 1 &&
512
+ this.sections[1].integer_min_digits <= 1 &&
513
+ imaginary_format[0].text === '-1' ) {
514
+ imaginary_format[0].text = '-';
515
+ drop_imaginary_coefficient = true;
516
+ }
517
+
518
+ }
519
+
520
+ let has_real_value = !!value.real;
521
+ if (has_real_value) {
522
+ real_format = this.FormatParts(value.real);
523
+ has_real_value = real_format.some(element => /[1-9]/.test(element.text));
524
+ }
525
+
526
+ const parts: TextPart[] = [];
527
+
528
+ if (has_real_value || (!has_real_value && !has_imaginary_value)) {
529
+
530
+ // has real part, or is === 0
531
+ parts.push(...real_format);
532
+
533
+ if (has_imaginary_value) {
534
+
535
+ // also has imaginary part
536
+ const i = Math.abs(value.imaginary);
537
+ parts.push({ text: value.imaginary < 0 ? ` ${NumberFormat.minus_character} ` : ' + ' });
538
+
539
+ const reformatted_imaginary = drop_imaginary_coefficient ?
540
+ [] : this.FormatParts(Math.abs(value.imaginary));
541
+
542
+ parts.push(...reformatted_imaginary, { text: NumberFormat.imaginary_character });
543
+
544
+ }
545
+ }
546
+ else if (has_imaginary_value) {
547
+
548
+ // only imaginary part
549
+ parts.push(...imaginary_format, { text: NumberFormat.imaginary_character });
550
+
551
+ }
552
+
553
+ return parts;
554
+ }
555
+
556
+ /**
557
+ * this method composes the format as a set of parts with various
558
+ * states. it's intended for graphical representation where things
559
+ * like hidden characters and padding require multiple passes or measurement.
560
+ */
561
+ public FormatParts(value: any): TextPart[] {
562
+
563
+ // new, shortcut
564
+ if (typeof value !== 'number' && !this.sections[3]) {
565
+
566
+ // NOTE: that note (next line) seems to be incorrect, not sure why
567
+ // ofc if that was true there'd be no point to this block...
568
+
569
+ return [{ text: value.toString() }] as TextPart[]; // unreachable because we ensure 4 sections
570
+ }
571
+
572
+ const { parts, section } = this.BaseFormat(value);
573
+ let text_parts: TextPart[] = [];
574
+
575
+ if (section.date_format || section.string_format) {
576
+ for (const part of parts) {
577
+ if (typeof part === 'string') {
578
+ text_parts.push({ text: part });
579
+ }
580
+ else text_parts.push(part);
581
+ }
582
+ }
583
+ else {
584
+
585
+ // magic
586
+
587
+ if (this.magic_decimal && parts[1] === '') {
588
+ parts.splice(1, 1);
589
+ }
590
+
591
+ text_parts = [
592
+ ...(section.prefix.map((text_part) => {
593
+ return { ...text_part };
594
+ })),
595
+ { text: section.has_number_format ? parts.join(Localization.decimal_separator) : '' },
596
+ ...(section.suffix.map((text_part) => {
597
+ return { ...text_part };
598
+ })),
599
+ ];
600
+ }
601
+
602
+ for (let i = 1; i < text_parts.length; i++) {
603
+ if (text_parts[i].flag === text_parts[i - 1].flag) {
604
+ text_parts[i].text = text_parts[i - 1].text + text_parts[i].text;
605
+ text_parts[i - 1].text = '';
606
+ }
607
+ }
608
+
609
+ return text_parts.filter((text_part) => text_part.text); // remove empty
610
+ }
611
+
612
+ /**
613
+ * formats a number as text.
614
+ *
615
+ * this method will use a single space to replace hidden (leading-underscore)
616
+ * characters. if a text width is provided, it will use that for padding;
617
+ * otherwise the padding character (we only allow a single padding character)
618
+ * is rendered once.
619
+ *
620
+ * FIXME: date, string (this is lagging)
621
+ * UPDATE: unifying, basing this on the text part functionality
622
+ */
623
+ public Format(value: any, text_width = 0): string {
624
+
625
+ /*
626
+ const parts = this.FormatParts(value);
627
+ let padded = -1;
628
+
629
+ const formatted = parts.map((part, index) => {
630
+ switch (part.flag) {
631
+ case TextPartFlag.padded:
632
+ padded = index;
633
+ return part.text;
634
+
635
+ case TextPartFlag.hidden:
636
+ return part.text.replace(/./g, ' ');
637
+
638
+ case TextPartFlag.formatting:
639
+ return '';
640
+
641
+ default:
642
+ return part.text;
643
+ }
644
+ });
645
+
646
+ if (padded >= 0 && text_width) {
647
+ const total_length = formatted.reduce((a, str, index) => (index === padded) ? a : a + str.length, 0);
648
+ let tmp = '';
649
+ for (let i = 0; i < text_width - total_length; i++){
650
+ tmp += formatted[padded];
651
+ }
652
+ formatted[padded] = tmp;
653
+ }
654
+
655
+ return formatted.join('');
656
+ */
657
+
658
+ return NumberFormat.FormatPartsAsText(this.FormatParts(value), text_width);
659
+
660
+ }
661
+
662
+ public ZeroPad(text: string, length: number): string {
663
+ while (text.length < length) text = '0' + text;
664
+ return text;
665
+ }
666
+
667
+ public DateFormat(value: number) {
668
+
669
+ const date = LotusDate(value);
670
+ const section = this.sections[0];
671
+
672
+ let hours = date.getUTCHours();
673
+ if (section.twelve_hour) {
674
+ if (hours > 12) hours -= 12;
675
+ if (hours === 0) hours = 12;
676
+ }
677
+
678
+ const parts: TextPart[] = section.prefix.map((part) => {
679
+ if (part.flag === TextPartFlag.date_component_minutes) {
680
+ if (part.text === 'mm') {
681
+ return { text: this.ZeroPad((date.getUTCMinutes()).toString(), 2) };
682
+ }
683
+ return { text: this.ZeroPad((date.getUTCMinutes()).toString(), 1) };
684
+ }
685
+ else if (part.flag === TextPartFlag.date_component) {
686
+ switch (part.text.toLowerCase()) {
687
+ case 'am/pm':
688
+ case 'a/p':
689
+ {
690
+ const elements = part.text.split('/');
691
+ return { text: date.getUTCHours() > 12 ? elements[1] : elements[0] };
692
+ }
693
+ case 'mmmmm':
694
+ return { text: Localization.date_components.long_months[date.getUTCMonth()][0] };
695
+ case 'mmmm':
696
+ if (part.text === 'MMMM') {
697
+ return { text: Localization.date_components.long_months[date.getUTCMonth()].toUpperCase() };
698
+ }
699
+ return { text: Localization.date_components.long_months[date.getUTCMonth()] };
700
+ case 'mmm':
701
+ if (part.text === 'MMM') {
702
+ return { text: Localization.date_components.short_months[date.getUTCMonth()].toUpperCase() };
703
+ }
704
+ return { text: Localization.date_components.short_months[date.getUTCMonth()] };
705
+ case 'mm':
706
+ return { text: this.ZeroPad((date.getUTCMonth() + 1).toString(), 2) };
707
+ case 'm':
708
+ return { text: this.ZeroPad((date.getUTCMonth() + 1).toString(), 1) };
709
+
710
+ case 'ddddd':
711
+ case 'dddd':
712
+ if (part.text === 'DDDDD' || part.text === 'DDDD') {
713
+ return { text: Localization.date_components.long_days[date.getUTCDay()].toUpperCase() };
714
+ }
715
+ return { text: Localization.date_components.long_days[date.getUTCDay()] };
716
+ case 'ddd':
717
+ if (part.text === 'DDD') {
718
+ return { text: Localization.date_components.short_days[date.getUTCDay()].toUpperCase() };
719
+ }
720
+ return { text: Localization.date_components.short_days[date.getUTCDay()] };
721
+ case 'dd':
722
+ return { text: this.ZeroPad((date.getUTCDate()).toString(), 2) };
723
+ case 'd':
724
+ return { text: this.ZeroPad((date.getUTCDate()).toString(), 1) };
725
+
726
+ case 'yyyy':
727
+ case 'yyy':
728
+ return { text: date.getUTCFullYear().toString() };
729
+ case 'yy':
730
+ case 'y':
731
+ // return { text: (date.getUTCFullYear() % 100).toString() };
732
+ return { text: this.ZeroPad((date.getUTCFullYear() % 100).toString(), 2) };
733
+
734
+ case 'hh':
735
+ return { text: this.ZeroPad(hours.toString(), 2) };
736
+ case 'h':
737
+ return { text: this.ZeroPad(hours.toString(), 1) };
738
+
739
+ case 'ss':
740
+ return { text: this.ZeroPad((date.getUTCSeconds()).toString(), 2) };
741
+ case 's':
742
+ return { text: this.ZeroPad((date.getUTCSeconds()).toString(), 1) };
743
+
744
+ }
745
+
746
+ const match = part.text.match(/^(s+)\.(0+)$/);
747
+ if (match) {
748
+ return {
749
+ text: this.ZeroPad(date.getUTCSeconds().toString(), match[1].length) +
750
+ Localization.decimal_separator +
751
+ (date.getUTCMilliseconds() / 1000).toFixed(match[2].length).substr(2),
752
+ };
753
+ }
754
+
755
+ }
756
+ return { ...part }; // text: part.text, state: part.state};
757
+ });
758
+
759
+ return { parts, section };
760
+ }
761
+
762
+ public StringFormat(value: string, section: NumberFormatSection) {
763
+ const parts: TextPart[] = [];
764
+ for (const part of section.prefix) {
765
+ if (part.flag === TextPartFlag.literal) {
766
+ parts.push({ text: value });
767
+ }
768
+ else parts.push({ ...part });
769
+ }
770
+ return {
771
+ parts, section,
772
+ };
773
+ }
774
+
775
+ /*
776
+ public DecimalAdjustRound(value: number, exp: number) {
777
+
778
+ if (!exp) { return Math.round(value); }
779
+
780
+ value = +value;
781
+ // exp = +exp;
782
+
783
+ // Shift
784
+ let values = value.toString().split('e');
785
+ value = Math.round(+(values[0] + 'e' + (values[1] ? (+values[1] - exp) : -exp)));
786
+
787
+ // Shift back
788
+ values = value.toString().split('e');
789
+ return +(values[0] + 'e' + (values[1] ? (+values[1] + exp) : exp));
790
+
791
+ }
792
+ */
793
+
794
+ public Round2(value: number, digits: number): number {
795
+ const m = Math.pow(10, digits);
796
+ return Math.round(m * value) / m;
797
+ }
798
+
799
+ public FormatFraction(value: number, section: NumberFormatSection): string {
800
+
801
+ if (section.percent) {
802
+ value *= 100;
803
+ }
804
+
805
+ let candidate = {
806
+ denominator: 1,
807
+ numerator: Math.round(value),
808
+ error: Math.abs(Math.round(value) - value),
809
+ };
810
+
811
+ if (section.fraction_denominator) {
812
+ candidate.denominator = section.fraction_denominator;
813
+ candidate.numerator = Math.round(value * candidate.denominator);
814
+ }
815
+ else {
816
+
817
+ if (candidate.error) {
818
+ const limit = NumberFormat.fraction_limits[section.fraction_denominator_digits - 1] || NumberFormat.fraction_limits[0];
819
+ for (let denominator = 2; denominator <= limit; denominator++) {
820
+ const numerator = Math.round(value * denominator);
821
+ const error = Math.abs(numerator / denominator - value);
822
+ if (error < candidate.error) {
823
+ candidate = {
824
+ numerator, denominator, error,
825
+ };
826
+ if (!error) { break; }
827
+ }
828
+ }
829
+ }
830
+
831
+ }
832
+
833
+ const text: string[] = [];
834
+
835
+ if (section.fraction_integer) {
836
+ const integer = Math.floor(candidate.numerator / candidate.denominator);
837
+ candidate.numerator %= candidate.denominator;
838
+ if (integer || !candidate.numerator) {
839
+ text.push(integer.toString());
840
+ if (candidate.numerator) {
841
+ text.push(' ');
842
+ }
843
+ }
844
+ }
845
+ else if (!candidate.numerator) {
846
+ text.push('0');
847
+ }
848
+
849
+ if (candidate.numerator) {
850
+ text.push(candidate.numerator.toString());
851
+ text.push('/');
852
+ text.push(candidate.denominator.toString());
853
+ }
854
+
855
+ return text.join('');
856
+
857
+ }
858
+
859
+ public BaseFormat(value: any) {
860
+
861
+ if (this.sections[0].date_format) {
862
+ return this.DateFormat(Number(value));
863
+ }
864
+
865
+ if (typeof value !== 'number') {
866
+ return this.StringFormat(value.toString(), this.sections[3]);
867
+ }
868
+
869
+ let section = this.sections[0];
870
+ let zero_regexp = this.decimal_zero_regexp[0];
871
+
872
+ if (value < 0) {
873
+ section = this.sections[1];
874
+ }
875
+
876
+ const max_digits = section.percent ?
877
+ section.decimal_max_digits + 2 :
878
+ section.decimal_max_digits;
879
+
880
+ const epsilon = Math.pow(10, -max_digits) / 2;
881
+ let abs_value = Math.abs(value);
882
+
883
+ if (abs_value < epsilon) {
884
+ section = this.sections[2];
885
+ zero_regexp = this.decimal_zero_regexp[2];
886
+ }
887
+
888
+ // there's kind of a weird thing here where we might have
889
+ // a non-zero number but scaling turns it into zero...
890
+
891
+ if (section.scaling) {
892
+ abs_value /= section.scaling;
893
+ if (abs_value < epsilon) {
894
+ section = this.sections[2];
895
+ zero_regexp = this.decimal_zero_regexp[2];
896
+ }
897
+ }
898
+
899
+ if (section.string_format) {
900
+ return this.StringFormat(value.toString(), section);
901
+ }
902
+
903
+ let representation = '';
904
+
905
+ // special handling for fractions skips most of the other bits
906
+ if (section.fraction_format) {
907
+ return { parts: [this.FormatFraction(abs_value, section)], section };
908
+ }
909
+
910
+ if (section.exponential) {
911
+ representation = abs_value.toExponential(section.decimal_max_digits);
912
+ }
913
+ else {
914
+ if (section.percent) {
915
+ abs_value *= 100;
916
+ }
917
+ representation = this.Round2(abs_value, section.decimal_max_digits).toFixed(section.decimal_max_digits);
918
+ }
919
+
920
+ if (zero_regexp) {
921
+ representation = representation.replace(zero_regexp, '');
922
+ }
923
+
924
+ const parts = representation.split('.');
925
+
926
+ while (parts[0].length < section.integer_min_digits) {
927
+ parts[0] = ('0000000000000000' + parts[0]).slice(-section.integer_min_digits);
928
+ }
929
+
930
+ if (section.integer_min_digits === 0 && parts[0] === '0') {
931
+ parts[0] = ''; // not sure why anyone would want that
932
+ }
933
+
934
+ if (section.grouping) {
935
+ parts[0] = parts[0].replace(NumberFormat.grouping_regexp, '$&' + Localization.grouping_separator);
936
+ }
937
+
938
+ return { parts, section };
939
+
940
+ }
941
+
942
+ }