From 60317d4ef1996d01f8d24d12af37a1e088a0c9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Boirard?= Date: Tue, 4 Feb 2025 10:00:34 +0100 Subject: [PATCH] Use SERIAL_NUMBER for Date parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: CeĢdric Boirard --- .../src/pages/MapSheetFields.tsx | 2 -- plugins/google-sheets/src/sheets.ts | 30 +++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index 7db40c6e..c72abd8a 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -56,8 +56,6 @@ const inferFieldType = (cellValue: CellValue): CollectionFieldType => { // If the cell value contains a newline, it's probably a formatted text field if (cellValueLowered.includes("\n")) return "formattedText" - const maybeDate = new Date(cellValueLowered) - if (!Number.isNaN(maybeDate.getTime())) return "date" if (/^#[0-9a-f]{6}$/.test(cellValueLowered)) return "color" if (/<[a-z][\s\S]*>/.test(cellValueLowered)) return "formattedText" diff --git a/plugins/google-sheets/src/sheets.ts b/plugins/google-sheets/src/sheets.ts index de543f02..dcac3180 100644 --- a/plugins/google-sheets/src/sheets.ts +++ b/plugins/google-sheets/src/sheets.ts @@ -167,7 +167,7 @@ function fetchSheet(spreadsheetId: string, sheetTitle: string, range?: string) { query: { range: range ?? sheetTitle, valueRenderOption: "UNFORMATTED_VALUE", - dateTimeRenderOption: "FORMATTED_STRING", + dateTimeRenderOption: "SERIAL_NUMBER", }, }) } @@ -279,6 +279,30 @@ export interface SyncMutationOptions { lastSyncedTime: string | null } +const BASE_DATE_1900 = new Date(Date.UTC(1899, 11, 30)) +const BASE_DATE_1904 = new Date(Date.UTC(1904, 0, 1)) +const MS_PER_DAY = 24 * 60 * 60 * 1000 // hours * minutes * seconds * milliseconds + +/** + * Extracts a date from a serial number in Lotus 1-2-3 date representation. + */ +function extractDateFromSerialNumber(serialNumber: number) { + // Use 1900 system by default, but if date is before 1904, + // switch to 1904 system + let baseDate = BASE_DATE_1900 + const date1900 = new Date(BASE_DATE_1900.getTime() + serialNumber * MS_PER_DAY) + + if (date1900 < BASE_DATE_1904) { + baseDate = BASE_DATE_1904 + } + + const wholeDays = Math.floor(serialNumber) + const fractionalDay = serialNumber - wholeDays + const milliseconds = Math.round(fractionalDay * MS_PER_DAY) + + return new Date(baseDate.getTime() + wholeDays * MS_PER_DAY + milliseconds) +} + function getFieldValue(fieldType: CollectionFieldType, cellValue: CellValue) { switch (fieldType) { case "number": { @@ -293,9 +317,9 @@ function getFieldValue(fieldType: CollectionFieldType, cellValue: CellValue) { return CELL_BOOLEAN_VALUES.includes(cellValue) } case "date": { - if (typeof cellValue !== "string") return null + if (typeof cellValue !== "number") return null try { - const date = new Date(Date.parse(cellValue)) + const date = extractDateFromSerialNumber(cellValue) return date.toISOString() } catch { return null