@office-kit/xlsx 0.16.0 → 0.18.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/README.md +89 -4
- package/dist/cell/cell.d.ts +12 -19
- package/dist/{cell-Df3cKRIl.mjs → cell-B5OUUdfI.mjs} +13 -7
- package/dist/cell-B5OUUdfI.mjs.map +1 -0
- package/dist/{cell-style-CNsET6WU.mjs → cell-style-CbB4nXTY.mjs} +11 -4
- package/dist/cell-style-CbB4nXTY.mjs.map +1 -0
- package/dist/cell.mjs +1 -1
- package/dist/{coordinate-Bt5Gni2I.mjs → coordinate-CxC88uBk.mjs} +31 -2
- package/dist/coordinate-CxC88uBk.mjs.map +1 -0
- package/dist/{defined-names-Ctu3F6ls.mjs → defined-names-Cmp8rbvI.mjs} +2 -2
- package/dist/{defined-names-Ctu3F6ls.mjs.map → defined-names-Cmp8rbvI.mjs.map} +1 -1
- package/dist/{differential-BPKa7wXi.mjs → differential-QLaKEiBk.mjs} +2 -2
- package/dist/{differential-BPKa7wXi.mjs.map → differential-QLaKEiBk.mjs.map} +1 -1
- package/dist/{drawing-ZJ3h4VHD.mjs → drawing-Cv6UtInY.mjs} +2 -2
- package/dist/{drawing-ZJ3h4VHD.mjs.map → drawing-Cv6UtInY.mjs.map} +1 -1
- package/dist/drawing.mjs +1 -1
- package/dist/{escape-rnWSPpWY.mjs → escape-rA5UpibT.mjs} +26 -3
- package/dist/escape-rA5UpibT.mjs.map +1 -0
- package/dist/io/load.d.ts +30 -0
- package/dist/io.mjs +2 -2
- package/dist/{iterparse-DZE0zy2M.mjs → iterparse-CZoRXvZW.mjs} +2 -2
- package/dist/{iterparse-DZE0zy2M.mjs.map → iterparse-CZoRXvZW.mjs.map} +1 -1
- package/dist/{load-B1sK-LjT.mjs → load-BpnCPv1S.mjs} +84 -45
- package/dist/load-BpnCPv1S.mjs.map +1 -0
- package/dist/{manifest-pLmx7KYh.mjs → manifest-9p0ccqmR.mjs} +10 -11
- package/dist/manifest-9p0ccqmR.mjs.map +1 -0
- package/dist/node.mjs +1 -1
- package/dist/packaging.mjs +1 -1
- package/dist/{parser-By6RWZVW.mjs → parser-B1tB9YaE.mjs} +15 -5
- package/dist/parser-B1tB9YaE.mjs.map +1 -0
- package/dist/{reader-DHMxLBQV.mjs → reader-pC7bxYAs.mjs} +30 -7
- package/dist/reader-pC7bxYAs.mjs.map +1 -0
- package/dist/{save-DmdWwxmw.mjs → save-1Z3344nz.mjs} +8 -8
- package/dist/{save-DmdWwxmw.mjs.map → save-1Z3344nz.mjs.map} +1 -1
- package/dist/schema.mjs +1 -1
- package/dist/{serialize-BC2Wu3bR.mjs → serialize-BtxgssFK.mjs} +24 -7
- package/dist/serialize-BtxgssFK.mjs.map +1 -0
- package/dist/{serializer-C6nm0mRJ.mjs → serializer-N6XIwsgp.mjs} +3 -3
- package/dist/{serializer-C6nm0mRJ.mjs.map → serializer-N6XIwsgp.mjs.map} +1 -1
- package/dist/streaming.mjs +129 -77
- package/dist/streaming.mjs.map +1 -1
- package/dist/styles/display-text.d.ts +95 -0
- package/dist/styles/format-code.d.ts +125 -0
- package/dist/styles/index.d.ts +1 -0
- package/dist/styles/numbers.d.ts +7 -0
- package/dist/styles.mjs +1161 -4
- package/dist/styles.mjs.map +1 -0
- package/dist/{stylesheet-writer-oXSuGnJy.mjs → stylesheet-writer-BEn-0P0x.mjs} +480 -173
- package/dist/stylesheet-writer-BEn-0P0x.mjs.map +1 -0
- package/dist/{table-BrP0RwUd.mjs → table-BxOLUZs3.mjs} +2 -2
- package/dist/{table-BrP0RwUd.mjs.map → table-BxOLUZs3.mjs.map} +1 -1
- package/dist/{tree-BX-sRRVp.mjs → tree-CspNjuYl.mjs} +12 -2
- package/dist/tree-CspNjuYl.mjs.map +1 -0
- package/dist/utils/cell-number.d.ts +10 -0
- package/dist/utils/coordinate.d.ts +11 -0
- package/dist/utils/escape.d.ts +17 -0
- package/dist/utils/xsd-boolean.d.ts +9 -0
- package/dist/utils.mjs +2 -2
- package/dist/workbook/index.d.ts +1 -1
- package/dist/workbook/workbook.d.ts +9 -46
- package/dist/{workbook-DOTFzCpz.mjs → workbook-tG9oYtzU.mjs} +50 -13
- package/dist/workbook-tG9oYtzU.mjs.map +1 -0
- package/dist/workbook.mjs +3 -3
- package/dist/worksheet/index.d.ts +1 -1
- package/dist/worksheet/worksheet.d.ts +55 -14
- package/dist/{worksheet-B2NOQ6dM.mjs → worksheet-DLMyDD7W.mjs} +87 -22
- package/dist/worksheet-DLMyDD7W.mjs.map +1 -0
- package/dist/worksheet.mjs +3 -3
- package/dist/xml/namespaces.d.ts +7 -0
- package/dist/xml/parser.d.ts +12 -0
- package/dist/xml/strict-package.d.ts +14 -0
- package/dist/xml.mjs +5 -5
- package/dist/zip.mjs +1 -1
- package/package.json +1 -1
- package/dist/cell-Df3cKRIl.mjs.map +0 -1
- package/dist/cell-style-CNsET6WU.mjs.map +0 -1
- package/dist/coordinate-Bt5Gni2I.mjs.map +0 -1
- package/dist/escape-rnWSPpWY.mjs.map +0 -1
- package/dist/load-B1sK-LjT.mjs.map +0 -1
- package/dist/manifest-pLmx7KYh.mjs.map +0 -1
- package/dist/parser-By6RWZVW.mjs.map +0 -1
- package/dist/reader-DHMxLBQV.mjs.map +0 -1
- package/dist/serialize-BC2Wu3bR.mjs.map +0 -1
- package/dist/stylesheet-writer-oXSuGnJy.mjs.map +0 -1
- package/dist/tree-BX-sRRVp.mjs.map +0 -1
- package/dist/workbook-DOTFzCpz.mjs.map +0 -1
- package/dist/worksheet-B2NOQ6dM.mjs.map +0 -1
package/dist/styles.mjs
CHANGED
|
@@ -1,4 +1,1161 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { r as
|
|
4
|
-
|
|
1
|
+
import { N as richTextToString, h as isFormulaValue, o as cellValueAsString, p as isErrorValue, u as isDurationValue, v as isRichTextValue } from "./cell-B5OUUdfI.mjs";
|
|
2
|
+
import { An as mixColors, Cn as contrastRatio, Dn as lighten, En as hslToHex, Fn as rotateHue, Mn as pickReadableTextColor, Nn as resolveIndexedColor, On as luminance, Pn as rgbColor, Sn as colorToHex, Tn as hexToHsl, bn as adjustLightness, jn as normaliseRgb, kn as makeColor, wn as darken, xn as adjustSaturation } from "./worksheet-DLMyDD7W.mjs";
|
|
3
|
+
import { $ as BUILTIN_NAMED_STYLES, $t as alignmentToCss, A as setCellBorder, At as classifyDateFormat, B as setItalic, Bt as makeFont, C as setBold, Ct as FORMAT_NUMBER, D as setCellAsNumber, Dt as FORMAT_TEXT, E as setCellAsDate, Et as FORMAT_PERCENTAGE_00, F as setCellProtection, Ft as DEFAULT_FONT, G as setRangeNumberFormat, Gt as makeGradientStop, H as setRangeBackgroundColor, Ht as fillToCss, I as setCellStyle, J as setRangeWrapText, Jt as borderToCss, K as setRangeProtection, Kt as makePatternFill, L as setFontColor, M as setCellFill, Mt as isDateFormat, N as setCellFont, Nt as isTimedeltaFormat, O as setCellAsPercent, Ot as builtinFormatCode, P as setCellNumberFormat, Pt as makeNumberFormat, Q as makeProtection, R as setFontName, S as rotateCellText, St as FORMAT_GENERAL, T as setCellAsCurrency, Tt as FORMAT_PERCENTAGE, U as setRangeBorderBox, Ut as makeFill, V as setRangeAlignment, W as setRangeFont, Wt as makeGradientFill, X as setUnderline, Xt as makeSide, Y as setStrikethrough, Yt as makeBorder, Z as wrapCellText, _ as getCellNumberFormat, _t as BUILTIN_FORMATS_MAX_SIZE, a as cellStyleToCss, at as addFill, b as patchCellFont, bt as FORMAT_DATE_TIMEDELTA, c as clearCellStyle, d as copyCellStyle, dt as listCellStyleXfs, en as makeAlignment, et as addNamedStyle, f as formatAsHeader, ft as listCellXfs, g as getCellFont, gt as BUILTIN_FORMATS, h as getCellFill, ht as makeStylesheet, i as applyNamedStyle, it as addCellXf, j as setCellBorderAll, jt as isBuiltinFormat, k as setCellBackgroundColor, kt as builtinFormatId, l as clearRangeStyle, lt as defaultCellXf, m as getCellBorder, mt as listFonts, n as alignCellVertical, nt as addBorder, o as centerCell, ot as addFont, p as getCellAlignment, pt as listFills, q as setRangeStyle, r as applyBuiltinStyle, rt as addCellStyleXf, s as clearCellBackground, st as addNumFmt, t as alignCellHorizontal, tt as ensureBuiltinStyle, u as cloneCellStyle, ut as listBorders, v as getCellProtection, vt as FORMAT_COLOR_NAMES, w as setCellAlignment, wt as FORMAT_NUMBER_00, x as registerCellStyle, xt as FORMAT_DATE_YYYYMMDD2, y as indentCell, yt as FORMAT_DATE_DATETIME, z as setFontSize, zt as fontToCss } from "./cell-style-CbB4nXTY.mjs";
|
|
4
|
+
import { a as excelToDate, i as durationToExcel, r as dateToExcel } from "./datetime-B2ySVlXt.mjs";
|
|
5
|
+
import { r as makeDifferentialStyle, t as addDxf } from "./differential-QLaKEiBk.mjs";
|
|
6
|
+
//#region src/styles/format-code.ts
|
|
7
|
+
/** Excel carries 15 significant decimal digits and never shows more. */
|
|
8
|
+
const EXCEL_SIGNIFICANT_DIGITS = 15;
|
|
9
|
+
/** Positive, negative, zero, text. */
|
|
10
|
+
const MAX_SECTIONS = 4;
|
|
11
|
+
const GROUP_SEPARATOR = ",";
|
|
12
|
+
const GROUP_SIZE = 3;
|
|
13
|
+
const SCALE_DIVISOR = 1e3;
|
|
14
|
+
const PERCENT_MULTIPLIER = 100;
|
|
15
|
+
/** `mmm` and wider render a month or weekday name, never a minute. */
|
|
16
|
+
const NAME_TOKEN_WIDTH = 3;
|
|
17
|
+
const MONTH_FULL_WIDTH = 4;
|
|
18
|
+
/** Excel pads a `General` exponent to two digits, so 1e-7 shows as `1E-07`. */
|
|
19
|
+
const GENERAL_EXPONENT_DIGITS = 2;
|
|
20
|
+
const FULL_YEAR_DIGITS = 4;
|
|
21
|
+
/** Excel stores time to the millisecond, so `.0000` gains nothing over `.000`. */
|
|
22
|
+
const MAX_SUBSECOND_DIGITS = 3;
|
|
23
|
+
const SECONDS_PER_DAY = 86400;
|
|
24
|
+
const SECONDS_PER_HOUR = 3600;
|
|
25
|
+
const SECONDS_PER_MINUTE = 60;
|
|
26
|
+
const MINUTES_PER_HOUR = 60;
|
|
27
|
+
const HOURS_PER_DAY = 24;
|
|
28
|
+
const HOURS_PER_HALF_DAY = 12;
|
|
29
|
+
const ZERO_CHAR_CODE = "0".charCodeAt(0);
|
|
30
|
+
const FIVE_CHAR_CODE = "5".charCodeAt(0);
|
|
31
|
+
const NINE_CHAR_CODE = "9".charCodeAt(0);
|
|
32
|
+
/** Terms and closeness at which the fraction search stops chasing a double. */
|
|
33
|
+
const FRACTION_MAX_TERMS = 64;
|
|
34
|
+
const FRACTION_EPSILON = 1e-12;
|
|
35
|
+
const MONTH_NAMES = [
|
|
36
|
+
"January",
|
|
37
|
+
"February",
|
|
38
|
+
"March",
|
|
39
|
+
"April",
|
|
40
|
+
"May",
|
|
41
|
+
"June",
|
|
42
|
+
"July",
|
|
43
|
+
"August",
|
|
44
|
+
"September",
|
|
45
|
+
"October",
|
|
46
|
+
"November",
|
|
47
|
+
"December"
|
|
48
|
+
];
|
|
49
|
+
const WEEKDAY_NAMES = [
|
|
50
|
+
"Sunday",
|
|
51
|
+
"Monday",
|
|
52
|
+
"Tuesday",
|
|
53
|
+
"Wednesday",
|
|
54
|
+
"Thursday",
|
|
55
|
+
"Friday",
|
|
56
|
+
"Saturday"
|
|
57
|
+
];
|
|
58
|
+
const COLOR_NAMES = new Set(FORMAT_COLOR_NAMES.toLowerCase().split("|"));
|
|
59
|
+
const INDEXED_COLOR_RE = /^color\s*\d+$/i;
|
|
60
|
+
/**
|
|
61
|
+
* The body of an elapsed-time bracket: one letter repeated, as in `[h]` or
|
|
62
|
+
* `[mm]`. `isTimedeltaFormat` in `./numbers.js` answers the different question
|
|
63
|
+
* of whether a whole code is a duration, so it cannot stand in here.
|
|
64
|
+
*/
|
|
65
|
+
const ELAPSED_RE = /^([hms])\1*$/i;
|
|
66
|
+
/**
|
|
67
|
+
* Plain (never exponential) decimal digits of a finite non-negative number at
|
|
68
|
+
* Excel's precision. Rounding has to go through this string: `(1.005).toFixed(2)`
|
|
69
|
+
* is `"1.00"` because the double nearest 1.005 sits just below it, while Excel
|
|
70
|
+
* rounds the 15-digit decimal it shows and prints `1.01`.
|
|
71
|
+
*/
|
|
72
|
+
const toPlainDecimal = (value) => {
|
|
73
|
+
const text = value.toPrecision(EXCEL_SIGNIFICANT_DIGITS);
|
|
74
|
+
const parsed = /^(\d+)(?:\.(\d+))?e([+-]\d+)$/.exec(text);
|
|
75
|
+
if (parsed === null) return text;
|
|
76
|
+
const intPart = parsed[1] ?? "";
|
|
77
|
+
const digits = intPart + (parsed[2] ?? "");
|
|
78
|
+
const point = intPart.length + Number(parsed[3] ?? "0");
|
|
79
|
+
if (point <= 0) return `0.${"0".repeat(-point)}${digits}`;
|
|
80
|
+
if (point >= digits.length) return digits + "0".repeat(point - digits.length);
|
|
81
|
+
return `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
82
|
+
};
|
|
83
|
+
/** Add one to the last digit of a digit string, growing it on overflow. */
|
|
84
|
+
const carryOne = (digits) => {
|
|
85
|
+
let i = digits.length - 1;
|
|
86
|
+
let tail = "";
|
|
87
|
+
while (i >= 0 && digits.charCodeAt(i) === NINE_CHAR_CODE) {
|
|
88
|
+
tail = `0${tail}`;
|
|
89
|
+
i--;
|
|
90
|
+
}
|
|
91
|
+
if (i < 0) return `1${tail}`;
|
|
92
|
+
return digits.slice(0, i) + String(digits.charCodeAt(i) - ZERO_CHAR_CODE + 1) + tail;
|
|
93
|
+
};
|
|
94
|
+
/** Round `int`.`frac` to `decimals` places, half away from zero. */
|
|
95
|
+
const roundParts = (int, frac, decimals) => {
|
|
96
|
+
if (frac.length <= decimals) return {
|
|
97
|
+
int,
|
|
98
|
+
frac: frac.padEnd(decimals, "0")
|
|
99
|
+
};
|
|
100
|
+
const kept = frac.slice(0, decimals);
|
|
101
|
+
if (frac.charCodeAt(decimals) < FIVE_CHAR_CODE) return {
|
|
102
|
+
int,
|
|
103
|
+
frac: kept
|
|
104
|
+
};
|
|
105
|
+
const bumped = carryOne(int + kept);
|
|
106
|
+
return {
|
|
107
|
+
int: bumped.slice(0, bumped.length - decimals),
|
|
108
|
+
frac: bumped.slice(bumped.length - decimals)
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const roundDecimal = (value, decimals) => {
|
|
112
|
+
const text = toPlainDecimal(value);
|
|
113
|
+
const dot = text.indexOf(".");
|
|
114
|
+
if (dot === -1) return roundParts(text, "", decimals);
|
|
115
|
+
return roundParts(text.slice(0, dot), text.slice(dot + 1), decimals);
|
|
116
|
+
};
|
|
117
|
+
/** Significant digits of a non-negative number, most significant first. */
|
|
118
|
+
const significantDigits = (value) => toPlainDecimal(value).replace(".", "").replace(/^0+/, "");
|
|
119
|
+
/** Power of ten of the leading significant digit; 0 for a zero value. */
|
|
120
|
+
const decimalOrder = (value) => {
|
|
121
|
+
const text = toPlainDecimal(value);
|
|
122
|
+
const dot = text.indexOf(".");
|
|
123
|
+
const int = (dot === -1 ? text : text.slice(0, dot)).replace(/^0+/, "");
|
|
124
|
+
if (int.length > 0) return int.length - 1;
|
|
125
|
+
const leading = (dot === -1 ? "" : text.slice(dot + 1)).search(/[1-9]/);
|
|
126
|
+
return leading === -1 ? 0 : -(leading + 1);
|
|
127
|
+
};
|
|
128
|
+
const pushLiteral = (tokens, text) => {
|
|
129
|
+
const last = tokens[tokens.length - 1];
|
|
130
|
+
if (last?.kind === "literal") tokens[tokens.length - 1] = {
|
|
131
|
+
kind: "literal",
|
|
132
|
+
text: last.text + text
|
|
133
|
+
};
|
|
134
|
+
else tokens.push({
|
|
135
|
+
kind: "literal",
|
|
136
|
+
text
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
const datePartOf = (letter) => {
|
|
140
|
+
if (letter === "y") return "year";
|
|
141
|
+
if (letter === "d") return "day";
|
|
142
|
+
if (letter === "h") return "hour";
|
|
143
|
+
if (letter === "s") return "second";
|
|
144
|
+
return "monthOrMinute";
|
|
145
|
+
};
|
|
146
|
+
/** `[Red]`, `[$€-407]`, `[hh]`, `[>100]`: everything Excel puts in brackets. */
|
|
147
|
+
const tokenizeBracket = (body, tokens) => {
|
|
148
|
+
const elapsed = ELAPSED_RE.exec(body);
|
|
149
|
+
if (elapsed !== null) {
|
|
150
|
+
const letter = (elapsed[1] ?? "h").toLowerCase();
|
|
151
|
+
const part = letter === "h" ? "hour" : letter === "m" ? "minute" : "second";
|
|
152
|
+
tokens.push({
|
|
153
|
+
kind: "elapsed",
|
|
154
|
+
part,
|
|
155
|
+
width: body.length
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (body.startsWith("$")) {
|
|
160
|
+
const symbol = body.slice(1).split("-")[0] ?? "";
|
|
161
|
+
if (symbol.length > 0) pushLiteral(tokens, symbol);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
const lower = body.toLowerCase();
|
|
165
|
+
if (COLOR_NAMES.has(lower) || INDEXED_COLOR_RE.test(body)) return true;
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
const MERIDIEM_LONG = "AM/PM";
|
|
169
|
+
const MERIDIEM_SHORT = "A/P";
|
|
170
|
+
const meridiemToken = (raw) => {
|
|
171
|
+
const slash = raw.indexOf("/");
|
|
172
|
+
return {
|
|
173
|
+
kind: "meridiem",
|
|
174
|
+
am: raw.slice(0, slash),
|
|
175
|
+
pm: raw.slice(slash + 1)
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
const tokenizeSection = (src) => {
|
|
179
|
+
const tokens = [];
|
|
180
|
+
let i = 0;
|
|
181
|
+
while (i < src.length) {
|
|
182
|
+
const ch = src.charAt(i);
|
|
183
|
+
if (ch === "\"") {
|
|
184
|
+
const end = src.indexOf("\"", i + 1);
|
|
185
|
+
if (end === -1) return void 0;
|
|
186
|
+
pushLiteral(tokens, src.slice(i + 1, end));
|
|
187
|
+
i = end + 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === "\\") {
|
|
191
|
+
pushLiteral(tokens, src.charAt(i + 1));
|
|
192
|
+
i += 2;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === "_") {
|
|
196
|
+
pushLiteral(tokens, " ");
|
|
197
|
+
i += 2;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ch === "*") {
|
|
201
|
+
i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === "[") {
|
|
205
|
+
const end = src.indexOf("]", i + 1);
|
|
206
|
+
if (end === -1) return void 0;
|
|
207
|
+
if (!tokenizeBracket(src.slice(i + 1, end), tokens)) return void 0;
|
|
208
|
+
i = end + 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (ch === "0" || ch === "#" || ch === "?") {
|
|
212
|
+
tokens.push({
|
|
213
|
+
kind: "digit",
|
|
214
|
+
placeholder: ch
|
|
215
|
+
});
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (ch === ".") {
|
|
220
|
+
tokens.push({ kind: "point" });
|
|
221
|
+
i++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ch === ",") {
|
|
225
|
+
tokens.push({ kind: "comma" });
|
|
226
|
+
i++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (ch === "%") {
|
|
230
|
+
tokens.push({ kind: "percent" });
|
|
231
|
+
i++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (ch === "/") {
|
|
235
|
+
tokens.push({ kind: "slash" });
|
|
236
|
+
i++;
|
|
237
|
+
const fixed = /^[1-9][0-9]*/.exec(src.slice(i));
|
|
238
|
+
if (fixed !== null) {
|
|
239
|
+
tokens.push({
|
|
240
|
+
kind: "fixedDenominator",
|
|
241
|
+
text: fixed[0]
|
|
242
|
+
});
|
|
243
|
+
i += fixed[0].length;
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (ch === "@") {
|
|
248
|
+
tokens.push({ kind: "textPlaceholder" });
|
|
249
|
+
i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const long = src.slice(i, i + 5);
|
|
253
|
+
if (long.toUpperCase() === MERIDIEM_LONG) {
|
|
254
|
+
tokens.push(meridiemToken(long));
|
|
255
|
+
i += 5;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const short = src.slice(i, i + 3);
|
|
259
|
+
if (short.toUpperCase() === MERIDIEM_SHORT) {
|
|
260
|
+
tokens.push(meridiemToken(short));
|
|
261
|
+
i += 3;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (src.slice(i, i + 7).toLowerCase() === "general") {
|
|
265
|
+
tokens.push({ kind: "general" });
|
|
266
|
+
i += 7;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const lower = ch.toLowerCase();
|
|
270
|
+
if (lower === "e") {
|
|
271
|
+
const sign = src.charAt(i + 1);
|
|
272
|
+
if (sign !== "+" && sign !== "-") return void 0;
|
|
273
|
+
tokens.push({
|
|
274
|
+
kind: "exponent",
|
|
275
|
+
explicitSign: sign === "+"
|
|
276
|
+
});
|
|
277
|
+
i += 2;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (lower === "y" || lower === "m" || lower === "d" || lower === "h" || lower === "s") {
|
|
281
|
+
let width = 1;
|
|
282
|
+
while (src.charAt(i + width).toLowerCase() === lower) width++;
|
|
283
|
+
tokens.push({
|
|
284
|
+
kind: "datePart",
|
|
285
|
+
part: datePartOf(lower),
|
|
286
|
+
width
|
|
287
|
+
});
|
|
288
|
+
i += width;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (lower === "g" || lower === "b") return void 0;
|
|
292
|
+
pushLiteral(tokens, ch);
|
|
293
|
+
i++;
|
|
294
|
+
}
|
|
295
|
+
return tokens;
|
|
296
|
+
};
|
|
297
|
+
const splitSections = (code) => {
|
|
298
|
+
const out = [];
|
|
299
|
+
let start = 0;
|
|
300
|
+
let i = 0;
|
|
301
|
+
while (i < code.length) {
|
|
302
|
+
const ch = code.charAt(i);
|
|
303
|
+
if (ch === "\"") {
|
|
304
|
+
const end = code.indexOf("\"", i + 1);
|
|
305
|
+
if (end === -1) return void 0;
|
|
306
|
+
i = end + 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (ch === "[") {
|
|
310
|
+
const end = code.indexOf("]", i + 1);
|
|
311
|
+
if (end === -1) return void 0;
|
|
312
|
+
i = end + 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (ch === "\\" || ch === "_" || ch === "*") {
|
|
316
|
+
i += 2;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (ch === ";") {
|
|
320
|
+
out.push(code.slice(start, i));
|
|
321
|
+
start = i + 1;
|
|
322
|
+
}
|
|
323
|
+
i++;
|
|
324
|
+
}
|
|
325
|
+
out.push(code.slice(start));
|
|
326
|
+
return out;
|
|
327
|
+
};
|
|
328
|
+
const literalFor = (token) => {
|
|
329
|
+
switch (token.kind) {
|
|
330
|
+
case "literal":
|
|
331
|
+
case "fixedDenominator": return token.text;
|
|
332
|
+
case "percent": return "%";
|
|
333
|
+
case "slash": return "/";
|
|
334
|
+
case "point": return ".";
|
|
335
|
+
case "comma": return GROUP_SEPARATOR;
|
|
336
|
+
default: return "";
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const onlyCommasBetween = (tokens, from, to) => {
|
|
340
|
+
for (let i = from + 1; i < to; i++) if (tokens[i]?.kind !== "comma") return false;
|
|
341
|
+
return true;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* A comma means three different things depending on where it sits: thousands
|
|
345
|
+
* grouping between digit placeholders, division by a thousand after the last
|
|
346
|
+
* one, and a plain comma anywhere else.
|
|
347
|
+
*/
|
|
348
|
+
const resolveCommas = (tokens) => {
|
|
349
|
+
const isDigit = tokens.map((t) => t.kind === "digit");
|
|
350
|
+
const firstDigit = isDigit.indexOf(true);
|
|
351
|
+
const lastDigit = isDigit.lastIndexOf(true);
|
|
352
|
+
const out = [];
|
|
353
|
+
let grouped = false;
|
|
354
|
+
let scale = 0;
|
|
355
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
356
|
+
const token = tokens[i];
|
|
357
|
+
if (token === void 0) continue;
|
|
358
|
+
if (token.kind !== "comma") {
|
|
359
|
+
out.push(token);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (firstDigit !== -1 && i > firstDigit && i < lastDigit) {
|
|
363
|
+
grouped = true;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (lastDigit !== -1 && i > lastDigit && onlyCommasBetween(tokens, lastDigit, i)) {
|
|
367
|
+
scale++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
pushLiteral(out, GROUP_SEPARATOR);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
tokens: out,
|
|
374
|
+
grouped,
|
|
375
|
+
scale
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
const placeholdersIn = (tokens, from, to) => {
|
|
379
|
+
const out = [];
|
|
380
|
+
for (let i = from; i < to; i++) {
|
|
381
|
+
const token = tokens[i];
|
|
382
|
+
if (token?.kind === "digit") out.push(token.placeholder);
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
};
|
|
386
|
+
const resolveNumberSection = (resolved) => {
|
|
387
|
+
const { tokens } = resolved;
|
|
388
|
+
if (tokens.filter((t) => t.kind === "point").length > 1) return void 0;
|
|
389
|
+
if (tokens.filter((t) => t.kind === "exponent").length > 1) return void 0;
|
|
390
|
+
const pointIndex = tokens.findIndex((t) => t.kind === "point");
|
|
391
|
+
const exponentIndex = tokens.findIndex((t) => t.kind === "exponent");
|
|
392
|
+
const intEnd = pointIndex !== -1 ? pointIndex : exponentIndex !== -1 ? exponentIndex : tokens.length;
|
|
393
|
+
const fracEnd = exponentIndex !== -1 ? exponentIndex : tokens.length;
|
|
394
|
+
const exponentToken = exponentIndex === -1 ? void 0 : tokens[exponentIndex];
|
|
395
|
+
return {
|
|
396
|
+
kind: "number",
|
|
397
|
+
tokens,
|
|
398
|
+
grouped: resolved.grouped,
|
|
399
|
+
scale: resolved.scale,
|
|
400
|
+
percents: tokens.filter((t) => t.kind === "percent").length,
|
|
401
|
+
intEnd,
|
|
402
|
+
intPlaceholders: placeholdersIn(tokens, 0, intEnd),
|
|
403
|
+
fracPlaceholders: pointIndex === -1 ? [] : placeholdersIn(tokens, pointIndex + 1, fracEnd),
|
|
404
|
+
exponentIndex,
|
|
405
|
+
exponentDigits: exponentIndex === -1 ? 0 : placeholdersIn(tokens, exponentIndex + 1, tokens.length).length,
|
|
406
|
+
exponentExplicitSign: exponentToken?.kind === "exponent" ? exponentToken.explicitSign : false
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
const digitRunStart = (tokens, end) => {
|
|
410
|
+
let start = end;
|
|
411
|
+
while (start > 0 && tokens[start - 1]?.kind === "digit") start--;
|
|
412
|
+
return start;
|
|
413
|
+
};
|
|
414
|
+
/** `# ??/??`: a slash with digit placeholders on both sides is a fraction. */
|
|
415
|
+
const resolveFractionSection = (tokens) => {
|
|
416
|
+
const slashIndex = tokens.findIndex((t) => t.kind === "slash");
|
|
417
|
+
if (slashIndex < 1 || tokens[slashIndex - 1]?.kind !== "digit") return void 0;
|
|
418
|
+
const numeratorStart = digitRunStart(tokens, slashIndex);
|
|
419
|
+
const after = tokens[slashIndex + 1];
|
|
420
|
+
let denominatorEnd;
|
|
421
|
+
let fixedDenominator = 0;
|
|
422
|
+
if (after?.kind === "digit") {
|
|
423
|
+
denominatorEnd = slashIndex + 1;
|
|
424
|
+
while (tokens[denominatorEnd]?.kind === "digit") denominatorEnd++;
|
|
425
|
+
} else if (after?.kind === "fixedDenominator") {
|
|
426
|
+
denominatorEnd = slashIndex + 2;
|
|
427
|
+
fixedDenominator = Number(after.text);
|
|
428
|
+
if (!Number.isSafeInteger(fixedDenominator) || fixedDenominator === 0) return void 0;
|
|
429
|
+
} else return;
|
|
430
|
+
let wholeEnd = -1;
|
|
431
|
+
let wholeStart = -1;
|
|
432
|
+
for (let i = numeratorStart - 1; i >= 0; i--) {
|
|
433
|
+
if (tokens[i]?.kind !== "digit") continue;
|
|
434
|
+
wholeEnd = i + 1;
|
|
435
|
+
wholeStart = digitRunStart(tokens, wholeEnd);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
kind: "fraction",
|
|
440
|
+
tokens,
|
|
441
|
+
slashIndex,
|
|
442
|
+
wholeStart,
|
|
443
|
+
wholeEnd,
|
|
444
|
+
numeratorStart,
|
|
445
|
+
denominatorEnd,
|
|
446
|
+
denominatorDigits: fixedDenominator > 0 ? 0 : placeholdersIn(tokens, slashIndex + 1, denominatorEnd).length,
|
|
447
|
+
fixedDenominator
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
const nextDateToken = (tokens, index, step) => {
|
|
451
|
+
for (let i = index + step; i >= 0 && i < tokens.length; i += step) {
|
|
452
|
+
const token = tokens[i];
|
|
453
|
+
if (token === void 0) return void 0;
|
|
454
|
+
if (token.kind === "datePart" || token.kind === "elapsed") return token;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
const resolveDateSection = (tokens) => {
|
|
458
|
+
const withSubseconds = [];
|
|
459
|
+
let subsecondDigits = 0;
|
|
460
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
461
|
+
const token = tokens[i];
|
|
462
|
+
if (token === void 0) continue;
|
|
463
|
+
if (token.kind !== "point") {
|
|
464
|
+
withSubseconds.push(token);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
let digits = 0;
|
|
468
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
469
|
+
const next = tokens[j];
|
|
470
|
+
if (next?.kind !== "digit" || next.placeholder !== "0") break;
|
|
471
|
+
digits++;
|
|
472
|
+
}
|
|
473
|
+
const previous = withSubseconds[withSubseconds.length - 1];
|
|
474
|
+
const afterSeconds = previous?.kind === "datePart" || previous?.kind === "elapsed" ? previous.part === "second" : false;
|
|
475
|
+
if (digits === 0 || !afterSeconds) {
|
|
476
|
+
pushLiteral(withSubseconds, ".");
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
withSubseconds.push({
|
|
480
|
+
kind: "subsecond",
|
|
481
|
+
digits
|
|
482
|
+
});
|
|
483
|
+
subsecondDigits = Math.max(subsecondDigits, digits);
|
|
484
|
+
i += digits;
|
|
485
|
+
}
|
|
486
|
+
const resolved = withSubseconds.map((token, index) => {
|
|
487
|
+
if (token.kind !== "datePart" || token.part !== "monthOrMinute") return token;
|
|
488
|
+
if (token.width >= NAME_TOKEN_WIDTH) return {
|
|
489
|
+
kind: "datePart",
|
|
490
|
+
part: "month",
|
|
491
|
+
width: token.width
|
|
492
|
+
};
|
|
493
|
+
const previous = nextDateToken(withSubseconds, index, -1);
|
|
494
|
+
const next = nextDateToken(withSubseconds, index, 1);
|
|
495
|
+
return {
|
|
496
|
+
kind: "datePart",
|
|
497
|
+
part: previous?.part === "hour" || next?.part === "second" ? "minute" : "month",
|
|
498
|
+
width: token.width
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
const elapsed = resolved.some((t) => t.kind === "elapsed");
|
|
502
|
+
const calendarPart = resolved.some((t) => t.kind === "datePart" && (t.part === "year" || t.part === "month" || t.part === "day"));
|
|
503
|
+
if (resolved.some((t) => t.kind === "digit" || t.kind === "exponent")) return void 0;
|
|
504
|
+
if (elapsed && calendarPart) return void 0;
|
|
505
|
+
return {
|
|
506
|
+
kind: "date",
|
|
507
|
+
tokens: resolved,
|
|
508
|
+
elapsed,
|
|
509
|
+
calendar: calendarPart,
|
|
510
|
+
hasMeridiem: resolved.some((t) => t.kind === "meridiem"),
|
|
511
|
+
subsecondDigits
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
const parseSection = (src) => {
|
|
515
|
+
const tokens = tokenizeSection(src);
|
|
516
|
+
if (tokens === void 0) return void 0;
|
|
517
|
+
if (tokens.length === 0) return { kind: "blank" };
|
|
518
|
+
if (tokens.some((t) => t.kind === "general")) return tokens.length === 1 ? { kind: "general" } : void 0;
|
|
519
|
+
const hasText = tokens.some((t) => t.kind === "textPlaceholder");
|
|
520
|
+
const hasDate = tokens.some((t) => t.kind === "datePart" || t.kind === "elapsed" || t.kind === "meridiem");
|
|
521
|
+
if (hasText && hasDate) return void 0;
|
|
522
|
+
if (hasText) return {
|
|
523
|
+
kind: "text",
|
|
524
|
+
tokens
|
|
525
|
+
};
|
|
526
|
+
if (hasDate) return resolveDateSection(tokens);
|
|
527
|
+
if (tokens.every((token) => token.kind === "literal")) return {
|
|
528
|
+
kind: "literal",
|
|
529
|
+
text: tokens.map(literalFor).join("")
|
|
530
|
+
};
|
|
531
|
+
const resolved = resolveCommas(tokens);
|
|
532
|
+
return resolveFractionSection(resolved.tokens) ?? resolveNumberSection(resolved);
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Distinct format codes in a workbook number in the tens while cells number in
|
|
536
|
+
* the millions, so the parse is memoised. Both bounds exist because the codes
|
|
537
|
+
* arrive from an untrusted `styles.xml`: a full map drops its oldest entry
|
|
538
|
+
* rather than every entry, so a workbook carrying more codes than fit loses one
|
|
539
|
+
* cached parse per new code instead of all of them, and a code longer than the
|
|
540
|
+
* 255 characters Excel itself allows is parsed on each call rather than stored.
|
|
541
|
+
*/
|
|
542
|
+
const PARSE_CACHE_LIMIT = 256;
|
|
543
|
+
const MAX_CACHED_CODE_LENGTH = 255;
|
|
544
|
+
const parseCache = /* @__PURE__ */ new Map();
|
|
545
|
+
const buildFormat = (code) => {
|
|
546
|
+
const raw = splitSections(code);
|
|
547
|
+
if (raw === void 0 || raw.length > MAX_SECTIONS) return void 0;
|
|
548
|
+
const sections = [];
|
|
549
|
+
for (const src of raw) {
|
|
550
|
+
const section = parseSection(src);
|
|
551
|
+
if (section === void 0) return void 0;
|
|
552
|
+
sections.push(section);
|
|
553
|
+
}
|
|
554
|
+
return { sections };
|
|
555
|
+
};
|
|
556
|
+
/** Parse a `numFmt` format code, or `undefined` when it is out of scope. */
|
|
557
|
+
function parseFormatCode(code) {
|
|
558
|
+
const cached = parseCache.get(code);
|
|
559
|
+
if (cached !== void 0) return cached.format;
|
|
560
|
+
const format = buildFormat(code);
|
|
561
|
+
if (code.length > MAX_CACHED_CODE_LENGTH) return format;
|
|
562
|
+
if (parseCache.size >= PARSE_CACHE_LIMIT) {
|
|
563
|
+
const oldest = parseCache.keys().next().value;
|
|
564
|
+
if (oldest !== void 0) parseCache.delete(oldest);
|
|
565
|
+
}
|
|
566
|
+
parseCache.set(code, { format });
|
|
567
|
+
return format;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* True when the format reads as a calendar date: a date section carrying a
|
|
571
|
+
* year, month or day, rather than a time of day (`h:mm`), an elapsed span
|
|
572
|
+
* (`[h]:mm`) or a numeric layout. The section selected for the value decides.
|
|
573
|
+
*/
|
|
574
|
+
function hasCalendarDate(format, value) {
|
|
575
|
+
const first = pickSection(format.sections, value)?.section;
|
|
576
|
+
return first?.kind === "date" && first.calendar;
|
|
577
|
+
}
|
|
578
|
+
const padFor = (placeholder) => {
|
|
579
|
+
if (placeholder === "0") return "0";
|
|
580
|
+
return placeholder === "?" ? " " : "";
|
|
581
|
+
};
|
|
582
|
+
const withGrouping = (digits, grouped) => {
|
|
583
|
+
if (!grouped || digits.length <= GROUP_SIZE) return digits;
|
|
584
|
+
const groups = [];
|
|
585
|
+
for (let end = digits.length; end > 0; end -= GROUP_SIZE) groups.unshift(digits.slice(Math.max(0, end - GROUP_SIZE), end));
|
|
586
|
+
return groups.join(GROUP_SEPARATOR);
|
|
587
|
+
};
|
|
588
|
+
/**
|
|
589
|
+
* Render `[from, to)` right to left, feeding `digits` into the placeholders from
|
|
590
|
+
* the low-order end. The leftmost placeholder absorbs whatever is left over,
|
|
591
|
+
* which is how `#,##0` still prints all seven digits of 1,234,567.
|
|
592
|
+
*/
|
|
593
|
+
const renderDigitsRightToLeft = (tokens, from, to, digits) => {
|
|
594
|
+
const leftmost = placeholderIndex(tokens, from, to);
|
|
595
|
+
const pieces = [];
|
|
596
|
+
let pos = digits.length;
|
|
597
|
+
for (let i = to - 1; i >= from; i--) {
|
|
598
|
+
const token = tokens[i];
|
|
599
|
+
if (token === void 0) continue;
|
|
600
|
+
if (token.kind !== "digit") {
|
|
601
|
+
pieces.push(literalFor(token));
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (i === leftmost) {
|
|
605
|
+
pieces.push(pos > 0 ? digits.slice(0, pos) : padFor(token.placeholder));
|
|
606
|
+
pos = 0;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (pos === 0) {
|
|
610
|
+
pieces.push(padFor(token.placeholder));
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
let take = pos - 1;
|
|
614
|
+
while (take > 0 && digits.charAt(take - 1) === GROUP_SEPARATOR) take--;
|
|
615
|
+
pieces.push(digits.slice(take, pos));
|
|
616
|
+
pos = take;
|
|
617
|
+
}
|
|
618
|
+
pieces.reverse();
|
|
619
|
+
return pieces.join("");
|
|
620
|
+
};
|
|
621
|
+
const placeholderIndex = (tokens, from, to) => {
|
|
622
|
+
for (let i = from; i < to; i++) if (tokens[i]?.kind === "digit") return i;
|
|
623
|
+
return -1;
|
|
624
|
+
};
|
|
625
|
+
/** `#` drops trailing zeros, `?` blanks them, `0` keeps them. */
|
|
626
|
+
const renderFracDigits = (placeholders, digits) => {
|
|
627
|
+
const pieces = [];
|
|
628
|
+
let trailing = true;
|
|
629
|
+
for (let i = placeholders.length - 1; i >= 0; i--) {
|
|
630
|
+
const placeholder = placeholders[i];
|
|
631
|
+
if (placeholder === void 0) continue;
|
|
632
|
+
const digit = digits.charAt(i);
|
|
633
|
+
if (trailing && digit === "0" && placeholder !== "0") {
|
|
634
|
+
pieces.push(placeholder === "?" ? " " : "");
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
trailing = false;
|
|
638
|
+
pieces.push(digit);
|
|
639
|
+
}
|
|
640
|
+
pieces.reverse();
|
|
641
|
+
return pieces.join("");
|
|
642
|
+
};
|
|
643
|
+
const assembleNumber = (section, parts, exponentText) => {
|
|
644
|
+
const minIntDigits = section.intPlaceholders.filter((p) => p === "0").length;
|
|
645
|
+
const intDigits = withGrouping(parts.int.replace(/^0+/, "").padStart(minIntDigits, "0"), section.grouped);
|
|
646
|
+
const head = renderDigitsRightToLeft(section.tokens, 0, section.intEnd, intDigits);
|
|
647
|
+
const intText = section.intPlaceholders.length === 0 ? head + intDigits : head;
|
|
648
|
+
const fracText = renderFracDigits(section.fracPlaceholders, parts.frac);
|
|
649
|
+
const tail = [];
|
|
650
|
+
let fracEmitted = false;
|
|
651
|
+
for (let i = section.intEnd; i < section.tokens.length; i++) {
|
|
652
|
+
const token = section.tokens[i];
|
|
653
|
+
if (token === void 0) continue;
|
|
654
|
+
if (token.kind === "point") {
|
|
655
|
+
tail.push(fracText.length > 0 ? "." : "");
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (token.kind === "exponent") {
|
|
659
|
+
tail.push(exponentText);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (token.kind === "digit") {
|
|
663
|
+
if (section.exponentIndex !== -1 && i > section.exponentIndex) continue;
|
|
664
|
+
if (!fracEmitted) {
|
|
665
|
+
tail.push(fracText);
|
|
666
|
+
fracEmitted = true;
|
|
667
|
+
}
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
tail.push(literalFor(token));
|
|
671
|
+
}
|
|
672
|
+
return intText + tail.join("");
|
|
673
|
+
};
|
|
674
|
+
const partsAreZero = (parts) => !/[1-9]/.test(parts.int + parts.frac);
|
|
675
|
+
const renderScientificSection = (section, magnitude) => {
|
|
676
|
+
const decimals = section.fracPlaceholders.length;
|
|
677
|
+
const step = Math.max(1, section.intPlaceholders.length);
|
|
678
|
+
let digits = significantDigits(magnitude);
|
|
679
|
+
let order = decimalOrder(magnitude);
|
|
680
|
+
for (;;) {
|
|
681
|
+
const exponent = magnitude === 0 ? 0 : Math.floor(order / step) * step;
|
|
682
|
+
const intWidth = order - exponent + 1;
|
|
683
|
+
const parts = roundParts(digits.slice(0, intWidth).padEnd(intWidth, "0") || "0", digits.slice(intWidth), decimals);
|
|
684
|
+
if (parts.int.length > intWidth) {
|
|
685
|
+
order += 1;
|
|
686
|
+
digits = "1";
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const exponentText = `E${exponent < 0 ? "-" : section.exponentExplicitSign ? "+" : ""}${String(Math.abs(exponent)).padStart(section.exponentDigits, "0")}`;
|
|
690
|
+
return {
|
|
691
|
+
text: assembleNumber(section, parts, exponentText),
|
|
692
|
+
zero: partsAreZero(parts)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
const renderNumberSection = (section, magnitude) => {
|
|
697
|
+
const scaled = magnitude * PERCENT_MULTIPLIER ** section.percents / SCALE_DIVISOR ** section.scale;
|
|
698
|
+
if (!Number.isFinite(scaled)) return void 0;
|
|
699
|
+
if (section.exponentIndex !== -1) return renderScientificSection(section, scaled);
|
|
700
|
+
const parts = roundDecimal(scaled, section.fracPlaceholders.length);
|
|
701
|
+
return {
|
|
702
|
+
text: assembleNumber(section, parts, ""),
|
|
703
|
+
zero: partsAreZero(parts)
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* Closest `p/q` with `q <= maxDenominator`. Continued-fraction convergents alone
|
|
708
|
+
* are not always the closest (0.7 gives 2/3 before 5/7), so the last step also
|
|
709
|
+
* weighs the best semiconvergent.
|
|
710
|
+
*/
|
|
711
|
+
const bestFraction = (value, maxDenominator) => {
|
|
712
|
+
let numeratorPrev = 0;
|
|
713
|
+
let numerator = 1;
|
|
714
|
+
let denominatorPrev = 1;
|
|
715
|
+
let denominator = 0;
|
|
716
|
+
let best = {
|
|
717
|
+
numerator: Math.round(value),
|
|
718
|
+
denominator: 1
|
|
719
|
+
};
|
|
720
|
+
let x = value;
|
|
721
|
+
for (let term = 0; term < FRACTION_MAX_TERMS; term++) {
|
|
722
|
+
const a = Math.floor(x);
|
|
723
|
+
const nextNumerator = a * numerator + numeratorPrev;
|
|
724
|
+
const nextDenominator = a * denominator + denominatorPrev;
|
|
725
|
+
if (nextDenominator > maxDenominator) {
|
|
726
|
+
const steps = Math.floor((maxDenominator - denominatorPrev) / denominator);
|
|
727
|
+
if (steps > 0) {
|
|
728
|
+
const candidate = {
|
|
729
|
+
numerator: steps * numerator + numeratorPrev,
|
|
730
|
+
denominator: steps * denominator + denominatorPrev
|
|
731
|
+
};
|
|
732
|
+
if (Math.abs(value - candidate.numerator / candidate.denominator) < Math.abs(value - best.numerator / best.denominator)) best = candidate;
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
numeratorPrev = numerator;
|
|
737
|
+
numerator = nextNumerator;
|
|
738
|
+
denominatorPrev = denominator;
|
|
739
|
+
denominator = nextDenominator;
|
|
740
|
+
best = {
|
|
741
|
+
numerator,
|
|
742
|
+
denominator
|
|
743
|
+
};
|
|
744
|
+
const remainder = x - a;
|
|
745
|
+
if (remainder < FRACTION_EPSILON) break;
|
|
746
|
+
x = 1 / remainder;
|
|
747
|
+
}
|
|
748
|
+
return best;
|
|
749
|
+
};
|
|
750
|
+
const blankFor = (placeholder) => placeholder === "#" ? "" : " ";
|
|
751
|
+
const renderDigitsLeftToRight = (tokens, from, to, digits) => {
|
|
752
|
+
const pieces = [];
|
|
753
|
+
let pos = 0;
|
|
754
|
+
const lastPlaceholder = lastPlaceholderIndex(tokens, from, to);
|
|
755
|
+
for (let i = from; i < to; i++) {
|
|
756
|
+
const token = tokens[i];
|
|
757
|
+
if (token === void 0) continue;
|
|
758
|
+
if (token.kind !== "digit") {
|
|
759
|
+
pieces.push(literalFor(token));
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (i === lastPlaceholder) {
|
|
763
|
+
pieces.push(pos < digits.length ? digits.slice(pos) : padFor(token.placeholder));
|
|
764
|
+
pos = digits.length;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (pos < digits.length) {
|
|
768
|
+
pieces.push(digits.charAt(pos));
|
|
769
|
+
pos++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
pieces.push(padFor(token.placeholder));
|
|
773
|
+
}
|
|
774
|
+
return pieces.join("");
|
|
775
|
+
};
|
|
776
|
+
const lastPlaceholderIndex = (tokens, from, to) => {
|
|
777
|
+
for (let i = to - 1; i >= from; i--) if (tokens[i]?.kind === "digit") return i;
|
|
778
|
+
return -1;
|
|
779
|
+
};
|
|
780
|
+
const blankRange = (tokens, from, to) => {
|
|
781
|
+
const pieces = [];
|
|
782
|
+
for (let i = from; i < to; i++) {
|
|
783
|
+
const token = tokens[i];
|
|
784
|
+
if (token === void 0) continue;
|
|
785
|
+
pieces.push(token.kind === "digit" ? blankFor(token.placeholder) : token.kind === "slash" ? " " : literalFor(token));
|
|
786
|
+
}
|
|
787
|
+
return pieces.join("");
|
|
788
|
+
};
|
|
789
|
+
/** The digits a code spells the denominator out as: `16` in `# ?/16`. */
|
|
790
|
+
const fixedDenominatorText = (section) => {
|
|
791
|
+
const token = section.tokens[section.slashIndex + 1];
|
|
792
|
+
return token?.kind === "fixedDenominator" ? token.text : "";
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* `?` and `#` pad the denominator on the right, which is what lines the slash up
|
|
796
|
+
* down a column. A `0` cannot: a pad zero on the right would multiply the
|
|
797
|
+
* denominator by ten, so a run carrying one fills from the right instead.
|
|
798
|
+
*/
|
|
799
|
+
const denominatorText = (section, denominator) => {
|
|
800
|
+
const from = section.slashIndex + 1;
|
|
801
|
+
const digits = String(denominator);
|
|
802
|
+
return placeholdersIn(section.tokens, from, section.denominatorEnd).includes("0") ? renderDigitsRightToLeft(section.tokens, from, section.denominatorEnd, digits) : renderDigitsLeftToRight(section.tokens, from, section.denominatorEnd, digits);
|
|
803
|
+
};
|
|
804
|
+
const renderFractionSection = (section, magnitude) => {
|
|
805
|
+
const hasWhole = section.wholeStart !== -1;
|
|
806
|
+
const maxDenominator = section.fixedDenominator > 0 ? section.fixedDenominator : 10 ** section.denominatorDigits - 1;
|
|
807
|
+
let whole = hasWhole ? Math.floor(magnitude) : 0;
|
|
808
|
+
const remainder = magnitude - whole;
|
|
809
|
+
const approximation = section.fixedDenominator > 0 ? {
|
|
810
|
+
numerator: Math.round(remainder * section.fixedDenominator),
|
|
811
|
+
denominator: section.fixedDenominator
|
|
812
|
+
} : bestFraction(remainder, Math.max(1, maxDenominator));
|
|
813
|
+
const denominator = approximation.denominator;
|
|
814
|
+
let numerator = approximation.numerator;
|
|
815
|
+
if (hasWhole && numerator === denominator && numerator !== 0) {
|
|
816
|
+
whole += 1;
|
|
817
|
+
numerator = 0;
|
|
818
|
+
}
|
|
819
|
+
const pieces = [];
|
|
820
|
+
if (hasWhole) {
|
|
821
|
+
pieces.push(renderDigitsRightToLeft(section.tokens, 0, section.wholeStart, ""));
|
|
822
|
+
const wholeDigits = whole === 0 ? "" : String(whole);
|
|
823
|
+
pieces.push(renderDigitsRightToLeft(section.tokens, section.wholeStart, section.wholeEnd, wholeDigits));
|
|
824
|
+
pieces.push(blankRange(section.tokens, section.wholeEnd, section.numeratorStart));
|
|
825
|
+
} else pieces.push(renderDigitsRightToLeft(section.tokens, 0, section.numeratorStart, ""));
|
|
826
|
+
if (numerator === 0 && hasWhole) {
|
|
827
|
+
pieces.push(blankRange(section.tokens, section.numeratorStart, section.slashIndex + 1));
|
|
828
|
+
pieces.push(section.fixedDenominator > 0 ? " ".repeat(fixedDenominatorText(section).length) : blankRange(section.tokens, section.slashIndex + 1, section.denominatorEnd));
|
|
829
|
+
} else {
|
|
830
|
+
pieces.push(renderDigitsRightToLeft(section.tokens, section.numeratorStart, section.slashIndex, String(numerator)));
|
|
831
|
+
pieces.push("/");
|
|
832
|
+
pieces.push(section.fixedDenominator > 0 ? fixedDenominatorText(section) : denominatorText(section, denominator));
|
|
833
|
+
}
|
|
834
|
+
pieces.push(renderDigitsRightToLeft(section.tokens, section.denominatorEnd, section.tokens.length, ""));
|
|
835
|
+
return {
|
|
836
|
+
text: pieces.join(""),
|
|
837
|
+
zero: whole === 0 && numerator === 0
|
|
838
|
+
};
|
|
839
|
+
};
|
|
840
|
+
/**
|
|
841
|
+
* Excel's `General`: the value at the 15 significant digits Excel stores, which
|
|
842
|
+
* is what turns `1.1 + 2.2` into `3.3` instead of `3.3000000000000003`. How
|
|
843
|
+
* many of those digits Excel shows, and where it gives up and switches to
|
|
844
|
+
* scientific notation, also depends on the column width, which is not part of
|
|
845
|
+
* the cell data, so every stored digit is kept and the notation switches where
|
|
846
|
+
* JavaScript's own shortest round-trip form does.
|
|
847
|
+
*/
|
|
848
|
+
const renderGeneral = (magnitude) => {
|
|
849
|
+
const plain = String(Number(magnitude.toPrecision(EXCEL_SIGNIFICANT_DIGITS)));
|
|
850
|
+
const exponential = /^(\d(?:\.\d+)?)e([+-])(\d+)$/.exec(plain);
|
|
851
|
+
if (exponential === null) return plain;
|
|
852
|
+
return `${exponential[1] ?? ""}E${exponential[2] ?? ""}${(exponential[3] ?? "").padStart(GENERAL_EXPONENT_DIGITS, "0")}`;
|
|
853
|
+
};
|
|
854
|
+
const twoDigits = (value, width) => width === 1 ? String(value) : String(value).padStart(2, "0");
|
|
855
|
+
const renderDatePart = (token, fields, hasMeridiem) => {
|
|
856
|
+
switch (token.part) {
|
|
857
|
+
case "year": return token.width <= 2 ? String(fields.year % 100).padStart(2, "0") : String(fields.year).padStart(FULL_YEAR_DIGITS, "0");
|
|
858
|
+
case "month": {
|
|
859
|
+
if (token.width < NAME_TOKEN_WIDTH) return twoDigits(fields.month, token.width);
|
|
860
|
+
const name = MONTH_NAMES[fields.month - 1] ?? "";
|
|
861
|
+
if (token.width === NAME_TOKEN_WIDTH) return name.slice(0, NAME_TOKEN_WIDTH);
|
|
862
|
+
return token.width === MONTH_FULL_WIDTH ? name : name.slice(0, 1);
|
|
863
|
+
}
|
|
864
|
+
case "day": {
|
|
865
|
+
if (token.width < NAME_TOKEN_WIDTH) return twoDigits(fields.day, token.width);
|
|
866
|
+
const name = WEEKDAY_NAMES[fields.weekday] ?? "";
|
|
867
|
+
return token.width === NAME_TOKEN_WIDTH ? name.slice(0, NAME_TOKEN_WIDTH) : name;
|
|
868
|
+
}
|
|
869
|
+
case "hour": {
|
|
870
|
+
const hour = hasMeridiem ? fields.hour % HOURS_PER_HALF_DAY || HOURS_PER_HALF_DAY : fields.hour;
|
|
871
|
+
return twoDigits(hour, token.width);
|
|
872
|
+
}
|
|
873
|
+
case "minute": return twoDigits(fields.minute, token.width);
|
|
874
|
+
case "second": return twoDigits(fields.second, token.width);
|
|
875
|
+
default: return "";
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const renderElapsed = (token, fields) => {
|
|
879
|
+
const total = token.part === "hour" ? Math.floor(fields.totalSeconds / SECONDS_PER_HOUR) : token.part === "minute" ? Math.floor(fields.totalSeconds / SECONDS_PER_MINUTE) : fields.totalSeconds;
|
|
880
|
+
return String(total).padStart(token.width, "0");
|
|
881
|
+
};
|
|
882
|
+
/**
|
|
883
|
+
* A section can carry two fractional-second groups of different widths
|
|
884
|
+
* (`[ss].000" "ss.0`). The serial is rounded once, at the widest, so a narrower
|
|
885
|
+
* group rounds the digits that rounding already produced.
|
|
886
|
+
*/
|
|
887
|
+
const subsecondText = (subsecond, digits) => {
|
|
888
|
+
if (digits >= subsecond.length) return subsecond.padEnd(digits, "0");
|
|
889
|
+
const kept = subsecond.slice(0, digits);
|
|
890
|
+
if (subsecond.charCodeAt(digits) < FIVE_CHAR_CODE) return kept;
|
|
891
|
+
const bumped = carryOne(kept);
|
|
892
|
+
return bumped.length > digits ? "9".repeat(digits) : bumped;
|
|
893
|
+
};
|
|
894
|
+
const renderDateTokens = (section, fields) => {
|
|
895
|
+
const pieces = [];
|
|
896
|
+
for (const token of section.tokens) switch (token.kind) {
|
|
897
|
+
case "datePart":
|
|
898
|
+
pieces.push(renderDatePart(token, fields, section.hasMeridiem));
|
|
899
|
+
break;
|
|
900
|
+
case "elapsed":
|
|
901
|
+
pieces.push(renderElapsed(token, fields));
|
|
902
|
+
break;
|
|
903
|
+
case "meridiem":
|
|
904
|
+
pieces.push(fields.hour < HOURS_PER_HALF_DAY ? token.am : token.pm);
|
|
905
|
+
break;
|
|
906
|
+
case "subsecond":
|
|
907
|
+
pieces.push(`.${subsecondText(fields.subsecond, token.digits)}`);
|
|
908
|
+
break;
|
|
909
|
+
default: pieces.push(literalFor(token));
|
|
910
|
+
}
|
|
911
|
+
return pieces.join("");
|
|
912
|
+
};
|
|
913
|
+
const renderDateSection = (section, serial, epoch) => {
|
|
914
|
+
const subDigits = Math.min(section.subsecondDigits, MAX_SUBSECOND_DIGITS);
|
|
915
|
+
const subUnits = 10 ** subDigits;
|
|
916
|
+
const unitsPerDay = SECONDS_PER_DAY * subUnits;
|
|
917
|
+
const units = Math.round(serial * unitsPerDay);
|
|
918
|
+
if (!Number.isSafeInteger(units)) return void 0;
|
|
919
|
+
const totalSeconds = Math.floor(units / subUnits);
|
|
920
|
+
const subsecond = String(units % subUnits).padStart(subDigits, "0");
|
|
921
|
+
if (section.elapsed) return renderDateTokens(section, {
|
|
922
|
+
year: 0,
|
|
923
|
+
month: 1,
|
|
924
|
+
day: 1,
|
|
925
|
+
weekday: 0,
|
|
926
|
+
hour: Math.floor(totalSeconds / SECONDS_PER_HOUR) % HOURS_PER_DAY,
|
|
927
|
+
minute: Math.floor(totalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR,
|
|
928
|
+
second: totalSeconds % SECONDS_PER_MINUTE,
|
|
929
|
+
totalSeconds,
|
|
930
|
+
subsecond
|
|
931
|
+
});
|
|
932
|
+
const date = excelToDate(units / unitsPerDay, { epoch });
|
|
933
|
+
if (Number.isNaN(date.getTime())) return void 0;
|
|
934
|
+
return renderDateTokens(section, {
|
|
935
|
+
year: date.getUTCFullYear(),
|
|
936
|
+
month: date.getUTCMonth() + 1,
|
|
937
|
+
day: date.getUTCDate(),
|
|
938
|
+
weekday: date.getUTCDay(),
|
|
939
|
+
hour: date.getUTCHours(),
|
|
940
|
+
minute: date.getUTCMinutes(),
|
|
941
|
+
second: date.getUTCSeconds(),
|
|
942
|
+
totalSeconds,
|
|
943
|
+
subsecond
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
/**
|
|
947
|
+
* Excel picks the section by sign: one section covers everything, two split
|
|
948
|
+
* at zero, three or four add a dedicated zero branch. Only a lone section has
|
|
949
|
+
* to be handed the sign, because the others spell their own.
|
|
950
|
+
*/
|
|
951
|
+
const pickSection = (sections, value) => {
|
|
952
|
+
const first = sections[0];
|
|
953
|
+
if (first === void 0) return void 0;
|
|
954
|
+
if (sections.length === 1) return {
|
|
955
|
+
section: first,
|
|
956
|
+
signed: value < 0
|
|
957
|
+
};
|
|
958
|
+
if (value < 0) {
|
|
959
|
+
const negative = sections[1];
|
|
960
|
+
return negative === void 0 ? void 0 : {
|
|
961
|
+
section: negative,
|
|
962
|
+
signed: false
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (value === 0 && sections.length > 2) {
|
|
966
|
+
const zero = sections[2];
|
|
967
|
+
return zero === void 0 ? void 0 : {
|
|
968
|
+
section: zero,
|
|
969
|
+
signed: false
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
section: first,
|
|
974
|
+
signed: false
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
const renderSection = (section, magnitude, epoch) => {
|
|
978
|
+
switch (section.kind) {
|
|
979
|
+
case "literal": return {
|
|
980
|
+
text: section.text,
|
|
981
|
+
zero: true
|
|
982
|
+
};
|
|
983
|
+
case "blank": return {
|
|
984
|
+
text: "",
|
|
985
|
+
zero: true
|
|
986
|
+
};
|
|
987
|
+
case "general":
|
|
988
|
+
case "text": return {
|
|
989
|
+
text: renderGeneral(magnitude),
|
|
990
|
+
zero: magnitude === 0
|
|
991
|
+
};
|
|
992
|
+
case "number": return renderNumberSection(section, magnitude);
|
|
993
|
+
case "fraction": return renderFractionSection(section, magnitude);
|
|
994
|
+
case "date": {
|
|
995
|
+
const text = renderDateSection(section, magnitude, epoch);
|
|
996
|
+
return text === void 0 ? void 0 : {
|
|
997
|
+
text,
|
|
998
|
+
zero: false
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
/**
|
|
1004
|
+
* Render a numeric cell value (a plain number, or a date / duration already
|
|
1005
|
+
* converted to its Excel serial). `undefined` means the value has no reading
|
|
1006
|
+
* under this format and the caller should degrade.
|
|
1007
|
+
*/
|
|
1008
|
+
function renderNumericValue(format, value, epoch) {
|
|
1009
|
+
if (!Number.isFinite(value)) return void 0;
|
|
1010
|
+
const picked = pickSection(format.sections, value);
|
|
1011
|
+
if (picked === void 0) return void 0;
|
|
1012
|
+
if (picked.section.kind === "date" && !picked.section.elapsed && value < 0) return void 0;
|
|
1013
|
+
const rendered = renderSection(picked.section, Math.abs(value), epoch);
|
|
1014
|
+
if (rendered === void 0) return void 0;
|
|
1015
|
+
if (!picked.signed || rendered.zero) return rendered.text;
|
|
1016
|
+
return `-${rendered.text}`;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Render a string cell value. Only the fourth section of a code applies to
|
|
1020
|
+
* text, plus a lone section that is itself a text layout (`@" pcs"`); under any
|
|
1021
|
+
* other code Excel prints the string unchanged.
|
|
1022
|
+
*/
|
|
1023
|
+
function renderTextValue(format, text) {
|
|
1024
|
+
const section = format.sections.length === MAX_SECTIONS ? format.sections[MAX_SECTIONS - 1] : format.sections.length === 1 ? format.sections[0] : void 0;
|
|
1025
|
+
if (section === void 0) return text;
|
|
1026
|
+
if (section.kind === "blank") return "";
|
|
1027
|
+
if (section.kind === "literal" && format.sections.length === MAX_SECTIONS) return section.text;
|
|
1028
|
+
if (section.kind !== "text") return text;
|
|
1029
|
+
return section.tokens.map((token) => token.kind === "textPlaceholder" ? text : literalFor(token)).join("");
|
|
1030
|
+
}
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region src/styles/display-text.ts
|
|
1033
|
+
const epochOf = (wb) => wb.date1904 ? "mac" : "windows";
|
|
1034
|
+
const renderValue = (format, value, epoch) => {
|
|
1035
|
+
if (isRichTextValue(value)) return renderTextValue(format, richTextToString(value.runs));
|
|
1036
|
+
if (typeof value === "string") return renderTextValue(format, value);
|
|
1037
|
+
if (value instanceof Date) return renderNumericValue(format, dateToExcel(value, { epoch }), epoch);
|
|
1038
|
+
if (isDurationValue(value)) return Number.isFinite(value.ms) ? renderNumericValue(format, durationToExcel(value.ms), epoch) : void 0;
|
|
1039
|
+
if (typeof value === "number") return renderNumericValue(format, value, epoch);
|
|
1040
|
+
};
|
|
1041
|
+
const displayText = (code, value, epoch) => {
|
|
1042
|
+
if (value === null) return "";
|
|
1043
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
1044
|
+
if (isErrorValue(value)) return value.code;
|
|
1045
|
+
if (value instanceof Date && Number.isNaN(value.getTime())) return String(value);
|
|
1046
|
+
if (isFormulaValue(value)) {
|
|
1047
|
+
if (value.cachedValue === void 0) return "";
|
|
1048
|
+
if (value.cachedValueType === "error") return String(value.cachedValue);
|
|
1049
|
+
return displayText(code, value.cachedValue, epoch);
|
|
1050
|
+
}
|
|
1051
|
+
const format = parseFormatCode(code);
|
|
1052
|
+
if (format === void 0) return cellValueAsString(value);
|
|
1053
|
+
return renderValue(format, value, epoch) ?? cellValueAsString(value);
|
|
1054
|
+
};
|
|
1055
|
+
/**
|
|
1056
|
+
* The text Excel prints in `c`: the cell's value put through the number format
|
|
1057
|
+
* its style points at.
|
|
1058
|
+
*
|
|
1059
|
+
* This is not a shorter `cellValueAsString`. That one takes a `CellValue` and
|
|
1060
|
+
* answers "what is this value in JavaScript terms", so it never sees the
|
|
1061
|
+
* stylesheet and cannot know that `0.5` is a percentage or that `45365` is a
|
|
1062
|
+
* date. This one takes the workbook and the cell, resolves the cell's `numFmt`,
|
|
1063
|
+
* and answers "what would somebody looking at this file in Excel read". Pick
|
|
1064
|
+
* `cellValueAsString` when you want the value and this when you want the text:
|
|
1065
|
+
* writing a sheet out as CSV, rendering it as HTML, diffing two workbooks by
|
|
1066
|
+
* what they show, or logging a cell for a human. Choosing wrong fails quietly,
|
|
1067
|
+
* which is why both say so.
|
|
1068
|
+
*
|
|
1069
|
+
* Covered:
|
|
1070
|
+
*
|
|
1071
|
+
* - `General`, at the 15 significant digits Excel stores. This is what turns
|
|
1072
|
+
* `=1.1+2.2` into `3.3` rather than `3.3000000000000003`.
|
|
1073
|
+
* - Every code in `BUILTIN_FORMATS`, including the accounting codes,
|
|
1074
|
+
* scientific notation and the two fraction codes. The one exception is that
|
|
1075
|
+
* catalogue's entry for numFmtId 44, whose stored form is missing the `;`
|
|
1076
|
+
* section separators Excel's own accounting code carries, so it reads as a
|
|
1077
|
+
* text layout.
|
|
1078
|
+
* - Custom numeric codes: `0` `#` `?` digit placeholders, thousands grouping,
|
|
1079
|
+
* trailing commas that divide by a thousand, `%`, `E+`/`E-` (including the
|
|
1080
|
+
* engineering step `##0.0E+0`), `?/?` fractions with a placeholder or a
|
|
1081
|
+
* spelled-out denominator, quoted and `\`-escaped literals, `_x` width
|
|
1082
|
+
* reservations, currency symbols, and colour codes (which change nothing
|
|
1083
|
+
* about the text).
|
|
1084
|
+
* - The positive / negative / zero / text sections of a multi-section code. A
|
|
1085
|
+
* lone section takes the minus sign in front of it; a value that rounds to
|
|
1086
|
+
* zero prints without one.
|
|
1087
|
+
* - Dates and times: `y` `m` `d` `h` `s` runs, `mmm` / `mmmm` month names,
|
|
1088
|
+
* `ddd` / `dddd` weekday names, `AM/PM` and `A/P`, fractional seconds, and
|
|
1089
|
+
* the elapsed forms `[h]` `[mm]` `[ss]`.
|
|
1090
|
+
* - Booleans as `TRUE` / `FALSE`, error cells as their token, rich text as its
|
|
1091
|
+
* runs joined, a formula cell from the value Excel cached for it, and an
|
|
1092
|
+
* empty cell as `''`.
|
|
1093
|
+
*
|
|
1094
|
+
* Not covered, and the cell falls back to `cellValueAsString` so the reading is
|
|
1095
|
+
* a plain coercion rather than a guess:
|
|
1096
|
+
*
|
|
1097
|
+
* - Comparison sections (`[>=100]"over";[<0]"under";0`).
|
|
1098
|
+
* - A section splicing two numeric layouts together (`0.00" ("0.00")"`).
|
|
1099
|
+
* - Calendar and numbering modifiers: era tokens (`g`, `e`, `b`) and bracket
|
|
1100
|
+
* groups such as `[DBNum1]` that replace the digits themselves.
|
|
1101
|
+
* - More than four sections, or an unterminated `"` or `[`.
|
|
1102
|
+
*
|
|
1103
|
+
* Three more places where the text is a judgement call, because Excel's own
|
|
1104
|
+
* answer is not recoverable from the cell:
|
|
1105
|
+
*
|
|
1106
|
+
* - `*x` repeats `x` until the column is full, so nothing is printed for it.
|
|
1107
|
+
* - Month and weekday names come out in English; a `[$-409]`-style locale tag
|
|
1108
|
+
* is read for its currency symbol and otherwise ignored.
|
|
1109
|
+
* - How wide the column is decides how many digits `General` shows and when it
|
|
1110
|
+
* gives up and switches to scientific notation. Every stored digit is kept.
|
|
1111
|
+
*
|
|
1112
|
+
* A `Date` cell value is converted to its serial and run through the format, so
|
|
1113
|
+
* it reads as a date only if the cell carries a date format. That is the same
|
|
1114
|
+
* rule the file itself follows: the worksheet stores a serial and the style
|
|
1115
|
+
* decides what it means. Use `setCellAsDate` on the write side.
|
|
1116
|
+
*/
|
|
1117
|
+
function getCellDisplayText(wb, c) {
|
|
1118
|
+
return displayText(getCellNumberFormat(wb, c), c.value, epochOf(wb));
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* The `Date` a date-formatted cell stands for, or `undefined` when the cell has
|
|
1122
|
+
* no date reading.
|
|
1123
|
+
*
|
|
1124
|
+
* Excel stores a date as a plain number of days since the workbook epoch, so
|
|
1125
|
+
* nothing in a loaded cell's value distinguishes `45365` the date from `45365`
|
|
1126
|
+
* the count: the cell's number format is the only evidence, and
|
|
1127
|
+
* `cellValueAsDate` deliberately does not consult it. This composes the three
|
|
1128
|
+
* steps that reading one otherwise takes, `getCellNumberFormat` then the
|
|
1129
|
+
* format's own reading then `excelToDate` with the epoch from `wb.date1904`,
|
|
1130
|
+
* into the call that reading a foreign workbook actually needs.
|
|
1131
|
+
*
|
|
1132
|
+
* - A `Date` value passes through, formatted or not.
|
|
1133
|
+
* - A number needs a format that names a day: a year, month or day part.
|
|
1134
|
+
* Without one it is a count, and the answer is `undefined`.
|
|
1135
|
+
* - A formula cell is read from the value Excel cached for it.
|
|
1136
|
+
* - A time-of-day format (`h:mm`) names a moment inside a day but not which
|
|
1137
|
+
* day, and an elapsed-time format (`[h]:mm`) measures a span rather than
|
|
1138
|
+
* naming a moment, so neither has a date reading. Neither does a `duration`
|
|
1139
|
+
* value: its `ms` is already the span.
|
|
1140
|
+
* - The format is read by the parser {@link getCellDisplayText} renders with,
|
|
1141
|
+
* so the two agree on what any one cell is. A code outside the subset that
|
|
1142
|
+
* one documents has no date reading here either.
|
|
1143
|
+
*
|
|
1144
|
+
* The returned `Date` is built in UTC, which is how `excelToDate` and
|
|
1145
|
+
* `dateToExcel` read every serial in this library. Use `getUTCFullYear` and
|
|
1146
|
+
* friends, or the local-time accessors will shift the day for anybody east or
|
|
1147
|
+
* west of UTC.
|
|
1148
|
+
*/
|
|
1149
|
+
function getCellDate(wb, c) {
|
|
1150
|
+
const value = isFormulaValue(c.value) ? c.value.cachedValue : c.value;
|
|
1151
|
+
if (value instanceof Date) return value;
|
|
1152
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
1153
|
+
const format = parseFormatCode(getCellNumberFormat(wb, c));
|
|
1154
|
+
if (format === void 0 || !hasCalendarDate(format, value)) return void 0;
|
|
1155
|
+
const date = excelToDate(value, { epoch: epochOf(wb) });
|
|
1156
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
1157
|
+
}
|
|
1158
|
+
//#endregion
|
|
1159
|
+
export { BUILTIN_FORMATS, BUILTIN_FORMATS_MAX_SIZE, BUILTIN_NAMED_STYLES, DEFAULT_FONT, FORMAT_DATE_DATETIME, FORMAT_DATE_TIMEDELTA, FORMAT_DATE_YYYYMMDD2, FORMAT_GENERAL, FORMAT_NUMBER, FORMAT_NUMBER_00, FORMAT_PERCENTAGE, FORMAT_PERCENTAGE_00, FORMAT_TEXT, addBorder, addCellStyleXf, addCellXf, addDxf, addFill, addFont, addNamedStyle, addNumFmt, adjustLightness, adjustSaturation, alignCellHorizontal, alignCellVertical, alignmentToCss, applyBuiltinStyle, applyNamedStyle, borderToCss, builtinFormatCode, builtinFormatId, cellStyleToCss, centerCell, classifyDateFormat, clearCellBackground, clearCellStyle, clearRangeStyle, cloneCellStyle, colorToHex, contrastRatio, copyCellStyle, darken, defaultCellXf, ensureBuiltinStyle, fillToCss, fontToCss, formatAsHeader, getCellAlignment, getCellBorder, getCellDate, getCellDisplayText, getCellFill, getCellFont, getCellNumberFormat, getCellProtection, hexToHsl, hslToHex, indentCell, isBuiltinFormat, isDateFormat, isTimedeltaFormat, lighten, listBorders, listCellStyleXfs, listCellXfs, listFills, listFonts, luminance, makeAlignment, makeBorder, makeColor, makeDifferentialStyle, makeFill, makeFont, makeGradientFill, makeGradientStop, makeNumberFormat, makePatternFill, makeProtection, makeSide, makeStylesheet, mixColors, normaliseRgb, patchCellFont, pickReadableTextColor, registerCellStyle, resolveIndexedColor, rgbColor, rotateCellText, rotateHue, setBold, setCellAlignment, setCellAsCurrency, setCellAsDate, setCellAsNumber, setCellAsPercent, setCellBackgroundColor, setCellBorder, setCellBorderAll, setCellFill, setCellFont, setCellNumberFormat, setCellProtection, setCellStyle, setFontColor, setFontName, setFontSize, setItalic, setRangeAlignment, setRangeBackgroundColor, setRangeBorderBox, setRangeFont, setRangeNumberFormat, setRangeProtection, setRangeStyle, setRangeWrapText, setStrikethrough, setUnderline, wrapCellText };
|
|
1160
|
+
|
|
1161
|
+
//# sourceMappingURL=styles.mjs.map
|