diff --git a/src/io/excellOffice.test.ts b/src/io/excellOffice.test.ts
index 012e8d1..863b2e9 100644
--- a/src/io/excellOffice.test.ts
+++ b/src/io/excellOffice.test.ts
@@ -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",
+ ``
+ );
+ zip.file(
+ "xl/_rels/workbook.xml.rels",
+ ``
+ );
+ zip.file("xl/styles.xml", ``);
+ zip.file(
+ "xl/worksheets/sheet1.xml",
+ `4346550.5
`
+ );
+
+ 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",
diff --git a/src/io/excellOffice.ts b/src/io/excellOffice.ts
index 9814498..c306967 100644
--- a/src/io/excellOffice.ts
+++ b/src/io/excellOffice.ts
@@ -226,6 +226,7 @@ export async function importXlsxFile(file: File): Promise = [],
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 = [], date1904 = false) {
const cells: Record = {};
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 {
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) => {