Support XLSX 1904 date imports
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
workbookPrintAreas,
|
workbookPrintAreas,
|
||||||
workbookRelationshipTargets,
|
workbookRelationshipTargets,
|
||||||
workbookSheetRefs,
|
workbookSheetRefs,
|
||||||
|
workbookUsesDate1904,
|
||||||
worksheetXmlToAutoFilter,
|
worksheetXmlToAutoFilter,
|
||||||
worksheetXmlToCells,
|
worksheetXmlToCells,
|
||||||
worksheetXmlToCellFormats,
|
worksheetXmlToCellFormats,
|
||||||
@@ -615,6 +616,38 @@ describe("excellOffice OOXML helpers", () => {
|
|||||||
).toBe("sheet-7");
|
).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 () => {
|
it("сохраняет и импортирует активный лист XLSX через workbookView activeTab", async () => {
|
||||||
const blob = await exportXlsxBlob({
|
const blob = await exportXlsxBlob({
|
||||||
id: "book-1",
|
id: "book-1",
|
||||||
|
|||||||
+46
-6
@@ -226,6 +226,7 @@ export async function importXlsxFile(file: File): Promise<ImportedExcellWorkbook
|
|||||||
const themeColors = await readOptionalWorkbookThemeColors(zip, parser);
|
const themeColors = await readOptionalWorkbookThemeColors(zip, parser);
|
||||||
const styleTable = xlsxStylesXmlToCellFormatTable(await readOptionalWorkbookStyles(zip, parser), themeColors);
|
const styleTable = xlsxStylesXmlToCellFormatTable(await readOptionalWorkbookStyles(zip, parser), themeColors);
|
||||||
const sheetRefs = workbookSheetRefs(workbookXml);
|
const sheetRefs = workbookSheetRefs(workbookXml);
|
||||||
|
const date1904 = workbookUsesDate1904(workbookXml);
|
||||||
const printAreas = workbookPrintAreas(workbookXml, sheetRefs);
|
const printAreas = workbookPrintAreas(workbookXml, sheetRefs);
|
||||||
const relationshipTargets = workbookRelationshipTargets(relationshipsXml);
|
const relationshipTargets = workbookRelationshipTargets(relationshipsXml);
|
||||||
const sheets = await Promise.all(
|
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 sheetRelationshipsXml = await readOptionalSheetRelationships(zip, parser, sheetPath);
|
||||||
const commentsPath = worksheetCommentsPath(sheetRelationshipsXml, sheetPath);
|
const commentsPath = worksheetCommentsPath(sheetRelationshipsXml, sheetPath);
|
||||||
const commentsXml = commentsPath ? await readOptionalParsedXml(zip, parser, commentsPath) : undefined;
|
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);
|
const charts = await worksheetXmlToCharts(zip, parser, sheetXml, sheetRelationshipsXml, sheetPath, parsedSheet.name);
|
||||||
return { ...parsedSheet, ...(printAreas[sheet.id] ? { printArea: printAreas[sheet.id] } : {}), charts };
|
return { ...parsedSheet, ...(printAreas[sheet.id] ? { printArea: printAreas[sheet.id] } : {}), charts };
|
||||||
})
|
})
|
||||||
@@ -358,7 +359,8 @@ export function parsedWorksheetToSheet(
|
|||||||
relationshipsXml?: unknown,
|
relationshipsXml?: unknown,
|
||||||
styleTable: Array<CellFormat | undefined> = [],
|
styleTable: Array<CellFormat | undefined> = [],
|
||||||
themeColors: string[] = defaultXlsxThemeColors,
|
themeColors: string[] = defaultXlsxThemeColors,
|
||||||
commentsXml?: unknown
|
commentsXml?: unknown,
|
||||||
|
date1904 = false
|
||||||
): ParsedSheet {
|
): ParsedSheet {
|
||||||
const frozenPane = worksheetXmlToFrozenPane(worksheetXml);
|
const frozenPane = worksheetXmlToFrozenPane(worksheetXml);
|
||||||
const autoFilter = worksheetXmlToAutoFilter(worksheetXml);
|
const autoFilter = worksheetXmlToAutoFilter(worksheetXml);
|
||||||
@@ -383,7 +385,7 @@ export function parsedWorksheetToSheet(
|
|||||||
...(Object.keys(hiddenRows).length > 0 ? { hiddenRows } : {}),
|
...(Object.keys(hiddenRows).length > 0 ? { hiddenRows } : {}),
|
||||||
...(showGridLines === false ? { showGridLines } : {}),
|
...(showGridLines === false ? { showGridLines } : {}),
|
||||||
...(zoomScale ? { zoomScale } : {}),
|
...(zoomScale ? { zoomScale } : {}),
|
||||||
cells: worksheetXmlToCells(worksheetXml, sharedStrings),
|
cells: worksheetXmlToCells(worksheetXml, sharedStrings, styleTable, date1904),
|
||||||
mergedCells: worksheetXmlToMergedCells(worksheetXml),
|
mergedCells: worksheetXmlToMergedCells(worksheetXml),
|
||||||
hyperlinks: worksheetXmlToHyperlinks(worksheetXml, relationshipsXml),
|
hyperlinks: worksheetXmlToHyperlinks(worksheetXml, relationshipsXml),
|
||||||
comments: worksheetCommentsXmlToComments(commentsXml),
|
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 cells: Record<string, string> = {};
|
||||||
const sharedFormulas: SharedFormulaMap = {};
|
const sharedFormulas: SharedFormulaMap = {};
|
||||||
const parsedCells = worksheetCellRecords(worksheetXml);
|
const parsedCells = worksheetCellRecords(worksheetXml);
|
||||||
@@ -408,9 +410,10 @@ export function worksheetXmlToCells(worksheetXml: unknown, sharedStrings: string
|
|||||||
|
|
||||||
parsedCells.forEach(({ address, cell }) => {
|
parsedCells.forEach(({ address, cell }) => {
|
||||||
const text = xlsxCellToText(cell, sharedStrings, sharedFormulas, address);
|
const text = xlsxCellToText(cell, sharedStrings, sharedFormulas, address);
|
||||||
|
const importedText = date1904 ? date1904AdjustedCellText(cell, text, styleTable[numberAttribute(cell.s)]) : text;
|
||||||
|
|
||||||
if (text !== "") {
|
if (importedText !== "") {
|
||||||
cells[address] = text;
|
cells[address] = importedText;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1032,6 +1035,12 @@ export function workbookActiveSheetId(workbookXml: unknown, sheets: Array<{ id:
|
|||||||
return sheets[activeTab]?.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> {
|
export function workbookPrintAreas(workbookXml: unknown, sheets: Array<{ id: string; name: string }>): Record<string, string> {
|
||||||
const workbook = childRecord(workbookXml, "workbook");
|
const workbook = childRecord(workbookXml, "workbook");
|
||||||
const definedNames = childRecord(workbook, "definedNames");
|
const definedNames = childRecord(workbook, "definedNames");
|
||||||
@@ -1789,6 +1798,37 @@ function normalizedNumberFormat(value?: string) {
|
|||||||
return raw.slice(0, 255);
|
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) {
|
function xlsxNumberFormats(styleSheet: XmlRecord) {
|
||||||
const formats = { ...builtinNumberFormats };
|
const formats = { ...builtinNumberFormats };
|
||||||
toArray(childRecord(styleSheet, "numFmts").numFmt).forEach((numFmt) => {
|
toArray(childRecord(styleSheet, "numFmts").numFmt).forEach((numFmt) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user