auto-detect date format (DD/MM vs MM/DD) in CSV imports

This commit is contained in:
2026-02-21 16:56:27 +02:00
parent 33001cda8d
commit c20d20ea6c
6 changed files with 90 additions and 20 deletions
+30 -11
View File
@@ -9,6 +9,23 @@ export interface ImportEntry {
tags?: string[]
}
export type DateFormat = 'DMY' | 'MDY'
export type DateDetectResult = DateFormat | 'ambiguous'
export function detectDateFormat(rows: string[][], dateColIndex: number): DateDetectResult {
if (dateColIndex < 0) return 'ambiguous'
for (let i = 1; i < rows.length; i++) {
const val = (rows[i]?.[dateColIndex] || '').trim()
const match = val.match(/^(\d{1,2})[\/.](\d{1,2})[\/.](\d{2,4})$/)
if (!match) continue
const a = parseInt(match[1])
const b = parseInt(match[2])
if (a > 12) return 'DMY'
if (b > 12) return 'MDY'
}
return 'ambiguous'
}
export function parseCSV(text: string): string[][] {
const lines = text.split('\n').filter(l => l.trim())
return lines.map(line => {
@@ -44,7 +61,7 @@ export function parseDurationString(dur: string): number {
}
// Parse locale-variable date strings into YYYY-MM-DD
function parseFlexDate(dateStr: string): string {
function parseFlexDate(dateStr: string, formatHint?: DateFormat): string {
if (!dateStr) return new Date().toISOString().split('T')[0]
const s = dateStr.trim()
@@ -63,7 +80,9 @@ function parseFlexDate(dateStr: string): string {
if (parseInt(a) > 12) return `${year}-${b.padStart(2, '0')}-${a.padStart(2, '0')}`
// If second part > 12, must be month-first (MM/DD/YYYY)
if (parseInt(b) > 12) return `${year}-${a.padStart(2, '0')}-${b.padStart(2, '0')}`
// Ambiguous - assume MM/DD/YYYY (US default, most common Clockify setting)
// Use format hint if provided
if (formatHint === 'DMY') return `${year}-${b.padStart(2, '0')}-${a.padStart(2, '0')}`
// Default to MDY
return `${year}-${a.padStart(2, '0')}-${b.padStart(2, '0')}`
}
@@ -99,13 +118,13 @@ function parseFlexTime(timeStr: string): string {
return '00:00:00'
}
function safeDateTime(date: string, time?: string): string {
const d = parseFlexDate(date)
function safeDateTime(date: string, time?: string, dateFormat?: DateFormat): string {
const d = parseFlexDate(date, dateFormat)
const t = time ? parseFlexTime(time) : '00:00:00'
return `${d}T${t}`
}
function findCol(header: string[], ...terms: string[]): number {
export function findCol(header: string[], ...terms: string[]): number {
for (const term of terms) {
const idx = header.findIndex(h => h === term)
if (idx >= 0) return idx
@@ -121,7 +140,7 @@ function col(row: string[], idx: number): string {
return idx >= 0 ? (row[idx] || '') : ''
}
export function mapTogglCSV(rows: string[][]): ImportEntry[] {
export function mapTogglCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
const header = rows[0].map(h => h.toLowerCase().trim())
const descIdx = findCol(header, 'description')
const projIdx = findCol(header, 'project')
@@ -137,13 +156,13 @@ export function mapTogglCSV(rows: string[][]): ImportEntry[] {
client_name: col(row, clientIdx) || undefined,
task_name: col(row, taskIdx) || undefined,
description: col(row, descIdx) || undefined,
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined),
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined, dateFormat),
duration: parseDurationString(col(row, durationIdx)),
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined,
}))
}
export function mapClockifyCSV(rows: string[][]): ImportEntry[] {
export function mapClockifyCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
const header = rows[0].map(h => h.toLowerCase().trim())
const descIdx = findCol(header, 'description')
const projIdx = findCol(header, 'project')
@@ -172,14 +191,14 @@ export function mapClockifyCSV(rows: string[][]): ImportEntry[] {
client_name: col(row, clientIdx) || undefined,
task_name: col(row, taskIdx) || undefined,
description: col(row, descIdx) || undefined,
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined),
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined, dateFormat),
duration,
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined,
}
})
}
export function mapHarvestCSV(rows: string[][]): ImportEntry[] {
export function mapHarvestCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
const header = rows[0].map(h => h.toLowerCase().trim())
const dateIdx = findCol(header, 'date')
const projIdx = header.findIndex(h => h === 'project')
@@ -193,7 +212,7 @@ export function mapHarvestCSV(rows: string[][]): ImportEntry[] {
client_name: col(row, clientIdx) || undefined,
task_name: col(row, taskIdx) || undefined,
description: col(row, notesIdx) || undefined,
start_time: safeDateTime(col(row, dateIdx)),
start_time: safeDateTime(col(row, dateIdx), undefined, dateFormat),
duration: hoursIdx >= 0 && row[hoursIdx] ? Math.round(parseFloat(row[hoursIdx]) * 3600) : 0,
}))
}