function splitRow(line) { return line.split(/[;,\t]/).map((value) => value.trim()); } function isNumeric(value) { return value !== '' && Number.isFinite(Number(value)); } function detectHeader(rows) { if (rows.length === 0) { return { hasHeader: false, headers: [] }; } const [firstRow] = rows; const hasHeader = firstRow.some((value) => !isNumeric(value)); return { hasHeader, headers: hasHeader ? firstRow.map((value) => value.toLowerCase()) : [], }; } function detectTimeScale(headers) { const timeHeader = headers.find((header) => header.includes('time') || header.includes('timestamp')); if (!timeHeader) { return 1; } if (timeHeader.includes('sec') && !timeHeader.includes('msec') && !timeHeader.includes('ms')) { return 1000; } return 1; } function detectColumnIndexes(headers, columnCount) { if (headers.length === 0) { return { timeIndex: columnCount > 1 ? 0 : -1, valueIndex: columnCount > 1 ? 1 : 0, }; } const timeIndex = headers.findIndex((header) => header.includes('time') || header.includes('timestamp')); const valueIndex = headers.findIndex((header) => header.includes('value') || header.includes('signal') || header.includes('y')); return { timeIndex, valueIndex: valueIndex >= 0 ? valueIndex : (headers.length > 1 ? 1 : 0), }; } export function parseReplayCsv(text, { sampleRateHz = 60 } = {}) { const rows = text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#')) .map(splitRow) .filter((row) => row.some((value) => value !== '')); if (rows.length === 0) { throw new Error('CSV file is empty'); } const { hasHeader, headers } = detectHeader(rows); const dataRows = hasHeader ? rows.slice(1) : rows; const columnCount = rows[0].length; const { timeIndex, valueIndex } = detectColumnIndexes(headers, columnCount); const timeScale = detectTimeScale(headers); const intervalMs = 1000 / Math.max(1, sampleRateHz); const points = dataRows .map((row, index) => { const rawValue = row[valueIndex]; if (!isNumeric(rawValue)) { return null; } const parsedValue = Number(rawValue); const parsedTime = timeIndex >= 0 && isNumeric(row[timeIndex]) ? Number(row[timeIndex]) * timeScale : index * intervalMs; return { timeMs: parsedTime, value: parsedValue, }; }) .filter(Boolean) .sort((left, right) => left.timeMs - right.timeMs); if (points.length === 0) { throw new Error('CSV file did not contain any numeric data points'); } const firstTime = points[0].timeMs; const normalizedPoints = points.map((point) => ({ timeMs: point.timeMs - firstTime, value: point.value, })); return { points: normalizedPoints, metadata: { pointCount: normalizedPoints.length, durationMs: normalizedPoints.at(-1)?.timeMs ?? 0, }, }; }