diff options
Diffstat (limited to 'src/data/parse-replay-csv.js')
| -rw-r--r-- | src/data/parse-replay-csv.js | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/src/data/parse-replay-csv.js b/src/data/parse-replay-csv.js new file mode 100644 index 0000000..b6ce97a --- /dev/null +++ b/src/data/parse-replay-csv.js @@ -0,0 +1,108 @@ +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, + }, + }; +} |
