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
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
211
211
|
|
|
212
212
|
```html
|
|
213
213
|
<!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
|
|
214
|
-
<script src="https://unpkg.com/documonster@0.20.
|
|
214
|
+
<script src="https://unpkg.com/documonster@0.20.2/dist/iife/documonster.excel.iife.min.js"></script>
|
|
215
215
|
<script>
|
|
216
216
|
const { Workbook, Cell } = Documonster.Excel;
|
|
217
217
|
const wb = Workbook.create();
|
package/README_zh.md
CHANGED
|
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
198
198
|
|
|
199
199
|
```html
|
|
200
200
|
<!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
|
|
201
|
-
<script src="https://unpkg.com/documonster@0.20.
|
|
201
|
+
<script src="https://unpkg.com/documonster@0.20.2/dist/iife/documonster.excel.iife.min.js"></script>
|
|
202
202
|
<script>
|
|
203
203
|
const { Workbook, Cell } = Documonster.Excel;
|
|
204
204
|
const wb = Workbook.create();
|
|
@@ -374,7 +374,7 @@ function toCivilDate(value, date1904) {
|
|
|
374
374
|
* the only link a `CellData` has; a cell not yet attached to a workbook falls back to the 1900 system, which is
|
|
375
375
|
* also what the writers default to.
|
|
376
376
|
*/
|
|
377
|
-
function cellDate1904(cell) {
|
|
377
|
+
export function cellDate1904(cell) {
|
|
378
378
|
const workbook = cell.row?.worksheet?._workbook;
|
|
379
379
|
return workbook?.properties?.date1904 === true;
|
|
380
380
|
}
|
|
@@ -659,11 +659,7 @@ export function cellText(c) {
|
|
|
659
659
|
return c._value.toString();
|
|
660
660
|
}
|
|
661
661
|
export function cellDisplayText(c) {
|
|
662
|
-
return getCellDisplayText(
|
|
663
|
-
value: c._value.value,
|
|
664
|
-
numFmt: c.style.numFmt,
|
|
665
|
-
text: c._value.toString()
|
|
666
|
-
});
|
|
662
|
+
return getCellDisplayText(cellView(c));
|
|
667
663
|
}
|
|
668
664
|
const HTML_ESCAPE_MAP = {
|
|
669
665
|
'"': """,
|
|
@@ -686,6 +682,9 @@ export function cellView(c) {
|
|
|
686
682
|
get text() {
|
|
687
683
|
return c._value.toString();
|
|
688
684
|
},
|
|
685
|
+
get date1904() {
|
|
686
|
+
return cellDate1904(c);
|
|
687
|
+
},
|
|
689
688
|
get effectiveType() {
|
|
690
689
|
return c._value.effectiveType;
|
|
691
690
|
},
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { cellNumFmt } from "./cell.js";
|
|
1
2
|
import { METRIC_DISPLAY_NAMES, pivotError, isPivotError, formatPivotError, VALID_SUBTOTALS } from "./pivot-table-types.js";
|
|
2
3
|
import { rangeCreate, rangeExpand } from "./range.js";
|
|
3
4
|
import { rowDimensions, rowValues } from "./row.js";
|
|
4
5
|
import { tableModel } from "./table.js";
|
|
5
|
-
import { columnValues, getColumn, getRow, getSheetName, getSheetValues } from "./worksheet-core.js";
|
|
6
|
+
import { columnValues, findCell, getColumn, getRow, getSheetName, getSheetValues } from "./worksheet-core.js";
|
|
6
7
|
import { PivotTableError } from "../errors.js";
|
|
7
8
|
import { colCache } from "../utils/col-cache.js";
|
|
8
|
-
import { range } from "#platform/utils/utils";
|
|
9
|
+
import { isDateFmt, range } from "#platform/utils/utils";
|
|
9
10
|
// Re-export the pure OOXML data types/constants that were relocated to
|
|
10
11
|
// pivot-table-types.ts, preserving backward compatibility for existing
|
|
11
12
|
// `import { X } from "./pivot-table.js"` call sites (worksheet.ts,
|
|
12
13
|
// surface/pivot.ts, etc.).
|
|
13
14
|
export { METRIC_DISPLAY_NAMES, pivotError, isPivotError, formatPivotError, VALID_SUBTOTALS };
|
|
15
|
+
/** A cell's number format as a code, whichever of its two shapes it is stored in. */
|
|
16
|
+
function formatCodeOf(cell) {
|
|
17
|
+
const numFmt = cell === undefined ? undefined : cellNumFmt(cell);
|
|
18
|
+
return typeof numFmt === "string" ? numFmt : numFmt?.formatCode;
|
|
19
|
+
}
|
|
14
20
|
/** Extract the values array from a getRow result (RowData or `{ values }`). */
|
|
15
21
|
function rowValuesOf(row) {
|
|
16
22
|
return "cells" in row ? rowValues(row) : row.values;
|
|
@@ -91,6 +97,10 @@ function createTableSourceAdapter(table) {
|
|
|
91
97
|
}
|
|
92
98
|
return { values };
|
|
93
99
|
},
|
|
100
|
+
getNumberFormat(rowNumber, columnNumber) {
|
|
101
|
+
// The table's rows are a copy of its values; the formats stay on the worksheet's cells.
|
|
102
|
+
return formatCodeOf(findCell(table.worksheet, startRow + rowNumber - 1, startCol + columnNumber - 1));
|
|
103
|
+
},
|
|
94
104
|
getSheetValues() {
|
|
95
105
|
// Return sparse array where index 1 is header row, and subsequent indices are data rows
|
|
96
106
|
const result = [];
|
|
@@ -129,6 +139,7 @@ function resolveSource(model) {
|
|
|
129
139
|
getRow: (rowNumber) => getRow(ws, rowNumber),
|
|
130
140
|
getColumn: (columnNumber) => getColumn(ws, columnNumber),
|
|
131
141
|
getSheetValues: () => getSheetValues(ws),
|
|
142
|
+
getNumberFormat: (rowNumber, columnNumber) => formatCodeOf(findCell(ws, rowNumber, columnNumber)),
|
|
132
143
|
get dimensions() {
|
|
133
144
|
return computeSourceDimensions(ws);
|
|
134
145
|
}
|
|
@@ -492,8 +503,7 @@ function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
|
|
|
492
503
|
const columnValues = columnValuesOf(source.getColumn(columnIndex));
|
|
493
504
|
let sawDate = false;
|
|
494
505
|
for (let i = DATA_START_INDEX; i < columnValues.length; i++) {
|
|
495
|
-
const
|
|
496
|
-
const value = unwrapCellValue(cell);
|
|
506
|
+
const value = unwrapCellValue(columnValues[i]);
|
|
497
507
|
if (value === null || value === undefined) {
|
|
498
508
|
continue;
|
|
499
509
|
}
|
|
@@ -501,11 +511,9 @@ function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
|
|
|
501
511
|
sawDate = true;
|
|
502
512
|
continue;
|
|
503
513
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// A date format names a date or time component outside a literal.
|
|
508
|
-
if (typeof code === "string" && /(?:^|[^\\])[dmyhs]/i.test(code)) {
|
|
514
|
+
// A date format, by the same rule the readers use to turn a serial into a `Date`. Column values are bare
|
|
515
|
+
// values, so the format has to come from the cell itself.
|
|
516
|
+
if (typeof value === "number" && isDateFmt(source.getNumberFormat?.(i, columnIndex))) {
|
|
509
517
|
sawDate = true;
|
|
510
518
|
continue;
|
|
511
519
|
}
|
|
@@ -185,12 +185,10 @@ function readWideString(reader) {
|
|
|
185
185
|
/**
|
|
186
186
|
* A serial wearing a date format is a date — through `isDateFmt`, the same rule every other reader applies.
|
|
187
187
|
*
|
|
188
|
-
* This used to carry its own detector and its own epoch expression, and both differed from the buffered reader's
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* depending on which container it arrived in and whether the caller streamed it — the one thing two readers of the
|
|
193
|
-
* same document must not do, as `readXlsbInto` says in as many words.
|
|
188
|
+
* This used to carry its own detector and its own epoch expression, and both differed from the buffered reader's,
|
|
189
|
+
* so the same workbook read back differently depending on which container it arrived in and whether the caller
|
|
190
|
+
* streamed it — the one thing two readers of the same document must not do, as `readXlsbInto` says in as many words.
|
|
191
|
+
* There is now one tokenizer for number formats (`@utils/number-format`), shared with display and `TEXT`.
|
|
194
192
|
*/
|
|
195
193
|
function dated(value, numberFormat, context) {
|
|
196
194
|
return isDateFmt(numberFormat) ? excelToDate(value, context.date1904) : value;
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* fraction-of-day number, driven entirely by the *target cell's own* `numFmt`
|
|
4
4
|
* — not by guessing the shape of the input string.
|
|
5
5
|
*
|
|
6
|
-
* This is the inverse of
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* This is the inverse of `@utils/number-format-render`: instead of turning a
|
|
7
|
+
* value into display text per a format, it turns display text back into a
|
|
8
|
+
* value per that same format. Both read the format through the one tokenizer
|
|
9
|
+
* in `@utils/number-format`, so a literal on one side is a literal on the other.
|
|
10
10
|
*
|
|
11
11
|
* ## How it works (and why it is not a heuristic)
|
|
12
12
|
*
|
|
@@ -25,24 +25,14 @@
|
|
|
25
25
|
* is what makes this robust across arbitrary formats instead of accreting a
|
|
26
26
|
* patch per newly-discovered format shape.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"february",
|
|
31
|
-
"march",
|
|
32
|
-
"april",
|
|
33
|
-
"may",
|
|
34
|
-
"june",
|
|
35
|
-
"july",
|
|
36
|
-
"august",
|
|
37
|
-
"september",
|
|
38
|
-
"october",
|
|
39
|
-
"november",
|
|
40
|
-
"december"
|
|
41
|
-
];
|
|
28
|
+
import { DAY_NAMES, MONTH_NAMES, numberFormatFacets, splitFormatSections, tokenizeFormatSection } from "../../../utils/number-format.js";
|
|
29
|
+
const MONTHS_LONG = MONTH_NAMES.map(m => m.toLowerCase());
|
|
42
30
|
const MONTHS_SHORT = MONTHS_LONG.map(m => m.slice(0, 3));
|
|
31
|
+
const WEEKDAYS = DAY_NAMES.map(d => d.toLowerCase());
|
|
32
|
+
const WEEKDAYS_SHORT = WEEKDAYS.map(d => d.slice(0, 3));
|
|
43
33
|
/** A field consumes letters (names, AM/PM) rather than digits. */
|
|
44
34
|
function isAlphaField(role) {
|
|
45
|
-
return role === "month" || role === "ampm" || role === "ampmShort";
|
|
35
|
+
return role === "month" || role === "weekday" || role === "ampm" || role === "ampmShort";
|
|
46
36
|
}
|
|
47
37
|
/**
|
|
48
38
|
* Time-tail roles that may be omitted from the *end* of the input. Excel treats
|
|
@@ -53,181 +43,114 @@ const OMITTABLE_TRAILING_ROLES = new Set([
|
|
|
53
43
|
"hour",
|
|
54
44
|
"minute",
|
|
55
45
|
"second",
|
|
46
|
+
"secondFraction",
|
|
56
47
|
"ampm",
|
|
57
48
|
"ampmShort"
|
|
58
49
|
]);
|
|
59
50
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (let i = 0; i < fmt.length; i++) {
|
|
67
|
-
const ch = fmt[i];
|
|
68
|
-
if (ch === "\\") {
|
|
69
|
-
i++;
|
|
70
|
-
}
|
|
71
|
-
else if (ch === '"') {
|
|
72
|
-
quoted = !quoted;
|
|
73
|
-
}
|
|
74
|
-
else if (ch === ";" && !quoted) {
|
|
75
|
-
return fmt.slice(0, i);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return fmt;
|
|
79
|
-
}
|
|
80
|
-
// Sticky (`y`) so it only matches at the exact cursor, and case-insensitive
|
|
81
|
-
// (`i`) because Excel format tokens are case-insensitive (`YYYY-MM-DD`,
|
|
82
|
-
// `AM/PM`, `am/pm`, `A/P` are all valid and mean the same thing).
|
|
83
|
-
const TOKEN_RE = /yyyy|yy|mmmmm|mmmm|mmm|AM\/PM|A\/P|\[h+\]|\[m+\]|\[s+\]|mm|m|dd|d|hh|h|ss|s/iy;
|
|
84
|
-
/**
|
|
85
|
-
* Compile a format section into ordered field/literal segments. Consecutive
|
|
86
|
-
* literal characters coalesce into one literal segment; `mm`/`m` is resolved to
|
|
87
|
-
* month vs. minute by adjacency to an hour (before) or second (after) field,
|
|
88
|
-
* matching the render side's `resolveMonthOrMinute`.
|
|
51
|
+
* Compile a format's first section into ordered field/literal segments.
|
|
52
|
+
*
|
|
53
|
+
* The section is read by the shared number-format tokenizer, the same one that renders it and that
|
|
54
|
+
* decides on load whether a serial is a date — so a character is a field here exactly when it is a
|
|
55
|
+
* code there, and `m`/`mm` is a month or a minute by the same positional rule. Consecutive literal
|
|
56
|
+
* characters coalesce into one literal segment.
|
|
89
57
|
*/
|
|
90
58
|
export function compileFormat(fmt) {
|
|
91
|
-
const
|
|
59
|
+
const tokens = tokenizeFormatSection(splitFormatSections(fmt)[0]);
|
|
92
60
|
const segments = [];
|
|
93
|
-
// Indices (into `segments`) of month/minute placeholders awaiting resolution.
|
|
94
|
-
const ambiguous = [];
|
|
95
61
|
let literal = "";
|
|
96
|
-
let literalHasLetters = false;
|
|
97
62
|
const flushLiteral = () => {
|
|
98
63
|
if (literal.length > 0) {
|
|
99
|
-
segments.push({ kind: "literal", hasLetters:
|
|
64
|
+
segments.push({ kind: "literal", hasLetters: /\p{L}/u.test(literal) });
|
|
100
65
|
literal = "";
|
|
101
|
-
literalHasLetters = false;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const pushLiteralChar = (ch) => {
|
|
105
|
-
literal += ch;
|
|
106
|
-
if (/[A-Za-z]/.test(ch)) {
|
|
107
|
-
literalHasLetters = true;
|
|
108
66
|
}
|
|
109
67
|
};
|
|
110
68
|
const pushField = (role) => {
|
|
111
69
|
flushLiteral();
|
|
112
70
|
segments.push({ kind: "field", role });
|
|
113
71
|
};
|
|
114
|
-
for (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
pushField("
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
72
|
+
for (const token of tokens) {
|
|
73
|
+
switch (token.kind) {
|
|
74
|
+
case "date":
|
|
75
|
+
switch (token.part) {
|
|
76
|
+
case "year":
|
|
77
|
+
pushField(token.width <= 2 ? "year2" : "year4");
|
|
78
|
+
break;
|
|
79
|
+
case "month":
|
|
80
|
+
pushField("month");
|
|
81
|
+
break;
|
|
82
|
+
case "day":
|
|
83
|
+
pushField(token.width <= 2 ? "day" : "weekday");
|
|
84
|
+
break;
|
|
85
|
+
case "hour":
|
|
86
|
+
pushField("hour");
|
|
87
|
+
break;
|
|
88
|
+
case "minute":
|
|
89
|
+
pushField("minute");
|
|
90
|
+
break;
|
|
91
|
+
case "second":
|
|
92
|
+
pushField("second");
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "elapsed":
|
|
97
|
+
pushField(token.unit === "h"
|
|
98
|
+
? "elapsedHour"
|
|
99
|
+
: token.unit === "m"
|
|
100
|
+
? "elapsedMinute"
|
|
101
|
+
: "elapsedSecond");
|
|
102
|
+
break;
|
|
103
|
+
case "numeral":
|
|
104
|
+
literal += token.text;
|
|
105
|
+
break;
|
|
106
|
+
case "ampm":
|
|
107
|
+
pushField(token.short ? "ampmShort" : "ampm");
|
|
108
|
+
break;
|
|
109
|
+
case "literal":
|
|
110
|
+
literal += token.text;
|
|
111
|
+
break;
|
|
112
|
+
case "locale":
|
|
113
|
+
literal += token.symbol;
|
|
114
|
+
break;
|
|
115
|
+
case "pad":
|
|
116
|
+
literal += " ";
|
|
117
|
+
break;
|
|
118
|
+
case "digit":
|
|
119
|
+
literal += token.char;
|
|
120
|
+
break;
|
|
121
|
+
case "decimal":
|
|
122
|
+
literal += ".";
|
|
123
|
+
break;
|
|
124
|
+
case "thousands":
|
|
125
|
+
literal += ",";
|
|
126
|
+
break;
|
|
127
|
+
case "percent":
|
|
128
|
+
literal += "%";
|
|
129
|
+
break;
|
|
130
|
+
case "slash":
|
|
131
|
+
literal += "/";
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
// Fill, colour, condition and other bracket tags are display-only.
|
|
135
|
+
break;
|
|
150
136
|
}
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (m && m.index === i) {
|
|
155
|
-
const raw = m[0].toLowerCase();
|
|
156
|
-
switch (raw) {
|
|
157
|
-
case "yyyy":
|
|
158
|
-
pushField("year4");
|
|
159
|
-
break;
|
|
160
|
-
case "yy":
|
|
161
|
-
pushField("year2");
|
|
162
|
-
break;
|
|
163
|
-
case "mmmmm":
|
|
164
|
-
case "mmmm":
|
|
165
|
-
case "mmm":
|
|
166
|
-
pushField("month");
|
|
167
|
-
break;
|
|
168
|
-
case "dd":
|
|
169
|
-
case "d":
|
|
170
|
-
pushField("day");
|
|
171
|
-
break;
|
|
172
|
-
case "hh":
|
|
173
|
-
case "h":
|
|
174
|
-
pushField("hour");
|
|
175
|
-
break;
|
|
176
|
-
case "ss":
|
|
177
|
-
case "s":
|
|
178
|
-
pushField("second");
|
|
179
|
-
break;
|
|
180
|
-
case "am/pm":
|
|
181
|
-
pushField("ampm");
|
|
182
|
-
break;
|
|
183
|
-
case "a/p":
|
|
184
|
-
pushField("ampmShort");
|
|
185
|
-
break;
|
|
186
|
-
case "mm":
|
|
187
|
-
case "m":
|
|
188
|
-
// Provisionally month; resolved after the whole scan.
|
|
189
|
-
flushLiteral();
|
|
190
|
-
ambiguous.push(segments.length);
|
|
191
|
-
segments.push({ kind: "field", role: "month" });
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
i += m[0].length;
|
|
195
|
-
continue;
|
|
137
|
+
// `ss.00` and `[ss].00`: the tokenizer has already attached the decimal places to the seconds code.
|
|
138
|
+
if ((token.kind === "date" || token.kind === "elapsed") && token.fraction > 0) {
|
|
139
|
+
pushField("secondFraction");
|
|
196
140
|
}
|
|
197
|
-
// Anything else is a literal separator character.
|
|
198
|
-
pushLiteralChar(ch);
|
|
199
|
-
i++;
|
|
200
141
|
}
|
|
201
142
|
flushLiteral();
|
|
202
|
-
// Resolve each ambiguous m/mm: it is a minute if the nearest field before it
|
|
203
|
-
// is an hour, or the nearest field after it is a second (mirroring the
|
|
204
|
-
// render-side adjacency rule).
|
|
205
|
-
const fieldRoleAt = (segIndex, dir) => {
|
|
206
|
-
for (let k = segIndex + dir; k >= 0 && k < segments.length; k += dir) {
|
|
207
|
-
const seg = segments[k];
|
|
208
|
-
if (seg.kind === "field") {
|
|
209
|
-
return seg.role;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return undefined;
|
|
213
|
-
};
|
|
214
|
-
for (const idx of ambiguous) {
|
|
215
|
-
const before = fieldRoleAt(idx, -1);
|
|
216
|
-
const after = fieldRoleAt(idx, 1);
|
|
217
|
-
if (before === "hour" || before === "elapsedHour" || after === "second") {
|
|
218
|
-
segments[idx].role = "minute";
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
143
|
return segments;
|
|
222
144
|
}
|
|
223
|
-
|
|
145
|
+
/** A full name or its three-letter abbreviation — not any word that happens to start like one. */
|
|
146
|
+
function nameIndex(word, full, short) {
|
|
224
147
|
const lower = word.toLowerCase();
|
|
225
|
-
const fullIdx =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
return
|
|
148
|
+
const fullIdx = full.indexOf(lower);
|
|
149
|
+
return fullIdx !== -1 ? fullIdx : short.indexOf(lower);
|
|
150
|
+
}
|
|
151
|
+
function monthIndexFromName(word) {
|
|
152
|
+
const idx = nameIndex(word, MONTHS_LONG, MONTHS_SHORT);
|
|
153
|
+
return idx !== -1 ? idx : undefined;
|
|
231
154
|
}
|
|
232
155
|
/** True if the compiled format carries at least one date field. */
|
|
233
156
|
function hasDateField(segments) {
|
|
@@ -240,6 +163,7 @@ function hasTimeField(segments) {
|
|
|
240
163
|
(s.role === "hour" ||
|
|
241
164
|
s.role === "minute" ||
|
|
242
165
|
s.role === "second" ||
|
|
166
|
+
s.role === "secondFraction" ||
|
|
243
167
|
s.role === "elapsedHour" ||
|
|
244
168
|
s.role === "elapsedMinute" ||
|
|
245
169
|
s.role === "elapsedSecond"));
|
|
@@ -300,7 +224,13 @@ function matchSegments(segments, input) {
|
|
|
300
224
|
return remainingAreOptional(segments, s) ? finalize() : undefined;
|
|
301
225
|
}
|
|
302
226
|
const word = lm[0];
|
|
303
|
-
if (seg.role === "
|
|
227
|
+
if (seg.role === "weekday") {
|
|
228
|
+
// A weekday name is redundant with the date it sits beside; it must only be a real one.
|
|
229
|
+
if (nameIndex(word, WEEKDAYS, WEEKDAYS_SHORT) === -1) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (seg.role === "month") {
|
|
304
234
|
const idx = monthIndexFromName(word);
|
|
305
235
|
if (idx === undefined) {
|
|
306
236
|
return undefined;
|
|
@@ -329,6 +259,9 @@ function matchSegments(segments, input) {
|
|
|
329
259
|
case "year4":
|
|
330
260
|
result.year = num;
|
|
331
261
|
break;
|
|
262
|
+
case "secondFraction":
|
|
263
|
+
result.secondFraction = digits;
|
|
264
|
+
break;
|
|
332
265
|
case "year2":
|
|
333
266
|
// The `yy` token only controls display width; a typed 4-digit year is
|
|
334
267
|
// taken verbatim, a 1–2(–3) digit year uses the 1900/2000 pivot.
|
|
@@ -403,7 +336,8 @@ function matchSegments(segments, input) {
|
|
|
403
336
|
* meaning or the input does not conform to it.
|
|
404
337
|
*/
|
|
405
338
|
export function parseValueByFormat(fmt, input) {
|
|
406
|
-
|
|
339
|
+
// A text format (`yyyy-mm-dd@`) keeps what is typed as text, as the readers keep its values numbers.
|
|
340
|
+
if (input.trim() === "" || numberFormatFacets(fmt).text) {
|
|
407
341
|
return undefined;
|
|
408
342
|
}
|
|
409
343
|
const segments = compileFormat(fmt);
|
|
@@ -421,12 +355,16 @@ export function parseValueByFormat(fmt, input) {
|
|
|
421
355
|
const h = parsed.hour ?? 0;
|
|
422
356
|
const m = parsed.minute ?? 0;
|
|
423
357
|
const s = parsed.second ?? 0;
|
|
424
|
-
|
|
358
|
+
const fraction = Number("0." + (parsed.secondFraction ?? "0"));
|
|
359
|
+
return ((parsed.elapsedSeconds ?? 0) + h * 3600 + m * 60 + s + fraction) / 86400;
|
|
425
360
|
}
|
|
426
361
|
if (parsed.year === undefined || parsed.month === undefined || parsed.day === undefined) {
|
|
427
362
|
return undefined;
|
|
428
363
|
}
|
|
429
|
-
const date = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day, parsed.hour ?? 0, parsed.minute ?? 0, parsed.second ?? 0
|
|
364
|
+
const date = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day, parsed.hour ?? 0, parsed.minute ?? 0, parsed.second ?? 0,
|
|
365
|
+
// A `Date` holds whole milliseconds: take the first three typed digits, so .9999… stays on its
|
|
366
|
+
// second however many nines follow — converting through a double first rounded it up to 1.
|
|
367
|
+
Number((parsed.secondFraction ?? "").slice(0, 3).padEnd(3, "0"))));
|
|
430
368
|
// Reject overflowed components (e.g. day 31 in a 30-day month) rather than
|
|
431
369
|
// silently rolling over into the next month.
|
|
432
370
|
if (date.getUTCMonth() !== parsed.month - 1 || date.getUTCDate() !== parsed.day) {
|