Support XLSX 1904 date imports
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
workbookPrintAreas,
|
||||
workbookRelationshipTargets,
|
||||
workbookSheetRefs,
|
||||
workbookUsesDate1904,
|
||||
worksheetXmlToAutoFilter,
|
||||
worksheetXmlToCells,
|
||||
worksheetXmlToCellFormats,
|
||||
@@ -615,6 +616,38 @@ describe("excellOffice OOXML helpers", () => {
|
||||
).toBe("sheet-7");
|
||||
});
|
||||
|
||||
it("detects the XLSX 1904 date system flag from workbook properties", () => {
|
||||
expect(workbookUsesDate1904({ workbook: { workbookPr: { date1904: "1" } } })).toBe(true);
|
||||
expect(workbookUsesDate1904({ workbook: { workbookPr: { date1904: "true" } } })).toBe(true);
|
||||
expect(workbookUsesDate1904({ workbook: { workbookPr: { date1904: "0" } } })).toBe(false);
|
||||
});
|
||||
|
||||
it("converts imported XLSX 1904 date serials to the QExcell 1900 serial system", async () => {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"xl/workbook.xml",
|
||||
`<workbook><workbookPr date1904="1"/><sheets><sheet name="Dates" sheetId="1" r:id="rId1"/></sheets></workbook>`
|
||||
);
|
||||
zip.file(
|
||||
"xl/_rels/workbook.xml.rels",
|
||||
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="worksheets/sheet1.xml"/></Relationships>`
|
||||
);
|
||||
zip.file("xl/styles.xml", `<styleSheet><cellXfs count="3"><xf numFmtId="0"/><xf numFmtId="14"/><xf numFmtId="20"/></cellXfs></styleSheet>`);
|
||||
zip.file(
|
||||
"xl/worksheets/sheet1.xml",
|
||||
`<worksheet><sheetData><row r="1"><c r="A1" s="1"><v>43465</v></c><c r="B1"><v>5</v></c><c r="C1" s="2"><v>0.5</v></c></row></sheetData></worksheet>`
|
||||
);
|
||||
|
||||
const blob = await zip.generateAsync({
|
||||
type: "blob",
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
const imported = await importXlsxFile(new File([blob], "Date1904.xlsx", { type: blob.type }));
|
||||
|
||||
expect(imported.sheets[0].cells).toEqual({ A1: "44927", B1: "5", C1: "0.5" });
|
||||
expect(imported.sheets[0].cellFormats).toEqual({ A1: { numberFormat: "m/d/yy" }, C1: { numberFormat: "h:mm" } });
|
||||
});
|
||||
|
||||
it("сохраняет и импортирует активный лист XLSX через workbookView activeTab", async () => {
|
||||
const blob = await exportXlsxBlob({
|
||||
id: "book-1",
|
||||
|
||||
+46
-6
@@ -226,6 +226,7 @@ export async function importXlsxFile(file: File): Promise<ImportedExcellWorkbook
|
||||
const themeColors = await readOptionalWorkbookThemeColors(zip, parser);
|
||||
const styleTable = xlsxStylesXmlToCellFormatTable(await readOptionalWorkbookStyles(zip, parser), themeColors);
|
||||
const sheetRefs = workbookSheetRefs(workbookXml);
|
||||
const date1904 = workbookUsesDate1904(workbookXml);
|
||||
const printAreas = workbookPrintAreas(workbookXml, sheetRefs);
|
||||
const relationshipTargets = workbookRelationshipTargets(relationshipsXml);
|
||||
const sheets = await Promise.all(
|
||||
@@ -249,7 +250,7 @@ export async function importXlsxFile(file: File): Promise<ImportedExcellWorkbook
|
||||
const sheetRelationshipsXml = await readOptionalSheetRelationships(zip, parser, sheetPath);
|
||||
const commentsPath = worksheetCommentsPath(sheetRelationshipsXml, sheetPath);
|
||||
const commentsXml = commentsPath ? await readOptionalParsedXml(zip, parser, commentsPath) : undefined;
|
||||
const parsedSheet = parsedWorksheetToSheet(sheetXml, sheet, index, sharedStrings, sheetRelationshipsXml, styleTable, themeColors, commentsXml);
|
||||
const parsedSheet = parsedWorksheetToSheet(sheetXml, sheet, index, sharedStrings, sheetRelationshipsXml, styleTable, themeColors, commentsXml, date1904);
|
||||
const charts = await worksheetXmlToCharts(zip, parser, sheetXml, sheetRelationshipsXml, sheetPath, parsedSheet.name);
|
||||
return { ...parsedSheet, ...(printAreas[sheet.id] ? { printArea: printAreas[sheet.id] } : {}), charts };
|
||||
})
|
||||
@@ -358,7 +359,8 @@ export function parsedWorksheetToSheet(
|
||||
relationshipsXml?: unknown,
|
||||
styleTable: Array<CellFormat | undefined> = [],
|
||||
themeColors: string[] = defaultXlsxThemeColors,
|
||||
commentsXml?: unknown
|
||||
commentsXml?: unknown,
|
||||
date1904 = false
|
||||
): ParsedSheet {
|
||||
const frozenPane = worksheetXmlToFrozenPane(worksheetXml);
|
||||
const autoFilter = worksheetXmlToAutoFilter(worksheetXml);
|
||||
@@ -383,7 +385,7 @@ export function parsedWorksheetToSheet(
|
||||
...(Object.keys(hiddenRows).length > 0 ? { hiddenRows } : {}),
|
||||
...(showGridLines === false ? { showGridLines } : {}),
|
||||
...(zoomScale ? { zoomScale } : {}),
|
||||
cells: worksheetXmlToCells(worksheetXml, sharedStrings),
|
||||
cells: worksheetXmlToCells(worksheetXml, sharedStrings, styleTable, date1904),
|
||||
mergedCells: worksheetXmlToMergedCells(worksheetXml),
|
||||
hyperlinks: worksheetXmlToHyperlinks(worksheetXml, relationshipsXml),
|
||||
comments: worksheetCommentsXmlToComments(commentsXml),
|
||||
@@ -392,7 +394,7 @@ export function parsedWorksheetToSheet(
|
||||
};
|
||||
}
|
||||
|
||||
export function worksheetXmlToCells(worksheetXml: unknown, sharedStrings: string[]) {
|
||||
export function worksheetXmlToCells(worksheetXml: unknown, sharedStrings: string[], styleTable: Array<CellFormat | undefined> = [], date1904 = false) {
|
||||
const cells: Record<string, string> = {};
|
||||
const sharedFormulas: SharedFormulaMap = {};
|
||||
const parsedCells = worksheetCellRecords(worksheetXml);
|
||||
@@ -408,9 +410,10 @@ export function worksheetXmlToCells(worksheetXml: unknown, sharedStrings: string
|
||||
|
||||
parsedCells.forEach(({ address, cell }) => {
|
||||
const text = xlsxCellToText(cell, sharedStrings, sharedFormulas, address);
|
||||
const importedText = date1904 ? date1904AdjustedCellText(cell, text, styleTable[numberAttribute(cell.s)]) : text;
|
||||
|
||||
if (text !== "") {
|
||||
cells[address] = text;
|
||||
if (importedText !== "") {
|
||||
cells[address] = importedText;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1032,6 +1035,12 @@ export function workbookActiveSheetId(workbookXml: unknown, sheets: Array<{ id:
|
||||
return sheets[activeTab]?.id ?? "";
|
||||
}
|
||||
|
||||
export function workbookUsesDate1904(workbookXml: unknown) {
|
||||
const workbook = childRecord(workbookXml, "workbook");
|
||||
const workbookPr = childRecord(workbook, "workbookPr");
|
||||
return booleanAttribute(workbookPr.date1904 ?? workbookPr["@_date1904"]);
|
||||
}
|
||||
|
||||
export function workbookPrintAreas(workbookXml: unknown, sheets: Array<{ id: string; name: string }>): Record<string, string> {
|
||||
const workbook = childRecord(workbookXml, "workbook");
|
||||
const definedNames = childRecord(workbook, "definedNames");
|
||||
@@ -1789,6 +1798,37 @@ function normalizedNumberFormat(value?: string) {
|
||||
return raw.slice(0, 255);
|
||||
}
|
||||
|
||||
function date1904AdjustedCellText(cell: XmlRecord, text: string, format?: CellFormat) {
|
||||
if (!isNumericXlsxCell(cell) || !isDateSerialXlsxNumberFormat(format?.numberFormat)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const serial = Number(text);
|
||||
if (!Number.isFinite(serial) || serial < 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return normalizedXlsxNumberText(serial + 1462);
|
||||
}
|
||||
|
||||
function isNumericXlsxCell(cell: XmlRecord) {
|
||||
const type = typeof cell.t === "string" ? cell.t.trim() : "";
|
||||
return type === "" || type === "n";
|
||||
}
|
||||
|
||||
function isDateSerialXlsxNumberFormat(value?: string) {
|
||||
const pattern = normalizedNumberFormat(value)
|
||||
.replace(/"[^"]*"/g, "")
|
||||
.replace(/\\./g, "")
|
||||
.replace(/\[[^\]]+]/g, "")
|
||||
.toLowerCase();
|
||||
return /[dy]/.test(pattern) || (pattern.includes("m") && !/[hs]/.test(pattern));
|
||||
}
|
||||
|
||||
function normalizedXlsxNumberText(value: number) {
|
||||
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(10)));
|
||||
}
|
||||
|
||||
function xlsxNumberFormats(styleSheet: XmlRecord) {
|
||||
const formats = { ...builtinNumberFormats };
|
||||
toArray(childRecord(styleSheet, "numFmts").numFmt).forEach((numFmt) => {
|
||||
|
||||
Reference in New Issue
Block a user