reamkit 1.15.3 → 1.16.0
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/dist/esm/core/converter/facade.d.ts +5 -0
- package/dist/esm/core/converter/ream.d.ts +6 -0
- package/dist/esm/core/converter/ream.js +34 -8
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +187 -1
- package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
- package/dist/esm/core/drawingml/chart-geometry.js +371 -88
- package/dist/esm/core/drawingml/chart-parser.js +262 -11
- package/dist/esm/core/drawingml/colors.d.ts +6 -6
- package/dist/esm/core/drawingml/colors.js +58 -18
- package/dist/esm/core/drawingml/preset-geometry.js +7 -6
- package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
- package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
- package/dist/esm/core/drawingml/theme-parser.js +74 -1
- package/dist/esm/core/font/ttf-subset.js +26 -1
- package/dist/esm/core/images.js +172 -43
- package/dist/esm/core/indexed-colors.d.ts +11 -0
- package/dist/esm/core/indexed-colors.js +81 -0
- package/dist/esm/core/ir/sheet.d.ts +67 -3
- package/dist/esm/{excel → core}/number-format.d.ts +37 -0
- package/dist/esm/core/number-format.js +800 -0
- package/dist/esm/core/opc/alternate-content.d.ts +13 -0
- package/dist/esm/core/opc/alternate-content.js +72 -0
- package/dist/esm/core/opc/core-properties.js +1 -0
- package/dist/esm/core/opc/package.d.ts +14 -1
- package/dist/esm/core/opc/package.js +43 -4
- package/dist/esm/core/opc/relationships.js +1 -0
- package/dist/esm/core/opc/xml-entities.d.ts +12 -0
- package/dist/esm/core/opc/xml-entities.js +91 -0
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
- package/dist/esm/core/vector.d.ts +13 -0
- package/dist/esm/excel/activex-parser.d.ts +9 -0
- package/dist/esm/excel/activex-parser.js +16 -1
- package/dist/esm/excel/column-bands.d.ts +26 -1
- package/dist/esm/excel/column-bands.js +156 -20
- package/dist/esm/excel/comments-parser.js +5 -3
- package/dist/esm/excel/conditional-format.d.ts +25 -2
- package/dist/esm/excel/conditional-format.js +63 -19
- package/dist/esm/excel/escaped-text.d.ts +9 -0
- package/dist/esm/excel/escaped-text.js +20 -0
- package/dist/esm/excel/form-control-parser.js +1 -0
- package/dist/esm/excel/formula/dates.js +1 -1
- package/dist/esm/excel/header-footer.d.ts +9 -1
- package/dist/esm/excel/header-footer.js +162 -14
- package/dist/esm/excel/index.d.ts +1 -1
- package/dist/esm/excel/index.js +2 -2
- package/dist/esm/excel/pivot-table-parser.js +1 -0
- package/dist/esm/excel/print-model.d.ts +75 -0
- package/dist/esm/excel/print-model.js +902 -77
- package/dist/esm/excel/printer-settings.d.ts +16 -0
- package/dist/esm/excel/printer-settings.js +36 -0
- package/dist/esm/excel/rich-value.d.ts +13 -0
- package/dist/esm/excel/rich-value.js +120 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -7
- package/dist/esm/excel/sheet-drawing.d.ts +11 -0
- package/dist/esm/excel/sheet-drawing.js +26 -3
- package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
- package/dist/esm/excel/sheet-shape-parser.js +461 -32
- package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
- package/dist/esm/excel/sheet-to-flow.js +551 -36
- package/dist/esm/excel/slicer-parser.js +1 -0
- package/dist/esm/excel/styles-parser.d.ts +30 -2
- package/dist/esm/excel/styles-parser.js +177 -40
- package/dist/esm/excel/table-parser.js +1 -0
- package/dist/esm/excel/vml-drawing.d.ts +68 -0
- package/dist/esm/excel/vml-drawing.js +158 -0
- package/dist/esm/excel/workbook-parser.d.ts +5 -0
- package/dist/esm/excel/workbook-parser.js +5 -1
- package/dist/esm/excel/worksheet-parser.d.ts +2 -1
- package/dist/esm/excel/worksheet-parser.js +149 -28
- package/dist/esm/excel/xlsx-reader.js +271 -33
- package/dist/esm/excel/xlsx-to-pdf.js +12 -1
- package/dist/esm/html/html-writer.js +11 -2
- package/dist/esm/layout/page-doc.d.ts +26 -0
- package/dist/esm/layout/styled-layout.d.ts +2 -0
- package/dist/esm/layout/styled-layout.js +493 -64
- package/dist/esm/pdf/cid-font.js +20 -9
- package/dist/esm/pdf/objects.d.ts +13 -0
- package/dist/esm/pdf/objects.js +18 -1
- package/dist/esm/pdf/styled-page-emitter.js +164 -32
- package/dist/esm/pdf/text-page-renderer.js +1 -0
- package/dist/esm/pdf/vector-graphics.d.ts +3 -1
- package/dist/esm/pdf/vector-graphics.js +19 -2
- package/dist/esm/pptx/pptx-reader.js +1 -0
- package/dist/esm/pptx/slide-parser.js +12 -1
- package/dist/esm/svg/svg-writer.js +23 -4
- package/dist/esm/word/document-parser.js +1 -0
- package/dist/esm/word/drawing-parser.d.ts +34 -1
- package/dist/esm/word/drawing-parser.js +52 -2
- package/dist/esm/word/numbering-parser.js +1 -0
- package/dist/esm/word/settings-parser.js +1 -0
- package/dist/esm/word/styles-parser.js +1 -0
- package/package.json +7 -3
- package/dist/esm/excel/number-format.js +0 -476
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { INDEXED_COLORS } from "./indexed-colors.js";
|
|
2
|
+
//#region src/core/number-format.ts
|
|
3
|
+
var BUILTIN = new Map([
|
|
4
|
+
[0, "General"],
|
|
5
|
+
[1, "0"],
|
|
6
|
+
[2, "0.00"],
|
|
7
|
+
[3, "#,##0"],
|
|
8
|
+
[4, "#,##0.00"],
|
|
9
|
+
[5, "$#,##0_);($#,##0)"],
|
|
10
|
+
[6, "$#,##0_);[Red]($#,##0)"],
|
|
11
|
+
[7, "$#,##0.00_);($#,##0.00)"],
|
|
12
|
+
[8, "$#,##0.00_);[Red]($#,##0.00)"],
|
|
13
|
+
[9, "0%"],
|
|
14
|
+
[10, "0.00%"],
|
|
15
|
+
[11, "0.00E+00"],
|
|
16
|
+
[12, "# ?/?"],
|
|
17
|
+
[13, "# ??/??"],
|
|
18
|
+
[37, "#,##0_);(#,##0)"],
|
|
19
|
+
[38, "#,##0_);[Red](#,##0)"],
|
|
20
|
+
[39, "#,##0.00_);(#,##0.00)"],
|
|
21
|
+
[40, "#,##0.00_);[Red](#,##0.00)"],
|
|
22
|
+
[48, "##0.0E+0"],
|
|
23
|
+
[49, "@"]
|
|
24
|
+
]);
|
|
25
|
+
var BUILTIN_DATE_FORMATS = new Map([
|
|
26
|
+
[14, "m/d/yyyy"],
|
|
27
|
+
[15, "d-mmm-yy"],
|
|
28
|
+
[16, "d-mmm"],
|
|
29
|
+
[17, "mmm-yy"],
|
|
30
|
+
[18, "h:mm AM/PM"],
|
|
31
|
+
[19, "h:mm:ss AM/PM"],
|
|
32
|
+
[20, "h:mm"],
|
|
33
|
+
[21, "h:mm:ss"],
|
|
34
|
+
[22, "m/d/yyyy h:mm"],
|
|
35
|
+
[45, "mm:ss"],
|
|
36
|
+
[46, "[h]:mm:ss"],
|
|
37
|
+
[47, "mm:ss.0"]
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Render a cell's raw stored value through its number format (§18.8.30 built-ins
|
|
41
|
+
* and §18.8.31 custom codes). Handles `General`, text (`@`), the built-in date /
|
|
42
|
+
* time formats, custom date codes, and the numeric placeholder grammar (`0` `#`
|
|
43
|
+
* `,` `.` `%`, scientific notation, quoted literals, `;`-separated sections). An
|
|
44
|
+
* unknown id, or a value that does not parse as a number, falls back to a plain
|
|
45
|
+
* render of `rawValue`.
|
|
46
|
+
*
|
|
47
|
+
* @param rawValue The cell's raw stored value (a serial for dates).
|
|
48
|
+
* @param numFmtId The number-format id from the cell's `cellXf`.
|
|
49
|
+
* @param customFormats Custom format codes by id (from `<numFmts>`).
|
|
50
|
+
* @param date1904 The workbook date epoch (1904 vs the 1900 default).
|
|
51
|
+
* @returns The formatted display string.
|
|
52
|
+
*/
|
|
53
|
+
function applyNumberFormat(rawValue, numFmtId, customFormats, date1904 = false) {
|
|
54
|
+
if (rawValue.length === 0) return "";
|
|
55
|
+
if (numFmtId === 0) return defaultNumberRender(rawValue);
|
|
56
|
+
if (numFmtId === 49) return rawValue;
|
|
57
|
+
const builtinDate = BUILTIN_DATE_FORMATS.get(numFmtId);
|
|
58
|
+
if (builtinDate !== void 0) return formatExcelDate(rawValue, builtinDate, date1904);
|
|
59
|
+
const custom = customFormats.get(numFmtId);
|
|
60
|
+
if (custom !== void 0 && (isDateFormat(custom) || hasElapsedToken(custom))) return formatExcelDate(rawValue, custom, date1904);
|
|
61
|
+
const format = custom ?? BUILTIN.get(numFmtId);
|
|
62
|
+
if (!format) return defaultNumberRender(rawValue);
|
|
63
|
+
if (format.trim().toLowerCase() === "general") return defaultNumberRender(rawValue);
|
|
64
|
+
return applyFormatString(rawValue, format);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The colour a number format paints its cell in (§18.8.31), as a 6-digit RRGGBB,
|
|
68
|
+
* or undefined when the section that applies declares none.
|
|
69
|
+
*
|
|
70
|
+
* A `[Red]` at the head of a section is part of the format, not decoration: the
|
|
71
|
+
* accounting codes write the negative section as `[Red]-#,##0.00`, and rendering
|
|
72
|
+
* it in the cell's own font colour loses the one signal the format exists for.
|
|
73
|
+
* `[Color 12]` indexes the same legacy palette a `<color indexed>` does.
|
|
74
|
+
*
|
|
75
|
+
* @param rawValue The cell's raw stored value — it selects the section.
|
|
76
|
+
* @param numFmtId The number-format id from the cell's `cellXf`.
|
|
77
|
+
* @param customFormats Custom format codes by id (from `<numFmts>`).
|
|
78
|
+
*/
|
|
79
|
+
function numberFormatColorHex(rawValue, numFmtId, customFormats) {
|
|
80
|
+
const format = customFormats.get(numFmtId) ?? BUILTIN.get(numFmtId);
|
|
81
|
+
if (format === void 0 || !format.includes("[")) return void 0;
|
|
82
|
+
const sections = splitSections(format);
|
|
83
|
+
const n = Number(rawValue);
|
|
84
|
+
const idx = Number.isFinite(n) ? sectionIndexFor(n, sections) : 3;
|
|
85
|
+
if (idx < 0) return void 0;
|
|
86
|
+
return colorOfSection(sections[idx] ?? sections[0]);
|
|
87
|
+
}
|
|
88
|
+
var FORMAT_COLORS = new Map([
|
|
89
|
+
["black", "000000"],
|
|
90
|
+
["blue", "0000FF"],
|
|
91
|
+
["cyan", "00FFFF"],
|
|
92
|
+
["green", "00FF00"],
|
|
93
|
+
["magenta", "FF00FF"],
|
|
94
|
+
["red", "FF0000"],
|
|
95
|
+
["white", "FFFFFF"],
|
|
96
|
+
["yellow", "FFFF00"]
|
|
97
|
+
]);
|
|
98
|
+
function colorOfSection(section) {
|
|
99
|
+
for (const m of section.matchAll(/\[([^\]]*)\]/g)) {
|
|
100
|
+
const body = m[1].trim().toLowerCase();
|
|
101
|
+
const named = FORMAT_COLORS.get(body);
|
|
102
|
+
if (named !== void 0) return named;
|
|
103
|
+
const indexed = /^color\s*(\d+)$/.exec(body);
|
|
104
|
+
if (indexed) {
|
|
105
|
+
const i = Number(indexed[1]) - 1;
|
|
106
|
+
if (i >= 0 && i < INDEXED_COLORS.length) return INDEXED_COLORS[i];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* §18.8.31 — a section may carry a CONDITION in brackets (`[>999]`), and then
|
|
112
|
+
* the sections are no longer positive/negative/zero: the first whose test the
|
|
113
|
+
* value passes applies, and a section without one is the else. Returns the
|
|
114
|
+
* comparison, or undefined for a section that states none.
|
|
115
|
+
*
|
|
116
|
+
* The operator is what tells this bracket from the others a section may carry —
|
|
117
|
+
* `[Red]`, `[$€-407]`, `[h]` — none of which start with one.
|
|
118
|
+
*/
|
|
119
|
+
function conditionOf(section) {
|
|
120
|
+
const m = /\[\s*(<=|>=|<>|<|>|=)\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*\]/.exec(section);
|
|
121
|
+
if (!m) return void 0;
|
|
122
|
+
const rhs = Number(m[2]);
|
|
123
|
+
return Number.isFinite(rhs) ? {
|
|
124
|
+
op: m[1],
|
|
125
|
+
rhs
|
|
126
|
+
} : void 0;
|
|
127
|
+
}
|
|
128
|
+
function conditionHolds(n, c) {
|
|
129
|
+
switch (c.op) {
|
|
130
|
+
case "<": return n < c.rhs;
|
|
131
|
+
case "<=": return n <= c.rhs;
|
|
132
|
+
case ">": return n > c.rhs;
|
|
133
|
+
case ">=": return n >= c.rhs;
|
|
134
|
+
case "=": return n === c.rhs;
|
|
135
|
+
default: return n !== c.rhs;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The section that applies to `n`, or -1 when the format is all conditions and
|
|
140
|
+
* the value passes none of them — Excel then shows the number plainly, which is
|
|
141
|
+
* what FormatChoiceTests.xlsx expects of `[<10]#" Wow"` at 11.
|
|
142
|
+
*/
|
|
143
|
+
function sectionIndexFor(n, sections) {
|
|
144
|
+
const numeric = Math.min(sections.length, 3);
|
|
145
|
+
const conditions = sections.slice(0, numeric).map(conditionOf);
|
|
146
|
+
if (conditions.some((c) => c !== void 0)) {
|
|
147
|
+
for (let i = 0; i < numeric; i++) {
|
|
148
|
+
const c = conditions[i];
|
|
149
|
+
if (c === void 0 || conditionHolds(n, c)) return i;
|
|
150
|
+
}
|
|
151
|
+
return -1;
|
|
152
|
+
}
|
|
153
|
+
if (n > 0) return 0;
|
|
154
|
+
if (n < 0) return sections.length > 1 ? 1 : 0;
|
|
155
|
+
return sections.length > 2 ? 2 : 0;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Excel serial date → JS `Date`. The default 1900 epoch uses 1899-12-30 as day 0
|
|
159
|
+
* (so serial 1 is 1900-01-01) and inherits the Lotus 1-2-3 leap-year bug: serial 60
|
|
160
|
+
* is considered "1900-02-29" which never existed. For serial ≥ 61 the simple
|
|
161
|
+
* formula is exact; values < 60 are vanishingly rare in business sheets (and our
|
|
162
|
+
* render is approximate anyway).
|
|
163
|
+
*
|
|
164
|
+
* The 1904 epoch (legacy Mac Excel) uses 1904-01-01 as day 0 and has no leap bug.
|
|
165
|
+
* Files saved with `<workbookPr date1904="1"/>` store dates offset by exactly 1462
|
|
166
|
+
* days from the 1900-epoch interpretation.
|
|
167
|
+
*/
|
|
168
|
+
function excelSerialToDate(serial, date1904) {
|
|
169
|
+
const ms = serial * 86400 * 1e3;
|
|
170
|
+
if (date1904) return new Date(Date.UTC(1904, 0, 1) + ms);
|
|
171
|
+
return new Date(Date.UTC(1899, 11, 30) + ms);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The inverse over a UTC calendar date: `(year, month0, day)` → the integer Excel
|
|
175
|
+
* serial day, using the same epoch. Round-trips {@link excelSerialToDate} exactly
|
|
176
|
+
* for an integer serial (the time-of-day is zero), so a serial → parts → serial
|
|
177
|
+
* loop is stable. Used by the formula engine's date functions and the `timePeriod`
|
|
178
|
+
* windows (E-SHEET W9) to map an injected reference date into serial space.
|
|
179
|
+
*
|
|
180
|
+
* @param month0 The 0-indexed month (0 = January).
|
|
181
|
+
*/
|
|
182
|
+
function excelSerialFromUtcParts(year, month0, day, date1904) {
|
|
183
|
+
const epoch = date1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
|
|
184
|
+
return Math.round((Date.UTC(year, month0, day) - epoch) / 864e5);
|
|
185
|
+
}
|
|
186
|
+
var MONTH_ABBR = [
|
|
187
|
+
"Jan",
|
|
188
|
+
"Feb",
|
|
189
|
+
"Mar",
|
|
190
|
+
"Apr",
|
|
191
|
+
"May",
|
|
192
|
+
"Jun",
|
|
193
|
+
"Jul",
|
|
194
|
+
"Aug",
|
|
195
|
+
"Sep",
|
|
196
|
+
"Oct",
|
|
197
|
+
"Nov",
|
|
198
|
+
"Dec"
|
|
199
|
+
];
|
|
200
|
+
var MONTH_FULL = [
|
|
201
|
+
"January",
|
|
202
|
+
"February",
|
|
203
|
+
"March",
|
|
204
|
+
"April",
|
|
205
|
+
"May",
|
|
206
|
+
"June",
|
|
207
|
+
"July",
|
|
208
|
+
"August",
|
|
209
|
+
"September",
|
|
210
|
+
"October",
|
|
211
|
+
"November",
|
|
212
|
+
"December"
|
|
213
|
+
];
|
|
214
|
+
var DAY_ABBR = [
|
|
215
|
+
"Sun",
|
|
216
|
+
"Mon",
|
|
217
|
+
"Tue",
|
|
218
|
+
"Wed",
|
|
219
|
+
"Thu",
|
|
220
|
+
"Fri",
|
|
221
|
+
"Sat"
|
|
222
|
+
];
|
|
223
|
+
var DAY_FULL = [
|
|
224
|
+
"Sunday",
|
|
225
|
+
"Monday",
|
|
226
|
+
"Tuesday",
|
|
227
|
+
"Wednesday",
|
|
228
|
+
"Thursday",
|
|
229
|
+
"Friday",
|
|
230
|
+
"Saturday"
|
|
231
|
+
];
|
|
232
|
+
function pad2(n) {
|
|
233
|
+
return String(n).padStart(2, "0");
|
|
234
|
+
}
|
|
235
|
+
/** An elapsed h/m/s count, truncated and zero-padded to its placeholder width. */
|
|
236
|
+
function elapsed(value, width) {
|
|
237
|
+
const whole = Math.floor(value);
|
|
238
|
+
const digits = String(Math.abs(whole)).padStart(width, "0");
|
|
239
|
+
return whole < 0 ? `-${digits}` : digits;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `[h]`, `[mm]`, `[ss]` — the elapsed-time brackets (§18.8.31), which suppress
|
|
243
|
+
* the wrap at 24 h / 60 min / 60 s and count from serial 0 instead.
|
|
244
|
+
*
|
|
245
|
+
* They carry no date letter outside the bracket, so a code like `[ss].00` reads
|
|
246
|
+
* as a plain numeric format to {@link isDateFormat} and lands in the placeholder
|
|
247
|
+
* grammar, where `[ss]` is stripped as if it were a colour and `.00` renders the
|
|
248
|
+
* serial as ".3".
|
|
249
|
+
*/
|
|
250
|
+
function hasElapsedToken(code) {
|
|
251
|
+
return /\[(?:h+|m+|s+)\]/i.test(code.replace(/"[^"]*"/g, ""));
|
|
252
|
+
}
|
|
253
|
+
function isDateFormat(code) {
|
|
254
|
+
const cleaned = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "");
|
|
255
|
+
return /[dyhs]|m+/i.test(cleaned) && /[dyhs]/i.test(cleaned);
|
|
256
|
+
}
|
|
257
|
+
function tokenizeDateFormat(format) {
|
|
258
|
+
const tokens = [];
|
|
259
|
+
let i = 0;
|
|
260
|
+
while (i < format.length) {
|
|
261
|
+
const ch = format[i];
|
|
262
|
+
if (ch === "\"") {
|
|
263
|
+
let lit = "";
|
|
264
|
+
i++;
|
|
265
|
+
while (i < format.length && format[i] !== "\"") {
|
|
266
|
+
lit += format[i];
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
i++;
|
|
270
|
+
tokens.push({
|
|
271
|
+
kind: "lit",
|
|
272
|
+
text: lit
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (ch === "\\" && i + 1 < format.length) {
|
|
277
|
+
tokens.push({
|
|
278
|
+
kind: "lit",
|
|
279
|
+
text: format[i + 1]
|
|
280
|
+
});
|
|
281
|
+
i += 2;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (ch === "[") {
|
|
285
|
+
let body = "";
|
|
286
|
+
i++;
|
|
287
|
+
while (i < format.length && format[i] !== "]") {
|
|
288
|
+
body += format[i];
|
|
289
|
+
i++;
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
if (/^h+$/i.test(body)) tokens.push({
|
|
293
|
+
kind: "elapsed-h",
|
|
294
|
+
text: body
|
|
295
|
+
});
|
|
296
|
+
else if (/^m+$/i.test(body)) tokens.push({
|
|
297
|
+
kind: "elapsed-m",
|
|
298
|
+
text: body
|
|
299
|
+
});
|
|
300
|
+
else if (/^s+$/i.test(body)) tokens.push({
|
|
301
|
+
kind: "elapsed-s",
|
|
302
|
+
text: body
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (ch === ".") {
|
|
307
|
+
const last = tokens[tokens.length - 1];
|
|
308
|
+
const run = /^\.(0+)/.exec(format.substring(i));
|
|
309
|
+
if (run && (last?.kind === "s" || last?.kind === "elapsed-s")) {
|
|
310
|
+
tokens.push({
|
|
311
|
+
kind: "subsec",
|
|
312
|
+
text: run[1]
|
|
313
|
+
});
|
|
314
|
+
i += run[0].length;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const lower = ch.toLowerCase();
|
|
319
|
+
if (lower === "y" || lower === "m" || lower === "d" || lower === "h" || lower === "s") {
|
|
320
|
+
let run = ch;
|
|
321
|
+
i++;
|
|
322
|
+
while (i < format.length && format[i].toLowerCase() === lower) {
|
|
323
|
+
run += format[i];
|
|
324
|
+
i++;
|
|
325
|
+
}
|
|
326
|
+
if (lower === "y") tokens.push({
|
|
327
|
+
kind: "y",
|
|
328
|
+
text: run
|
|
329
|
+
});
|
|
330
|
+
else if (lower === "d") tokens.push({
|
|
331
|
+
kind: "d",
|
|
332
|
+
text: run
|
|
333
|
+
});
|
|
334
|
+
else if (lower === "h") tokens.push({
|
|
335
|
+
kind: "h",
|
|
336
|
+
text: run
|
|
337
|
+
});
|
|
338
|
+
else if (lower === "s") tokens.push({
|
|
339
|
+
kind: "s",
|
|
340
|
+
text: run
|
|
341
|
+
});
|
|
342
|
+
else tokens.push({
|
|
343
|
+
kind: "m",
|
|
344
|
+
text: run
|
|
345
|
+
});
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (format.startsWith("AM/PM", i) || format.startsWith("A/P", i)) {
|
|
349
|
+
const len = format.startsWith("AM/PM", i) ? 5 : 3;
|
|
350
|
+
tokens.push({
|
|
351
|
+
kind: "ampm",
|
|
352
|
+
text: format.substring(i, i + len)
|
|
353
|
+
});
|
|
354
|
+
i += len;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
tokens.push({
|
|
358
|
+
kind: "lit",
|
|
359
|
+
text: ch
|
|
360
|
+
});
|
|
361
|
+
i++;
|
|
362
|
+
}
|
|
363
|
+
return tokens;
|
|
364
|
+
}
|
|
365
|
+
function resolveMonthVsMinute(tokens) {
|
|
366
|
+
const resolved = tokens.map((t) => ({ ...t }));
|
|
367
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
368
|
+
const t = resolved[i];
|
|
369
|
+
if (t.kind !== "m") continue;
|
|
370
|
+
let prev = i - 1;
|
|
371
|
+
while (prev >= 0 && resolved[prev].kind === "lit") prev--;
|
|
372
|
+
let next = i + 1;
|
|
373
|
+
while (next < resolved.length && resolved[next].kind === "lit") next++;
|
|
374
|
+
const prevKind = prev >= 0 ? resolved[prev].kind : void 0;
|
|
375
|
+
const prevIsHour = prevKind === "h" || prevKind === "elapsed-h";
|
|
376
|
+
const nextIsSec = next < resolved.length && resolved[next].kind === "s";
|
|
377
|
+
if (prevIsHour || nextIsSec) continue;
|
|
378
|
+
resolved[i] = {
|
|
379
|
+
kind: "M",
|
|
380
|
+
text: t.text
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return resolved;
|
|
384
|
+
}
|
|
385
|
+
function formatExcelDate(rawValue, format, date1904) {
|
|
386
|
+
const serial = Number(rawValue);
|
|
387
|
+
if (!Number.isFinite(serial)) return rawValue;
|
|
388
|
+
format = splitSections(format)[0] ?? format;
|
|
389
|
+
const tokens = resolveMonthVsMinute(tokenizeDateFormat(format));
|
|
390
|
+
const subsecDigits = tokens.find((t) => t.kind === "subsec")?.text.length ?? 0;
|
|
391
|
+
const scale = 10 ** subsecDigits;
|
|
392
|
+
const totalSeconds = subsecDigits > 0 ? Math.round(serial * 86400 * scale) / scale : serial * 86400;
|
|
393
|
+
const date = excelSerialToDate(subsecDigits > 0 ? totalSeconds / 86400 : serial, date1904);
|
|
394
|
+
const year = date.getUTCFullYear();
|
|
395
|
+
const month = date.getUTCMonth() + 1;
|
|
396
|
+
const day = date.getUTCDate();
|
|
397
|
+
const weekday = date.getUTCDay();
|
|
398
|
+
const hour24 = date.getUTCHours();
|
|
399
|
+
const minute = date.getUTCMinutes();
|
|
400
|
+
const second = date.getUTCSeconds();
|
|
401
|
+
const hourValue = tokens.some((t) => t.kind === "ampm") ? hour24 % 12 === 0 ? 12 : hour24 % 12 : hour24;
|
|
402
|
+
const ampmLabel = (template) => {
|
|
403
|
+
const isUpper = template[0] === "A";
|
|
404
|
+
const isShort = template.length <= 3;
|
|
405
|
+
if (hour24 < 12) return isShort ? isUpper ? "A" : "a" : isUpper ? "AM" : "am";
|
|
406
|
+
return isShort ? isUpper ? "P" : "p" : isUpper ? "PM" : "pm";
|
|
407
|
+
};
|
|
408
|
+
let out = "";
|
|
409
|
+
for (const t of tokens) switch (t.kind) {
|
|
410
|
+
case "lit":
|
|
411
|
+
out += t.text;
|
|
412
|
+
break;
|
|
413
|
+
case "y":
|
|
414
|
+
out += t.text.length <= 2 ? pad2(year % 100) : String(year).padStart(4, "0");
|
|
415
|
+
break;
|
|
416
|
+
case "M": {
|
|
417
|
+
const len = t.text.length;
|
|
418
|
+
if (len === 5) out += MONTH_FULL[month - 1][0];
|
|
419
|
+
else if (len === 4) out += MONTH_FULL[month - 1];
|
|
420
|
+
else if (len === 3) out += MONTH_ABBR[month - 1];
|
|
421
|
+
else if (len === 2) out += pad2(month);
|
|
422
|
+
else out += String(month);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case "d": {
|
|
426
|
+
const len = t.text.length;
|
|
427
|
+
if (len >= 4) out += DAY_FULL[weekday];
|
|
428
|
+
else if (len === 3) out += DAY_ABBR[weekday];
|
|
429
|
+
else if (len === 2) out += pad2(day);
|
|
430
|
+
else out += String(day);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "h":
|
|
434
|
+
out += t.text.length >= 2 ? pad2(hourValue) : String(hourValue);
|
|
435
|
+
break;
|
|
436
|
+
case "elapsed-h":
|
|
437
|
+
out += elapsed(totalSeconds / 3600, t.text.length);
|
|
438
|
+
break;
|
|
439
|
+
case "elapsed-m":
|
|
440
|
+
out += elapsed(totalSeconds / 60, t.text.length);
|
|
441
|
+
break;
|
|
442
|
+
case "elapsed-s":
|
|
443
|
+
out += elapsed(totalSeconds, t.text.length);
|
|
444
|
+
break;
|
|
445
|
+
case "subsec":
|
|
446
|
+
out += (totalSeconds - Math.floor(totalSeconds)).toFixed(subsecDigits).substring(1);
|
|
447
|
+
break;
|
|
448
|
+
case "m":
|
|
449
|
+
out += t.text.length >= 2 ? pad2(minute) : String(minute);
|
|
450
|
+
break;
|
|
451
|
+
case "s":
|
|
452
|
+
out += t.text.length >= 2 ? pad2(second) : String(second);
|
|
453
|
+
break;
|
|
454
|
+
case "ampm":
|
|
455
|
+
out += ampmLabel(t.text);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
function defaultNumberRender(rawValue) {
|
|
461
|
+
const n = Number(rawValue);
|
|
462
|
+
if (!Number.isFinite(n)) return rawValue;
|
|
463
|
+
return excelExponent(String(n));
|
|
464
|
+
}
|
|
465
|
+
/** Respell ECMAScript's `3e-104` as Excel's `3E-104` (§18.8.31). */
|
|
466
|
+
function excelExponent(rendered) {
|
|
467
|
+
const m = /^(-?[\d.]+)e([+-]?)(\d+)$/i.exec(rendered);
|
|
468
|
+
if (!m) return rendered;
|
|
469
|
+
return `${m[1]}E${m[2] === "-" ? "-" : "+"}${m[3].padStart(2, "0")}`;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* A General number rounded to the decimals that fit `maxChars` characters.
|
|
473
|
+
*
|
|
474
|
+
* General is not a fixed format: a spreadsheet shows as many decimal places as
|
|
475
|
+
* the column has room for and ROUNDS to that, so 4.3900875881221957 in a
|
|
476
|
+
* column eight characters wide reads 4.390088. We rendered every stored digit
|
|
477
|
+
* and let the cell clip it, which turns the same value into 4.390087 — off by
|
|
478
|
+
* one in the last place shown, with nothing to say a digit was cut.
|
|
479
|
+
*
|
|
480
|
+
* The integer part is never dropped: a number too wide even without decimals
|
|
481
|
+
* keeps them all and the cell says so its own way (see hashOnOverflow).
|
|
482
|
+
*
|
|
483
|
+
* `fits` decides, and it must measure in the face this will be DRAWN in rather
|
|
484
|
+
* than count characters against the column's width in the document's unit.
|
|
485
|
+
* Counted that way we produced nine digits for a column that holds nine of
|
|
486
|
+
* Excel's and eight of ours, and the layout clipped the ninth — the very
|
|
487
|
+
* truncation this exists to prevent. Fewer digits than the reference shows is a
|
|
488
|
+
* coarser number; a clipped one is a different number.
|
|
489
|
+
*
|
|
490
|
+
* @param rawValue The cell's stored value.
|
|
491
|
+
* @param fits Whether a rendering will fit the cell.
|
|
492
|
+
*/
|
|
493
|
+
function generalToWidth(rawValue, fits) {
|
|
494
|
+
const n = Number(rawValue);
|
|
495
|
+
if (!Number.isFinite(n)) return defaultNumberRender(rawValue);
|
|
496
|
+
const full = defaultNumberRender(rawValue);
|
|
497
|
+
if (fits(full)) return full;
|
|
498
|
+
const dot = full.indexOf(".");
|
|
499
|
+
if (dot < 0) return scientificToWidth(n, fits) ?? full;
|
|
500
|
+
let shortened = full;
|
|
501
|
+
for (let decimals = full.length - dot - 2; decimals >= 0; decimals--) {
|
|
502
|
+
const shifted = Number(`${Number(n.toPrecision(15))}e${decimals}`);
|
|
503
|
+
const rounded = shifted < 0 ? -Math.round(-shifted) : Math.round(shifted);
|
|
504
|
+
const back = Number(`${rounded}e${-decimals}`);
|
|
505
|
+
if (!Number.isFinite(back)) break;
|
|
506
|
+
shortened = defaultNumberRender(String(back));
|
|
507
|
+
if (fits(shortened)) return shortened;
|
|
508
|
+
}
|
|
509
|
+
return scientificToWidth(n, fits) ?? shortened;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* `value` in scientific notation with as many mantissa decimals as `fits`
|
|
513
|
+
* allows, spelled Excel's way: an upper-case `E`, a signed exponent of at least
|
|
514
|
+
* two digits. Undefined when even a bare `1E+09` will not fit — the cell then
|
|
515
|
+
* says so with hashes rather than shrinking further.
|
|
516
|
+
*/
|
|
517
|
+
function scientificToWidth(value, fits) {
|
|
518
|
+
for (let decimals = MAX_SCIENTIFIC_DECIMALS; decimals >= 0; decimals--) {
|
|
519
|
+
const [mantissa, exp] = value.toExponential(decimals).split("e");
|
|
520
|
+
const out = `${mantissa}E${exp.startsWith("-") ? "-" : "+"}${exp.replace(/^[-+]/, "").padStart(2, "0")}`;
|
|
521
|
+
if (fits(out)) return out;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** Excel never shows more mantissa decimals than this under General. */
|
|
525
|
+
var MAX_SCIENTIFIC_DECIMALS = 10;
|
|
526
|
+
function applyFormatString(rawValue, format) {
|
|
527
|
+
const n = Number(rawValue);
|
|
528
|
+
const sections = splitSections(format);
|
|
529
|
+
if (!Number.isFinite(n)) {
|
|
530
|
+
const textSection = sections[3];
|
|
531
|
+
if (textSection !== void 0) return textSection.replace(/@/g, rawValue);
|
|
532
|
+
return rawValue;
|
|
533
|
+
}
|
|
534
|
+
const sectionIdx = sectionIndexFor(n, sections);
|
|
535
|
+
if (sectionIdx < 0) return defaultNumberRender(rawValue);
|
|
536
|
+
return applyNumericSection(n, sections[sectionIdx] ?? sections[0], sectionIdx === 1 && conditionOf(sections[0] ?? "") === void 0);
|
|
537
|
+
}
|
|
538
|
+
function splitSections(format) {
|
|
539
|
+
const out = [];
|
|
540
|
+
let current = "";
|
|
541
|
+
let inQuotes = false;
|
|
542
|
+
let bracketDepth = 0;
|
|
543
|
+
for (let i = 0; i < format.length; i++) {
|
|
544
|
+
const ch = format[i];
|
|
545
|
+
if (ch === "\"") {
|
|
546
|
+
inQuotes = !inQuotes;
|
|
547
|
+
current += ch;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (!inQuotes) {
|
|
551
|
+
if (ch === "[") bracketDepth++;
|
|
552
|
+
else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
553
|
+
else if (ch === "\\" && i + 1 < format.length) {
|
|
554
|
+
current += ch + format[i + 1];
|
|
555
|
+
i++;
|
|
556
|
+
continue;
|
|
557
|
+
} else if (ch === ";" && bracketDepth === 0) {
|
|
558
|
+
out.push(current);
|
|
559
|
+
current = "";
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
current += ch;
|
|
564
|
+
}
|
|
565
|
+
out.push(current);
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
function countDigitPlaceholders(s) {
|
|
569
|
+
let n = 0;
|
|
570
|
+
for (const c of s) if (c === "0" || c === "#" || c === "?") n++;
|
|
571
|
+
return n;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* `value` rounded to `decimals` places the way a spreadsheet rounds: on the
|
|
575
|
+
* DECIMAL number, half away from zero.
|
|
576
|
+
*
|
|
577
|
+
* `toFixed` rounds the binary double, which is not the number the file means.
|
|
578
|
+
* A rate stored as 0.0095 is 0.009499999999999999… in binary; times 100 that is
|
|
579
|
+
* 0.9499999999999998, and `toFixed(1)` dutifully answers "0.9" where Excel and
|
|
580
|
+
* every other reader show 1.0%. AverageTaxRates.xlsx had eleven of its
|
|
581
|
+
* percentages a tenth low for exactly this reason.
|
|
582
|
+
*
|
|
583
|
+
* Rounding to 15 significant digits first collapses the binary noise back to
|
|
584
|
+
* the decimal the author typed; shifting through a string exponent then keeps
|
|
585
|
+
* the scaling exact, so the final round sees 9.5 and not 9.499999999999998.
|
|
586
|
+
*/
|
|
587
|
+
function toFixedDecimal(value, decimals) {
|
|
588
|
+
if (!Number.isFinite(value)) return value.toFixed(decimals);
|
|
589
|
+
const decimal = Number(value.toPrecision(15));
|
|
590
|
+
const shifted = Number(`${decimal}e${decimals}`);
|
|
591
|
+
if (!Number.isFinite(shifted)) return value.toFixed(decimals);
|
|
592
|
+
const rounded = shifted < 0 ? -Math.round(-shifted) : Math.round(shifted);
|
|
593
|
+
const back = Number(`${rounded}e${-decimals}`);
|
|
594
|
+
return Number.isFinite(back) ? back.toFixed(decimals) : value.toFixed(decimals);
|
|
595
|
+
}
|
|
596
|
+
function formatScientific(value, cleaned, negativeSection) {
|
|
597
|
+
const m = /^(.*?)([eE])([+-])(.*)$/.exec(cleaned);
|
|
598
|
+
if (!m) return cleaned;
|
|
599
|
+
const mantissaFmt = m[1];
|
|
600
|
+
const eChar = m[2];
|
|
601
|
+
const expSignFmt = m[3];
|
|
602
|
+
const expFmt = m[4];
|
|
603
|
+
const dot = mantissaFmt.indexOf(".");
|
|
604
|
+
const intDigits = Math.max(1, countDigitPlaceholders(dot >= 0 ? mantissaFmt.slice(0, dot) : mantissaFmt));
|
|
605
|
+
const decimals = dot >= 0 ? countDigitPlaceholders(mantissaFmt.slice(dot + 1)) : 0;
|
|
606
|
+
let exp = 0;
|
|
607
|
+
let mant = Math.abs(value);
|
|
608
|
+
if (mant !== 0) {
|
|
609
|
+
exp = Math.floor(Math.log10(mant));
|
|
610
|
+
exp -= (exp % intDigits + intDigits) % intDigits;
|
|
611
|
+
mant = mant / Math.pow(10, exp);
|
|
612
|
+
if (Number(mant.toFixed(decimals)) >= Math.pow(10, intDigits)) {
|
|
613
|
+
exp += intDigits;
|
|
614
|
+
mant = mant / Math.pow(10, intDigits);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const mantStr = mant.toFixed(decimals);
|
|
618
|
+
const expDigits = countDigitPlaceholders(expFmt) || 2;
|
|
619
|
+
const expStr = String(Math.abs(exp)).padStart(expDigits, "0");
|
|
620
|
+
return `${value < 0 && !negativeSection ? "-" : ""}${mantStr}${eChar}${exp < 0 ? "-" : expSignFmt === "+" ? "+" : ""}${expStr}`;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Resolve the `[...]` codes a numeric section may carry (§18.8.31).
|
|
624
|
+
*
|
|
625
|
+
* `[$SYMBOL-LOCALE]` is a currency tag and its SYMBOL is *rendered* — dropping
|
|
626
|
+
* the whole bracket, as a colour or a locale is dropped, silently loses the `$`
|
|
627
|
+
* from `[$$-409]#,##0`. The symbol becomes a quoted literal so the placeholder
|
|
628
|
+
* grammar treats it as text. Everything else in brackets (colours, locales,
|
|
629
|
+
* conditions) is decoration here and goes.
|
|
630
|
+
*/
|
|
631
|
+
function resolveBracketCodes(format) {
|
|
632
|
+
return format.replace(/\[([^\]]*)\]/g, (_, body) => {
|
|
633
|
+
const symbol = /^\$([^-]*)(?:-.*)?$/.exec(body)?.[1]?.replace(/"/g, "") ?? "";
|
|
634
|
+
return symbol.length > 0 ? `"${symbol}"` : "";
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* §18.8.31 fraction formats — `# ?/?`, `# ??/??`, `?/?`, `# ?/16`.
|
|
639
|
+
*
|
|
640
|
+
* `?` is a digit placeholder like `#` but padded to width with spaces, and the
|
|
641
|
+
* count of them on the right of the slash bounds the denominator: `??` admits
|
|
642
|
+
* any denominator up to 99, so 25.378 renders as "25 31/82". A literal number
|
|
643
|
+
* there instead (`?/16`) fixes the denominator. Returns undefined when the
|
|
644
|
+
* section is not a fraction, leaving the ordinary grammar to run.
|
|
645
|
+
*/
|
|
646
|
+
function formatFraction(value, cleaned, negativeSection) {
|
|
647
|
+
const m = /^(.*?)([0#?]+)\s*\/\s*([0#?]+|\d+)(.*)$/.exec(cleaned);
|
|
648
|
+
if (!m) return void 0;
|
|
649
|
+
const head = m[1];
|
|
650
|
+
const denFmt = m[3];
|
|
651
|
+
const tail = m[4];
|
|
652
|
+
const intMatch = /([0#?]+)([^0#?]*)$/.exec(head);
|
|
653
|
+
const hasInteger = intMatch !== null;
|
|
654
|
+
const literalPrefix = unquoteLiteral(head.substring(0, intMatch ? intMatch.index : head.length));
|
|
655
|
+
const separator = intMatch ? unquoteLiteral(intMatch[2]) : " ";
|
|
656
|
+
const magnitude = Math.abs(value);
|
|
657
|
+
const whole = hasInteger ? Math.floor(magnitude) : 0;
|
|
658
|
+
const rest = magnitude - whole;
|
|
659
|
+
const fixedDen = /^\d+$/.test(denFmt) ? Number(denFmt) : void 0;
|
|
660
|
+
const maxDen = fixedDen ?? Math.pow(10, denFmt.length) - 1;
|
|
661
|
+
if (!Number.isFinite(maxDen) || maxDen < 1) return void 0;
|
|
662
|
+
let num;
|
|
663
|
+
let den;
|
|
664
|
+
if (fixedDen !== void 0) {
|
|
665
|
+
den = fixedDen;
|
|
666
|
+
num = Math.round(rest * fixedDen);
|
|
667
|
+
} else [num, den] = bestRational(rest, maxDen);
|
|
668
|
+
const pieces = [];
|
|
669
|
+
if (hasInteger) {
|
|
670
|
+
let integer = whole;
|
|
671
|
+
if (den > 0 && num >= den) {
|
|
672
|
+
integer += Math.floor(num / den);
|
|
673
|
+
num %= den;
|
|
674
|
+
}
|
|
675
|
+
if (integer !== 0 || num === 0) pieces.push(String(integer));
|
|
676
|
+
}
|
|
677
|
+
if (num !== 0 || !hasInteger) pieces.push(`${num}/${den}`);
|
|
678
|
+
const sign = value < 0 && !negativeSection ? "-" : "";
|
|
679
|
+
const gap = separator.length > 0 ? separator : " ";
|
|
680
|
+
return `${literalPrefix}${sign}${pieces.join(gap)}${unquoteLiteral(tail)}`;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The closest `n/d` to `value` with `d <= maxDen`, by continued fractions —
|
|
684
|
+
* the same approximation Excel and LibreOffice show for a `?/?` format.
|
|
685
|
+
*/
|
|
686
|
+
function bestRational(value, maxDen) {
|
|
687
|
+
if (value === 0) return [0, 1];
|
|
688
|
+
let [prevN, prevD] = [0, 1];
|
|
689
|
+
let [n, d] = [1, 0];
|
|
690
|
+
let x = value;
|
|
691
|
+
for (let i = 0; i < 64; i++) {
|
|
692
|
+
const a = Math.floor(x);
|
|
693
|
+
const nextN = a * n + prevN;
|
|
694
|
+
const nextD = a * d + prevD;
|
|
695
|
+
if (nextD > maxDen) break;
|
|
696
|
+
[prevN, prevD] = [n, d];
|
|
697
|
+
[n, d] = [nextN, nextD];
|
|
698
|
+
const frac = x - a;
|
|
699
|
+
if (frac < 1e-12) break;
|
|
700
|
+
x = 1 / frac;
|
|
701
|
+
}
|
|
702
|
+
if (d === 0) return [Math.round(value * maxDen), maxDen];
|
|
703
|
+
return [n, d];
|
|
704
|
+
}
|
|
705
|
+
function applyNumericSection(value, format, negativeSection) {
|
|
706
|
+
const cleaned = resolveBracketCodes(format);
|
|
707
|
+
if (cleaned.trim().toLowerCase() === "general") return defaultNumberRender(String(negativeSection ? Math.abs(value) : value));
|
|
708
|
+
const fraction = formatFraction(value, cleaned, negativeSection);
|
|
709
|
+
if (fraction !== void 0) return fraction;
|
|
710
|
+
if (/[eE][+-]/.test(cleaned)) return formatScientific(value, cleaned, negativeSection);
|
|
711
|
+
const isPercent = cleaned.includes("%");
|
|
712
|
+
let magnitude = Math.abs(value);
|
|
713
|
+
if (isPercent) magnitude *= 100;
|
|
714
|
+
const { intFormat, decFormat, literalPrefix, literalSuffix, scaleCommas } = splitNumberFormat(cleaned);
|
|
715
|
+
if (scaleCommas > 0) magnitude /= 1e3 ** scaleCommas;
|
|
716
|
+
let decimals = 0;
|
|
717
|
+
for (const c of decFormat) if (c === "0" || c === "#") decimals++;
|
|
718
|
+
const [intRaw, decRaw] = toFixedDecimal(magnitude, decimals).split(".");
|
|
719
|
+
const intStr = /,(?=\d)/.test(intFormat) || /[0#],[0#]/.test(intFormat) ? intRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : intRaw;
|
|
720
|
+
let numberPart = intRaw === "0" && !/[0?]/.test(intFormat) ? "" : intStr;
|
|
721
|
+
if (decimals > 0) {
|
|
722
|
+
let dec = decRaw ?? "".padEnd(decimals, "0");
|
|
723
|
+
if (dec.length < decimals) dec = dec.padEnd(decimals, "0");
|
|
724
|
+
let kept = "";
|
|
725
|
+
for (let i = 0; i < decFormat.length; i++) {
|
|
726
|
+
const placeholder = decFormat[i];
|
|
727
|
+
if (placeholder === "0" || placeholder === "#") kept += dec[i] ?? "0";
|
|
728
|
+
}
|
|
729
|
+
for (let i = kept.length - 1; i >= 0 && kept[i] === "0" && decFormat[i] === "#"; i--) kept = kept.substring(0, i);
|
|
730
|
+
if (kept.length > 0) numberPart += "." + kept;
|
|
731
|
+
else if (decFormat.includes("0")) numberPart += "." + dec;
|
|
732
|
+
}
|
|
733
|
+
let signPrefix = "";
|
|
734
|
+
if (value < 0 && !negativeSection) signPrefix = "-";
|
|
735
|
+
return `${literalPrefix}${signPrefix}${numberPart}${isPercent ? "%" : ""}${literalSuffix}`;
|
|
736
|
+
}
|
|
737
|
+
function splitNumberFormat(cleaned) {
|
|
738
|
+
let firstDigit = -1;
|
|
739
|
+
let lastDigit = -1;
|
|
740
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
741
|
+
const ch = cleaned[i];
|
|
742
|
+
if (ch === "0" || ch === "#") {
|
|
743
|
+
if (firstDigit < 0) firstDigit = i;
|
|
744
|
+
lastDigit = i;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (firstDigit < 0) return {
|
|
748
|
+
literalPrefix: unquoteLiteral(cleaned),
|
|
749
|
+
intFormat: "",
|
|
750
|
+
decFormat: "",
|
|
751
|
+
literalSuffix: "",
|
|
752
|
+
scaleCommas: 0
|
|
753
|
+
};
|
|
754
|
+
const literalPrefix = unquoteLiteral(cleaned.substring(0, firstDigit));
|
|
755
|
+
const rawSuffix = cleaned.substring(lastDigit + 1);
|
|
756
|
+
const scaleCommas = /^,+/.exec(rawSuffix)?.[0]?.length ?? 0;
|
|
757
|
+
const literalSuffix = unquoteLiteral(rawSuffix.substring(scaleCommas));
|
|
758
|
+
const digitRange = cleaned.substring(firstDigit, lastDigit + 1);
|
|
759
|
+
const dotIdx = digitRange.indexOf(".");
|
|
760
|
+
return {
|
|
761
|
+
literalPrefix,
|
|
762
|
+
intFormat: dotIdx >= 0 ? digitRange.substring(0, dotIdx) : digitRange,
|
|
763
|
+
decFormat: dotIdx >= 0 ? digitRange.substring(dotIdx + 1) : "",
|
|
764
|
+
literalSuffix,
|
|
765
|
+
scaleCommas
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function unquoteLiteral(s) {
|
|
769
|
+
let out = "";
|
|
770
|
+
for (let i = 0; i < s.length; i++) {
|
|
771
|
+
const ch = s[i];
|
|
772
|
+
if (ch === "\"") {
|
|
773
|
+
i++;
|
|
774
|
+
while (i < s.length && s[i] !== "\"") {
|
|
775
|
+
out += s[i];
|
|
776
|
+
i++;
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
781
|
+
out += s[i + 1];
|
|
782
|
+
i++;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (ch === "_" && i + 1 < s.length) {
|
|
786
|
+
out += " ";
|
|
787
|
+
i++;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (ch === "*" && i + 1 < s.length) {
|
|
791
|
+
i++;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (ch === "%") continue;
|
|
795
|
+
out += ch;
|
|
796
|
+
}
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
export { applyNumberFormat, excelSerialFromUtcParts, excelSerialToDate, generalToWidth, numberFormatColorHex };
|