documonster 0.20.1 → 0.20.2
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 +1 -1
- package/README_zh.md +1 -1
- package/dist/esm/modules/excel/core/cell.js +5 -6
- package/dist/esm/modules/excel/core/pivot-table.js +17 -9
- package/dist/esm/modules/excel/stream/xlsb-worksheet-reader.js +4 -6
- package/dist/esm/modules/excel/utils/cell-format-parse.js +112 -174
- package/dist/esm/modules/excel/utils/cell-format.js +42 -946
- package/dist/esm/modules/formula/functions/text.js +23 -631
- package/dist/esm/modules/pdf/excel-bridge.js +5 -5
- package/dist/esm/utils/number-format-render.js +814 -0
- package/dist/esm/utils/number-format.js +477 -0
- package/dist/esm/utils/utils.base.js +3 -70
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.draw.iife.min.js +1 -1
- package/dist/iife/documonster.excel.iife.min.js +50 -50
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.mermaid.iife.min.js +1 -1
- package/dist/iife/documonster.pdf.iife.min.js +18 -18
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +60 -60
- package/dist/iife/documonster.xml.iife.min.js +1 -1
- package/dist/types/modules/excel/core/cell.d.ts +11 -0
- package/dist/types/modules/excel/core/pivot-table.d.ts +7 -0
- package/dist/types/modules/excel/utils/cell-format-parse.d.ts +13 -9
- package/dist/types/modules/excel/utils/cell-format.d.ts +21 -33
- package/dist/types/modules/excel/utils/text-metrics.d.ts +1 -0
- package/dist/types/utils/number-format-render.d.ts +47 -0
- package/dist/types/utils/number-format.d.ts +215 -0
- package/dist/types/utils/utils.base.d.ts +1 -9
- package/package.json +1 -1
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a value through an Excel number format.
|
|
3
|
+
*
|
|
4
|
+
* **One renderer for every caller.** A cell's display text and the `TEXT` worksheet function
|
|
5
|
+
* format a number the same way in Excel, but this library used to carry two implementations:
|
|
6
|
+
* `excel/utils/cell-format.ts` for display and a second copy inside the formula engine. They had
|
|
7
|
+
* different rounding, different fraction search, different month/minute rules and different ideas
|
|
8
|
+
* of what a literal is — `TEXT` read `0" hours"` as a date and dropped every literal from a
|
|
9
|
+
* percentage, while the display path placed the minus sign after a currency symbol. The formula
|
|
10
|
+
* engine sits below the Excel module and could not import the display renderer, so the shared one
|
|
11
|
+
* lives here, at Layer 0, where both can reach it.
|
|
12
|
+
*
|
|
13
|
+
* It renders from the tokens of `./number-format`, so the question "is this character a code or a
|
|
14
|
+
* literal" has one answer for rendering, for date detection on load, for date-kind classification
|
|
15
|
+
* and for input parsing.
|
|
16
|
+
*/
|
|
17
|
+
import { serialToParts } from "./excel-serial.js";
|
|
18
|
+
import { DAY_NAMES, MONTH_NAMES, parseNumberFormat } from "./number-format.js";
|
|
19
|
+
/** What Excel shows for a value its format cannot display: a date past 9999 or a number past `double`. */
|
|
20
|
+
const NOT_DISPLAYABLE = "########";
|
|
21
|
+
/** Serial of 10000-01-01 under the 1900 epoch; a date at or past it has no display. */
|
|
22
|
+
const FIRST_SERIAL_PAST_9999 = 2958466;
|
|
23
|
+
/** Days between the epochs: the same calendar date is this much lower under the 1904 epoch. */
|
|
24
|
+
const EPOCH_1904_OFFSET = 1462;
|
|
25
|
+
/** Excel accepts at most three decimal places of a second (`ss.000`); more is an invalid format. */
|
|
26
|
+
const MAX_SECOND_DECIMALS = 3;
|
|
27
|
+
/**
|
|
28
|
+
* Format `value` as Excel would display it under `fmt`.
|
|
29
|
+
*
|
|
30
|
+
* Text is shown through the format's text section (the fourth, or a trailing section carrying
|
|
31
|
+
* `@`), or as is when there is none. Booleans are `TRUE`/`FALSE`. An empty format is `General`.
|
|
32
|
+
* A value the format cannot show — a date past 9999 or before 1900, a negative time, a conditional
|
|
33
|
+
* format no section of which matches — is shown as hashes, as Excel shows it in a cell.
|
|
34
|
+
*/
|
|
35
|
+
export function renderNumberFormat(fmt, value, options = {}) {
|
|
36
|
+
return tryRenderNumberFormat(fmt, value, options) ?? NOT_DISPLAYABLE;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* {@link renderNumberFormat}, but `undefined` for a value the format cannot show — which is where
|
|
40
|
+
* Excel's `TEXT()` returns `#VALUE!` and a cell shows hashes.
|
|
41
|
+
*/
|
|
42
|
+
export function tryRenderNumberFormat(fmt, value, options = {}) {
|
|
43
|
+
if (typeof value === "boolean") {
|
|
44
|
+
return value ? "TRUE" : "FALSE";
|
|
45
|
+
}
|
|
46
|
+
const parsed = parseNumberFormat(fmt.trim() === "" ? "General" : fmt);
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
return parsed.textSection ? renderTextSection(parsed.textSection.tokens, value) : value;
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(value)) {
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
const sections = parsed.numberSections;
|
|
54
|
+
if (sections.length === 0) {
|
|
55
|
+
// A text-only format such as `"n="@` says nothing about numbers, which are shown as General.
|
|
56
|
+
return formatGeneral(value);
|
|
57
|
+
}
|
|
58
|
+
const choice = chooseSection(sections, value);
|
|
59
|
+
if (choice === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const { section, signed } = choice;
|
|
63
|
+
const shown = signed ? value : Math.abs(value);
|
|
64
|
+
if (section.hasGeneral) {
|
|
65
|
+
return renderGeneralSection(section.tokens, shown);
|
|
66
|
+
}
|
|
67
|
+
if (section.hasDate || section.hasTime || section.hasElapsed) {
|
|
68
|
+
// A negative value has no date or clock reading, whichever section it reached. A lone elapsed count
|
|
69
|
+
// (`[h]`, `[ss]`) is the exception: it is a plain signed number of units.
|
|
70
|
+
if (value < 0 && !isLoneElapsed(section.tokens)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return renderDateSection(section.tokens, shown, options.date1904 === true);
|
|
74
|
+
}
|
|
75
|
+
return renderNumberSection(section.tokens, shown);
|
|
76
|
+
}
|
|
77
|
+
function isLoneElapsed(tokens) {
|
|
78
|
+
return (tokens.filter(t => t.kind === "elapsed").length === 1 &&
|
|
79
|
+
!tokens.some(t => t.kind === "date" || t.kind === "ampm"));
|
|
80
|
+
}
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Section choice
|
|
83
|
+
// =============================================================================
|
|
84
|
+
function compare(value, op, threshold) {
|
|
85
|
+
switch (op) {
|
|
86
|
+
case "=":
|
|
87
|
+
return value === threshold;
|
|
88
|
+
case "<>":
|
|
89
|
+
return value !== threshold;
|
|
90
|
+
case "<":
|
|
91
|
+
return value < threshold;
|
|
92
|
+
case "<=":
|
|
93
|
+
return value <= threshold;
|
|
94
|
+
case ">":
|
|
95
|
+
return value > threshold;
|
|
96
|
+
case ">=":
|
|
97
|
+
return value >= threshold;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** A condition no non-negative number can meet: `[<0]`, `[<-5]`, `[<=-1]`, `[=-5]`. */
|
|
101
|
+
function negativeOnly(c) {
|
|
102
|
+
return (c.op === "<" && c.value <= 0) || ((c.op === "<=" || c.op === "=") && c.value < 0);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Pick the section for a number, and say whether it keeps its sign; `undefined` when no section applies.
|
|
106
|
+
*
|
|
107
|
+
* Without conditions the roles are positional: a negative number takes the second section, which prints
|
|
108
|
+
* its own sign, and zero the third. With a condition, the rules below reproduce Excel exactly over 160
|
|
109
|
+
* measured combinations of condition, section count and value:
|
|
110
|
+
*
|
|
111
|
+
* - A conditioned section that matches is used. It drops the sign only when its condition admits no
|
|
112
|
+
* non-negative number (`[<0]`, `[<=-5]`); `[<100]` and `[<=0]` keep it.
|
|
113
|
+
* - A bare first section beside a conditioned second means `[>0]`.
|
|
114
|
+
* - With three or more sections a bare second one means `[<0]` and the third takes everything else.
|
|
115
|
+
* - With two sections a bare second one takes everything else. A negative number there drops its sign when
|
|
116
|
+
* the first condition covered every positive number or only negatives (`[>0]`, `[>=-5]`, `[<-10]`), and
|
|
117
|
+
* keeps it otherwise (`[>100]`, `[=-5]`).
|
|
118
|
+
* - Two conditions and no third section: a number matching neither has no display.
|
|
119
|
+
*/
|
|
120
|
+
function chooseSection(sections, value) {
|
|
121
|
+
const [first, second, third] = sections;
|
|
122
|
+
if (first.condition === undefined && second?.condition === undefined) {
|
|
123
|
+
if (sections.length === 1) {
|
|
124
|
+
return { section: first, signed: true };
|
|
125
|
+
}
|
|
126
|
+
if (value < 0) {
|
|
127
|
+
return { section: second, signed: false };
|
|
128
|
+
}
|
|
129
|
+
return { section: value === 0 && third ? third : first, signed: true };
|
|
130
|
+
}
|
|
131
|
+
const c1 = first.condition ?? { op: ">", value: 0 };
|
|
132
|
+
if (compare(value, c1.op, c1.value)) {
|
|
133
|
+
return { section: first, signed: !negativeOnly(c1) };
|
|
134
|
+
}
|
|
135
|
+
if (second === undefined) {
|
|
136
|
+
return { section: first, signed: true };
|
|
137
|
+
}
|
|
138
|
+
const c2 = second.condition;
|
|
139
|
+
if (c2) {
|
|
140
|
+
if (compare(value, c2.op, c2.value)) {
|
|
141
|
+
return { section: second, signed: !negativeOnly(c2) };
|
|
142
|
+
}
|
|
143
|
+
return third ? { section: third, signed: true } : undefined;
|
|
144
|
+
}
|
|
145
|
+
if (third) {
|
|
146
|
+
return value < 0 ? { section: second, signed: false } : { section: third, signed: true };
|
|
147
|
+
}
|
|
148
|
+
const coversPositives = (c1.op === ">" || c1.op === ">=") && c1.value <= 0;
|
|
149
|
+
const dropsSign = value < 0 && (coversPositives || (c1.op !== "=" && negativeOnly(c1)));
|
|
150
|
+
return { section: second, signed: !dropsSign };
|
|
151
|
+
}
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Shared pieces
|
|
154
|
+
// =============================================================================
|
|
155
|
+
/** What a token shows when it is not doing its job as a code — or is a pure layout token. */
|
|
156
|
+
function plainText(token) {
|
|
157
|
+
switch (token.kind) {
|
|
158
|
+
case "literal":
|
|
159
|
+
case "numeral":
|
|
160
|
+
return token.text;
|
|
161
|
+
case "pad":
|
|
162
|
+
return " ";
|
|
163
|
+
case "locale":
|
|
164
|
+
return token.symbol;
|
|
165
|
+
case "digit":
|
|
166
|
+
return token.char === "0" ? "0" : token.char === "?" ? " " : "";
|
|
167
|
+
case "decimal":
|
|
168
|
+
return ".";
|
|
169
|
+
case "thousands":
|
|
170
|
+
return ",";
|
|
171
|
+
case "percent":
|
|
172
|
+
return "%";
|
|
173
|
+
case "slash":
|
|
174
|
+
return "/";
|
|
175
|
+
case "exponent":
|
|
176
|
+
return (token.upper ? "E" : "e") + token.sign;
|
|
177
|
+
default:
|
|
178
|
+
// fill, colour, condition, other brackets, and codes with no meaning here.
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Render text through a text section. The caller decides whether there is one; an *empty* text section
|
|
184
|
+
* (`0;0;0;`) is a real section that shows nothing, not an absent one that shows the text as is.
|
|
185
|
+
*/
|
|
186
|
+
function renderTextSection(tokens, text) {
|
|
187
|
+
let out = "";
|
|
188
|
+
for (const token of tokens) {
|
|
189
|
+
out += token.kind === "text" ? text : plainText(token);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
/** Trailing zeros of a decimal fraction, and the point itself when nothing follows it. */
|
|
194
|
+
function stripDecimal(s) {
|
|
195
|
+
return s.includes(".") ? s.replace(/(?:\.0*|(\.\d*[1-9])0+)$/, "$1") : s;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Excel's `General`: at most eleven characters, not counting a minus sign, switching to scientific
|
|
199
|
+
* notation with six significant digits when a number cannot fit — `0.333333333`, `1.23457E+11`,
|
|
200
|
+
* `1234567891` for 1234567890.5. The shape of the algorithm is SheetJS's, which its authors calibrated
|
|
201
|
+
* against Excel; the rounding is decimal, at fifteen significant digits, so a half like -501059677.45 rounds
|
|
202
|
+
* away from zero as Excel does rather than as its nearest double would. It agreed with Excel on all 146
|
|
203
|
+
* values measured.
|
|
204
|
+
*/
|
|
205
|
+
export function formatGeneral(value) {
|
|
206
|
+
if (value === 0) {
|
|
207
|
+
return "0";
|
|
208
|
+
}
|
|
209
|
+
const sign = value < 0 ? "-" : "";
|
|
210
|
+
const x = Math.abs(value);
|
|
211
|
+
const fixed = (decimals) => {
|
|
212
|
+
const { int, frac } = toFixedDigits(x, decimals);
|
|
213
|
+
return (int || "0") + (decimals > 0 ? "." + frac : "");
|
|
214
|
+
};
|
|
215
|
+
const magnitude = Math.floor(Math.log10(x));
|
|
216
|
+
let s;
|
|
217
|
+
if (magnitude >= -4 && magnitude <= -1) {
|
|
218
|
+
s = fixed(9);
|
|
219
|
+
}
|
|
220
|
+
else if (Math.abs(magnitude) <= 9) {
|
|
221
|
+
s = stripDecimal(fixed(12));
|
|
222
|
+
if (s.length > 11) {
|
|
223
|
+
s = fixed(9 - magnitude);
|
|
224
|
+
if (s.split(".")[0].length > Math.max(magnitude, 0) + 1) {
|
|
225
|
+
// Rounding carried into the next magnitude: 9999999999.9 is 10000000000.
|
|
226
|
+
return sign + formatGeneral(Number(s));
|
|
227
|
+
}
|
|
228
|
+
if (stripDecimal(s).length > 11) {
|
|
229
|
+
s = x.toExponential(5);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (magnitude === 10) {
|
|
234
|
+
// Eleven integer digits fill the width: round to a whole number, which may carry to twelve digits.
|
|
235
|
+
const rounded = Number(toFixedDigits(x, 0).int);
|
|
236
|
+
return sign + (rounded >= 1e11 ? formatGeneral(rounded) : String(rounded));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
s = stripDecimal(fixed(11));
|
|
240
|
+
if (s.length > 11 || s === "0") {
|
|
241
|
+
s = x.toExponential(5);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
s = s.toUpperCase();
|
|
245
|
+
if (s.includes("E")) {
|
|
246
|
+
// At least two exponent digits, and no trailing zeros in the mantissa: `1.5E+15`, `1E-12`.
|
|
247
|
+
s = s.replace(/(?:\.0*|(\.\d*[1-9])0+)E/, "$1E").replace(/E([+-])(\d)$/, "E$10$2");
|
|
248
|
+
}
|
|
249
|
+
return sign + stripDecimal(s);
|
|
250
|
+
}
|
|
251
|
+
function renderGeneralSection(tokens, value) {
|
|
252
|
+
let out = "";
|
|
253
|
+
for (const token of tokens) {
|
|
254
|
+
out += token.kind === "general" ? formatGeneral(value) : plainText(token);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A non-negative number as decimal digit strings, rounded half away from zero at `decimals`
|
|
260
|
+
* places, and taken to fifteen significant digits first as Excel does — so `1.005` rounds to
|
|
261
|
+
* `1.01` rather than to the `1.00` its binary value would give.
|
|
262
|
+
*
|
|
263
|
+
* `int` has no leading zeros and is empty for a value below one. With `shift`, the value is first divided
|
|
264
|
+
* by `10^shift` exactly, by moving the decimal point.
|
|
265
|
+
*/
|
|
266
|
+
function toFixedDigits(x, decimals, shift = 0) {
|
|
267
|
+
if (x === 0) {
|
|
268
|
+
return { int: "", frac: "0".repeat(decimals) };
|
|
269
|
+
}
|
|
270
|
+
const [mantissa, exponent] = x.toExponential(14).split("e");
|
|
271
|
+
let digits = mantissa.replace(".", "");
|
|
272
|
+
// `shift` divides by 10^shift by moving the point in the digit string — never by dividing a `double`,
|
|
273
|
+
// which underflows to 0 for the smallest values and turned 5e-324 into `0.InE-324`.
|
|
274
|
+
let point = Number(exponent) + 1 - shift;
|
|
275
|
+
const keep = point + decimals;
|
|
276
|
+
if (keep < 0) {
|
|
277
|
+
return { int: "", frac: "0".repeat(decimals) };
|
|
278
|
+
}
|
|
279
|
+
if (keep < digits.length) {
|
|
280
|
+
const roundUp = digits.charCodeAt(keep) >= 53;
|
|
281
|
+
const kept = digits.slice(0, keep).split("");
|
|
282
|
+
if (roundUp) {
|
|
283
|
+
let i = kept.length - 1;
|
|
284
|
+
while (i >= 0 && kept[i] === "9") {
|
|
285
|
+
kept[i] = "0";
|
|
286
|
+
i--;
|
|
287
|
+
}
|
|
288
|
+
if (i >= 0) {
|
|
289
|
+
kept[i] = String(Number(kept[i]) + 1);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
kept.unshift("1");
|
|
293
|
+
point++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
digits = kept.join("");
|
|
297
|
+
}
|
|
298
|
+
const full = point <= 0 ? "0".repeat(-point) + digits : digits;
|
|
299
|
+
const at = Math.max(point, 0);
|
|
300
|
+
return {
|
|
301
|
+
int: full.slice(0, at).padEnd(at, "0").replace(/^0+/, ""),
|
|
302
|
+
frac: full.slice(at).padEnd(decimals, "0").slice(0, decimals)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function isDigitChar(s) {
|
|
306
|
+
return s !== undefined && s >= "0" && s <= "9";
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Lay an integer's digits into a run of placeholders, right-aligned.
|
|
310
|
+
*
|
|
311
|
+
* Returns one string per placeholder. A position with no digit shows `0` for `0`, a space for `?`
|
|
312
|
+
* and nothing for `#`; digits beyond the placeholders go in front of the first one, which is how
|
|
313
|
+
* `0` displays `12345`. With `grouping`, a comma follows every third digit from the right.
|
|
314
|
+
*/
|
|
315
|
+
function layInteger(digits, slots, grouping) {
|
|
316
|
+
const count = slots.length;
|
|
317
|
+
const charAt = (p) => {
|
|
318
|
+
if (p < digits.length) {
|
|
319
|
+
return digits[digits.length - 1 - p];
|
|
320
|
+
}
|
|
321
|
+
if (p >= count) {
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
const slot = slots[count - 1 - p];
|
|
325
|
+
return slot === "0" ? "0" : slot === "?" ? " " : "";
|
|
326
|
+
};
|
|
327
|
+
const withComma = (p) => {
|
|
328
|
+
const ch = charAt(p);
|
|
329
|
+
return grouping && p > 0 && p % 3 === 0 && isDigitChar(ch) && isDigitChar(charAt(p - 1))
|
|
330
|
+
? ch + ","
|
|
331
|
+
: ch;
|
|
332
|
+
};
|
|
333
|
+
const parts = [];
|
|
334
|
+
for (let j = 0; j < count; j++) {
|
|
335
|
+
let part = "";
|
|
336
|
+
if (j === 0) {
|
|
337
|
+
for (let p = digits.length - 1; p >= count; p--) {
|
|
338
|
+
part += withComma(p);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
part += withComma(count - 1 - j);
|
|
342
|
+
parts.push(part);
|
|
343
|
+
}
|
|
344
|
+
return parts;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Lay fraction digits into placeholders, left-aligned. Trailing zeros under `#` vanish and under
|
|
348
|
+
* `?` become spaces; a `0` placeholder, or any non-zero digit, stops the trimming.
|
|
349
|
+
*/
|
|
350
|
+
function layFraction(digits, slots) {
|
|
351
|
+
const parts = slots.map((_, k) => digits[k] ?? "0");
|
|
352
|
+
for (let k = slots.length - 1; k >= 0; k--) {
|
|
353
|
+
if (parts[k] !== "0" || slots[k] === "0") {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
parts[k] = slots[k] === "?" ? " " : "";
|
|
357
|
+
}
|
|
358
|
+
return parts;
|
|
359
|
+
}
|
|
360
|
+
function slotOf(token) {
|
|
361
|
+
return token.kind === "digit" ? token.char : "0";
|
|
362
|
+
}
|
|
363
|
+
// =============================================================================
|
|
364
|
+
// Numbers
|
|
365
|
+
// =============================================================================
|
|
366
|
+
function renderNumberSection(tokens, value) {
|
|
367
|
+
const negative = value < 0;
|
|
368
|
+
let x = Math.abs(value);
|
|
369
|
+
const expIndex = tokens.findIndex(t => t.kind === "exponent");
|
|
370
|
+
const fraction = expIndex === -1 ? findFraction(tokens) : undefined;
|
|
371
|
+
const numberEnd = expIndex === -1 ? tokens.length : expIndex;
|
|
372
|
+
let decimalIndex = -1;
|
|
373
|
+
for (let k = 0; k < numberEnd; k++) {
|
|
374
|
+
if (tokens[k].kind === "decimal") {
|
|
375
|
+
decimalIndex = k;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const intEnd = decimalIndex === -1 ? numberEnd : decimalIndex;
|
|
380
|
+
const lastDigitBefore = (end, start) => {
|
|
381
|
+
for (let k = end - 1; k >= start; k--) {
|
|
382
|
+
if (tokens[k].kind === "digit") {
|
|
383
|
+
return k;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return -1;
|
|
387
|
+
};
|
|
388
|
+
const lastIntDigit = lastDigitBefore(intEnd, 0);
|
|
389
|
+
const lastFracDigit = decimalIndex === -1 ? -1 : lastDigitBefore(numberEnd, decimalIndex + 1);
|
|
390
|
+
// A comma that ends a run of digit placeholders divides by a thousand — at the end of the integer
|
|
391
|
+
// part (`0,.0`) or of the fraction (`0.0,,"M"`). One with integer placeholders after it groups.
|
|
392
|
+
let grouping = false;
|
|
393
|
+
let scale = 0;
|
|
394
|
+
for (let k = 0; k < numberEnd; k++) {
|
|
395
|
+
if (tokens[k].kind !== "thousands") {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (fraction) {
|
|
399
|
+
// Only the whole-number part of a fraction groups (`#,##0 ?/?`); a fraction does not scale.
|
|
400
|
+
grouping || (grouping = fraction.integer.length > 0 && k < fraction.numerator[0]);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const partEnd = k < intEnd ? lastIntDigit : lastFracDigit;
|
|
404
|
+
if (partEnd !== -1 && k > partEnd) {
|
|
405
|
+
scale++;
|
|
406
|
+
}
|
|
407
|
+
else if (k < intEnd) {
|
|
408
|
+
grouping = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Each `%` multiplies by 100 and each scaling comma divides by 1,000. Zero stays zero, however many.
|
|
412
|
+
if (x !== 0) {
|
|
413
|
+
x =
|
|
414
|
+
(x * Math.pow(100, tokens.filter(t => t.kind === "percent").length)) / Math.pow(1000, scale);
|
|
415
|
+
}
|
|
416
|
+
if (!Number.isFinite(x)) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
if (fraction) {
|
|
420
|
+
return renderFraction(tokens, fraction, x, negative, grouping);
|
|
421
|
+
}
|
|
422
|
+
if (expIndex !== -1) {
|
|
423
|
+
return renderScientific(tokens, expIndex, decimalIndex, x, negative);
|
|
424
|
+
}
|
|
425
|
+
const intSlots = [];
|
|
426
|
+
const fracSlots = [];
|
|
427
|
+
tokens.forEach((t, k) => {
|
|
428
|
+
if (t.kind === "digit") {
|
|
429
|
+
(decimalIndex !== -1 && k > decimalIndex ? fracSlots : intSlots).push(t.char);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
const fixed = toFixedDigits(x, fracSlots.length);
|
|
433
|
+
// A value that rounds to zero shows no sign. A section with no digit placeholder rounds nothing, so it
|
|
434
|
+
// keeps the sign: `"x"` shows -5 as `-x`, as Excel does.
|
|
435
|
+
const isZero = (intSlots.length > 0 || fracSlots.length > 0) && fixed.int === "" && !/[1-9]/.test(fixed.frac);
|
|
436
|
+
const intParts = layInteger(fixed.int, intSlots, grouping);
|
|
437
|
+
const fracParts = layFraction(fixed.frac, fracSlots);
|
|
438
|
+
let out = negative && !isZero ? "-" : "";
|
|
439
|
+
let intCursor = 0;
|
|
440
|
+
let fracCursor = 0;
|
|
441
|
+
tokens.forEach((token, k) => {
|
|
442
|
+
switch (token.kind) {
|
|
443
|
+
case "digit":
|
|
444
|
+
out +=
|
|
445
|
+
decimalIndex !== -1 && k > decimalIndex ? fracParts[fracCursor++] : intParts[intCursor++];
|
|
446
|
+
break;
|
|
447
|
+
case "decimal":
|
|
448
|
+
if (k === decimalIndex && intSlots.length === 0) {
|
|
449
|
+
// `.00` still shows the integer part of 12.5: there is nowhere else to put it.
|
|
450
|
+
out += grouping ? fixed.int.replace(/\B(?=(\d{3})+$)/g, ",") : fixed.int;
|
|
451
|
+
}
|
|
452
|
+
out += ".";
|
|
453
|
+
break;
|
|
454
|
+
case "thousands":
|
|
455
|
+
break;
|
|
456
|
+
case "text":
|
|
457
|
+
out += formatGeneral(Math.abs(value));
|
|
458
|
+
break;
|
|
459
|
+
default:
|
|
460
|
+
out += plainText(token);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
function renderScientific(tokens, expIndex, decimalIndex, x, negative) {
|
|
466
|
+
const intSlots = [];
|
|
467
|
+
const fracSlots = [];
|
|
468
|
+
const expSlots = [];
|
|
469
|
+
tokens.forEach((t, k) => {
|
|
470
|
+
if (t.kind !== "digit") {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (k > expIndex) {
|
|
474
|
+
expSlots.push(t.char);
|
|
475
|
+
}
|
|
476
|
+
else if (decimalIndex !== -1 && decimalIndex < expIndex && k > decimalIndex) {
|
|
477
|
+
fracSlots.push(t.char);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
intSlots.push(t.char);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
// With several integer placeholders the exponent is a multiple of their count: `##0.0E+0`
|
|
484
|
+
// is engineering notation.
|
|
485
|
+
const step = Math.max(intSlots.length, 1);
|
|
486
|
+
let exp = 0;
|
|
487
|
+
let mantissa = { int: "", frac: "0".repeat(fracSlots.length) };
|
|
488
|
+
if (x !== 0) {
|
|
489
|
+
const magnitude = Number(x.toExponential(14).split("e")[1]);
|
|
490
|
+
exp = intSlots.length === 0 ? magnitude + 1 : Math.floor(magnitude / step) * step;
|
|
491
|
+
mantissa = toFixedDigits(x, fracSlots.length, exp);
|
|
492
|
+
if (mantissa.int.length > (intSlots.length === 0 ? 0 : step)) {
|
|
493
|
+
// Rounding carried into a new digit (9.999 → 10.00): move up one exponent step.
|
|
494
|
+
exp += intSlots.length === 0 ? 1 : step;
|
|
495
|
+
mantissa = toFixedDigits(x, fracSlots.length, exp);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const isZero = mantissa.int === "" && !/[1-9]/.test(mantissa.frac);
|
|
499
|
+
const intParts = layInteger(mantissa.int, intSlots, false);
|
|
500
|
+
const fracParts = layFraction(mantissa.frac, fracSlots);
|
|
501
|
+
const expParts = layInteger(String(Math.abs(exp)).replace(/^0$/, ""), expSlots, false);
|
|
502
|
+
let out = negative && !isZero ? "-" : "";
|
|
503
|
+
let intCursor = 0;
|
|
504
|
+
let fracCursor = 0;
|
|
505
|
+
let expCursor = 0;
|
|
506
|
+
tokens.forEach((token, k) => {
|
|
507
|
+
switch (token.kind) {
|
|
508
|
+
case "digit":
|
|
509
|
+
if (k > expIndex) {
|
|
510
|
+
out += expParts[expCursor++];
|
|
511
|
+
}
|
|
512
|
+
else if (decimalIndex !== -1 && decimalIndex < expIndex && k > decimalIndex) {
|
|
513
|
+
out += fracParts[fracCursor++];
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
out += intParts[intCursor++];
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
case "exponent":
|
|
520
|
+
if (k === expIndex) {
|
|
521
|
+
out += (token.upper ? "E" : "e") + (exp < 0 ? "-" : token.sign === "+" ? "+" : "");
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
out += plainText(token);
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
case "thousands":
|
|
528
|
+
break;
|
|
529
|
+
default:
|
|
530
|
+
out += plainText(token);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
/** A `?/?`, `# ??/??` or `# ?/8` layout, if the section has one. */
|
|
536
|
+
function findFraction(tokens) {
|
|
537
|
+
for (let slash = 1; slash < tokens.length; slash++) {
|
|
538
|
+
if (tokens[slash].kind !== "slash" || tokens[slash - 1].kind !== "digit") {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
let numStart = slash - 1;
|
|
542
|
+
while (numStart > 0 && tokens[numStart - 1].kind === "digit") {
|
|
543
|
+
numStart--;
|
|
544
|
+
}
|
|
545
|
+
const numerator = [];
|
|
546
|
+
for (let k = numStart; k < slash; k++) {
|
|
547
|
+
numerator.push(k);
|
|
548
|
+
}
|
|
549
|
+
const denominator = [];
|
|
550
|
+
let fixedDenominator;
|
|
551
|
+
if (tokens[slash + 1]?.kind === "numeral") {
|
|
552
|
+
// A fixed denominator is written as bare digits (`?/16`); a quoted or escaped digit is text.
|
|
553
|
+
let text = "";
|
|
554
|
+
for (let k = slash + 1; k < tokens.length; k++) {
|
|
555
|
+
const t = tokens[k];
|
|
556
|
+
if (t.kind === "numeral") {
|
|
557
|
+
text += t.text;
|
|
558
|
+
}
|
|
559
|
+
else if (t.kind === "digit" && t.char === "0") {
|
|
560
|
+
text += "0";
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
denominator.push(k);
|
|
566
|
+
}
|
|
567
|
+
fixedDenominator = Number(text);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
for (let k = slash + 1; k < tokens.length && tokens[k].kind === "digit"; k++) {
|
|
571
|
+
denominator.push(k);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (denominator.length === 0) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const integer = [];
|
|
578
|
+
for (let k = 0; k < numStart; k++) {
|
|
579
|
+
if (tokens[k].kind === "digit") {
|
|
580
|
+
integer.push(k);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { integer, numerator, slash, denominator, fixedDenominator };
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The fraction Excel shows for `x ≥ 0` with `d` no larger than `maxDenominator`: the last continued-fraction
|
|
589
|
+
* convergent within the bound. It is not always the closest fraction — Excel shows 0.14159 as 1/7, not
|
|
590
|
+
* 14/99 — and it matched Excel on all 150 values measured. A few dozen steps for any bound, where trying every
|
|
591
|
+
* denominator cost a billion iterations per cell for `?/?????????`.
|
|
592
|
+
*/
|
|
593
|
+
function closestFraction(x, maxDenominator) {
|
|
594
|
+
let p0 = 0;
|
|
595
|
+
let q0 = 1;
|
|
596
|
+
let p1 = 1;
|
|
597
|
+
let q1 = 0;
|
|
598
|
+
let r = x;
|
|
599
|
+
for (let step = 0; step < 64; step++) {
|
|
600
|
+
const a = Math.floor(r);
|
|
601
|
+
const q2 = q0 + a * q1;
|
|
602
|
+
if (q2 > maxDenominator) {
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
const p2 = p0 + a * p1;
|
|
606
|
+
p0 = p1;
|
|
607
|
+
q0 = q1;
|
|
608
|
+
p1 = p2;
|
|
609
|
+
q1 = q2;
|
|
610
|
+
const rest = r - a;
|
|
611
|
+
if (rest === 0 || p1 / q1 === x) {
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
r = 1 / rest;
|
|
615
|
+
}
|
|
616
|
+
return { n: p1, d: q1 };
|
|
617
|
+
}
|
|
618
|
+
/** A non-negative integer's decimal digits, without exponent notation however large it is. */
|
|
619
|
+
function integerDigits(n) {
|
|
620
|
+
return n === 0 ? "0" : toFixedDigits(n, 0).int;
|
|
621
|
+
}
|
|
622
|
+
function renderFraction(tokens, layout, x, negative, grouping) {
|
|
623
|
+
const mixed = layout.integer.length > 0;
|
|
624
|
+
let whole = mixed ? Math.floor(x) : 0;
|
|
625
|
+
const part = mixed ? x - whole : x;
|
|
626
|
+
let n;
|
|
627
|
+
let d;
|
|
628
|
+
if (layout.fixedDenominator !== undefined) {
|
|
629
|
+
d = layout.fixedDenominator;
|
|
630
|
+
n = Math.round(part * d);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
({ n, d } = closestFraction(part, Math.pow(10, layout.denominator.length) - 1));
|
|
634
|
+
}
|
|
635
|
+
if (mixed && n === d) {
|
|
636
|
+
whole++;
|
|
637
|
+
n = 0;
|
|
638
|
+
}
|
|
639
|
+
const isZero = whole === 0 && n === 0;
|
|
640
|
+
// A whole number shows no fraction: Excel blanks it to the width it would have taken.
|
|
641
|
+
const blank = n === 0 && mixed;
|
|
642
|
+
const wholeDigits = whole > 0 ? integerDigits(whole) : n === 0 ? "0" : "";
|
|
643
|
+
const intParts = layInteger(wholeDigits, layout.integer.map(k => slotOf(tokens[k])), grouping);
|
|
644
|
+
const numParts = layInteger(integerDigits(n), layout.numerator.map(k => slotOf(tokens[k])), false);
|
|
645
|
+
// The first denominator placeholder decides how unused places are filled: `0` pads zeros in front
|
|
646
|
+
// (`1/02`), `?` pads spaces behind so fractions line up (`1/2 `), `#` pads nothing. Padding never goes
|
|
647
|
+
// after the digits as zeros, which would change the value: 1/2 is not 1/20.
|
|
648
|
+
const denSlots = layout.denominator.map(k => slotOf(tokens[k]));
|
|
649
|
+
let denText = integerDigits(d);
|
|
650
|
+
if (layout.fixedDenominator === undefined && denText.length < denSlots.length) {
|
|
651
|
+
if (denSlots[0] === "0") {
|
|
652
|
+
denText = denText.padStart(denSlots.length, "0");
|
|
653
|
+
}
|
|
654
|
+
else if (denSlots[0] === "?") {
|
|
655
|
+
denText = denText.padEnd(denSlots.length, " ");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const numStart = layout.numerator[0];
|
|
659
|
+
const denStart = layout.denominator[0];
|
|
660
|
+
const denEnd = layout.denominator[layout.denominator.length - 1];
|
|
661
|
+
let out = negative && !isZero ? "-" : "";
|
|
662
|
+
let intCursor = 0;
|
|
663
|
+
let numCursor = 0;
|
|
664
|
+
tokens.forEach((token, k) => {
|
|
665
|
+
if (k >= numStart && k < layout.slash) {
|
|
666
|
+
out += blank ? " ".repeat(Math.max(numParts[numCursor].length, 1)) : numParts[numCursor];
|
|
667
|
+
numCursor++;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (k === layout.slash) {
|
|
671
|
+
out += blank ? " " : "/";
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (k >= denStart && k <= denEnd) {
|
|
675
|
+
if (k === denStart) {
|
|
676
|
+
out += blank ? " ".repeat(Math.max(denText.length, layout.denominator.length)) : denText;
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (layout.integer.includes(k)) {
|
|
681
|
+
out += intParts[intCursor++];
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (token.kind === "thousands") {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
out += plainText(token);
|
|
688
|
+
});
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
// =============================================================================
|
|
692
|
+
// Dates and times
|
|
693
|
+
// =============================================================================
|
|
694
|
+
function pad2(n) {
|
|
695
|
+
return String(n).padStart(2, "0");
|
|
696
|
+
}
|
|
697
|
+
function renderDateSection(tokens, value, date1904) {
|
|
698
|
+
if (value < 0) {
|
|
699
|
+
// Only a lone elapsed count reaches here negative (see `tryRenderNumberFormat`).
|
|
700
|
+
const magnitude = renderDateSection(tokens, -value, date1904);
|
|
701
|
+
return magnitude === undefined ? undefined : "-" + magnitude;
|
|
702
|
+
}
|
|
703
|
+
let precision = 0;
|
|
704
|
+
for (const t of tokens) {
|
|
705
|
+
if (t.kind === "date" || t.kind === "elapsed") {
|
|
706
|
+
if (t.fraction > MAX_SECOND_DECIMALS) {
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
precision = Math.max(precision, t.fraction);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// The day and the time of day are separated before scaling, so precision multiplies only a value below
|
|
713
|
+
// 86400: scaling the whole serial overflowed a safe integer, and a valid year-2447 date showed hashes at
|
|
714
|
+
// six decimals.
|
|
715
|
+
const scale = Math.pow(10, precision);
|
|
716
|
+
const unitsPerDay = 86400 * scale;
|
|
717
|
+
let day = Math.floor(value);
|
|
718
|
+
// Rounded to the shown precision of a second even when no time is shown, as Excel does: 23:59:59.95
|
|
719
|
+
// is the next day under `yyyy-mm-dd`.
|
|
720
|
+
let units = Math.round((value - day) * unitsPerDay);
|
|
721
|
+
if (units >= unitsPerDay) {
|
|
722
|
+
day++;
|
|
723
|
+
units -= unitsPerDay;
|
|
724
|
+
}
|
|
725
|
+
const subSecond = String(units % scale).padStart(precision, "0");
|
|
726
|
+
const secondOfDay = Math.floor(units / scale);
|
|
727
|
+
const totalSeconds = day * 86400 + secondOfDay;
|
|
728
|
+
// Checked after rounding, so 9999-12-31 23:59:59.9 is past the end too, and against the workbook's own
|
|
729
|
+
// epoch: the last displayable serial is 1462 lower under 1904. A lone elapsed count (`[ss]`) is a plain
|
|
730
|
+
// number and has no such bound; beside a clock or calendar field it does, as in Excel.
|
|
731
|
+
if (!isLoneElapsed(tokens) &&
|
|
732
|
+
day >= FIRST_SERIAL_PAST_9999 - (date1904 ? EPOCH_1904_OFFSET : 0)) {
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
// Serial 0 of the 1900 system is Excel's 1900-01-00, a Saturday.
|
|
736
|
+
const parts = day === 0 && !date1904 ? { year: 1900, month: 1, day: 0 } : serialToParts(day, date1904);
|
|
737
|
+
// From the serial, not the calendar date: Excel's 1900 calendar counts a 1900-02-29 that never existed,
|
|
738
|
+
// so serial 1 is a Sunday and every day before March 1900 is a weekday earlier than the real one.
|
|
739
|
+
// Serial 0 is a Saturday under 1900 and a Friday under 1904.
|
|
740
|
+
const weekday = (day + (date1904 ? 5 : 6)) % 7;
|
|
741
|
+
const hour = Math.floor(secondOfDay / 3600);
|
|
742
|
+
const minute = Math.floor((secondOfDay % 3600) / 60);
|
|
743
|
+
const second = secondOfDay % 60;
|
|
744
|
+
const twelveHour = tokens.some(t => t.kind === "ampm");
|
|
745
|
+
const shownHour = twelveHour ? hour % 12 || 12 : hour;
|
|
746
|
+
const fractionOf = (places) => places === 0 ? "" : "." + subSecond.padEnd(places, "0").slice(0, places);
|
|
747
|
+
let out = "";
|
|
748
|
+
// An elapsed count after a year, month or day field counts only the whole hours of that day, in its own
|
|
749
|
+
// unit: `dd [h]` shows 1.5 as `01 12`, `yyyy [mm]` shows 12:15 as 720 — where `[h] dd` shows `36 01`.
|
|
750
|
+
let calendarShown = false;
|
|
751
|
+
for (const token of tokens) {
|
|
752
|
+
switch (token.kind) {
|
|
753
|
+
case "date":
|
|
754
|
+
calendarShown || (calendarShown = token.part !== "hour" && token.part !== "minute" && token.part !== "second");
|
|
755
|
+
out += datePart(token.part, token.width, parts.year, parts.month, parts.day, weekday, shownHour, minute, second);
|
|
756
|
+
out += token.part === "second" ? fractionOf(token.fraction) : "";
|
|
757
|
+
break;
|
|
758
|
+
case "elapsed": {
|
|
759
|
+
const seconds = calendarShown ? hour * 3600 : totalSeconds;
|
|
760
|
+
const total = token.unit === "h"
|
|
761
|
+
? Math.floor(seconds / 3600)
|
|
762
|
+
: token.unit === "m"
|
|
763
|
+
? Math.floor(seconds / 60)
|
|
764
|
+
: seconds;
|
|
765
|
+
out += String(total).padStart(token.width, "0");
|
|
766
|
+
out += token.unit === "s" ? fractionOf(token.fraction) : "";
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "ampm": {
|
|
770
|
+
const pm = hour >= 12;
|
|
771
|
+
const text = token.short ? (pm ? "P" : "A") : pm ? "PM" : "AM";
|
|
772
|
+
out += token.lower ? text.toLowerCase() : text;
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
default:
|
|
776
|
+
out += plainText(token);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return out;
|
|
780
|
+
}
|
|
781
|
+
function datePart(part, width, year, month, day, weekday, hour, minute, second) {
|
|
782
|
+
switch (part) {
|
|
783
|
+
case "year":
|
|
784
|
+
case "buddhistYear": {
|
|
785
|
+
const y = part === "year" ? year : year + 543;
|
|
786
|
+
return width <= 2 ? pad2(((y % 100) + 100) % 100) : String(y).padStart(4, "0");
|
|
787
|
+
}
|
|
788
|
+
case "month":
|
|
789
|
+
if (width === 1) {
|
|
790
|
+
return String(month);
|
|
791
|
+
}
|
|
792
|
+
if (width === 2) {
|
|
793
|
+
return pad2(month);
|
|
794
|
+
}
|
|
795
|
+
if (width === 3) {
|
|
796
|
+
return MONTH_NAMES[month - 1].slice(0, 3);
|
|
797
|
+
}
|
|
798
|
+
return width === 5 ? MONTH_NAMES[month - 1][0] : MONTH_NAMES[month - 1];
|
|
799
|
+
case "day":
|
|
800
|
+
if (width === 1) {
|
|
801
|
+
return String(day);
|
|
802
|
+
}
|
|
803
|
+
if (width === 2) {
|
|
804
|
+
return pad2(day);
|
|
805
|
+
}
|
|
806
|
+
return width === 3 ? DAY_NAMES[weekday].slice(0, 3) : DAY_NAMES[weekday];
|
|
807
|
+
case "hour":
|
|
808
|
+
return width === 1 ? String(hour) : pad2(hour);
|
|
809
|
+
case "minute":
|
|
810
|
+
return width === 1 ? String(minute) : pad2(minute);
|
|
811
|
+
default:
|
|
812
|
+
return width === 1 ? String(second) : pad2(second);
|
|
813
|
+
}
|
|
814
|
+
}
|