auto-detect date format (DD/MM vs MM/DD) in CSV imports
This commit is contained in:
+30
-11
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user