glutamatt's picture
glutamatt HF Staff
calories
0f92957 verified
raw
history blame
7.77 kB
import Papa from 'papaparse';
import type { Activity } from '@/types';
import { calculateTSS } from './tssCalculator';
import { validateCSV } from './csvValidator';
export function parseCSV(file: File, userFTP: number = 343): Promise<Activity[]> {
return new Promise((resolve, reject) => {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: (results) => {
try {
// Validate CSV structure first
const validation = validateCSV(results.data);
if (!validation.isValid) {
const errorMessage = [
'CSV Validation Failed:',
'',
...validation.errors.map(err => `❌ ${err}`),
...(validation.warnings.length > 0 ? ['', 'Warnings:', ...validation.warnings.map(warn => `⚠️ ${warn}`)] : []),
].join('\n');
reject(new Error(errorMessage));
return;
}
// Log warnings to console if any
if (validation.warnings.length > 0) {
console.warn('CSV Validation Warnings:', validation.warnings);
}
const activities = results.data.map((row: any) => {
// Parse date - Garmin format: "2025-12-02 09:19:49"
const dateStr = row['Date'] || row['Activity Date'] || row['date'];
if (!dateStr || dateStr === '--') {
return null;
}
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
return null;
}
// Parse distance - Garmin format: "8.34" (in km)
let distance: number | undefined;
const distanceStr = row['Distance'] || row['distance'];
if (distanceStr && distanceStr !== '--' && distanceStr !== '0.00') {
const parsed = parseFloat(distanceStr.replace(/,/g, ''));
if (!isNaN(parsed) && parsed > 0) {
distance = parsed;
}
}
// Parse duration - Garmin format: "00:48:51" (HH:MM:SS)
let duration: number | undefined;
let durationSeconds: number | undefined;
const durationStr = row['Time'] || row['Duration'] || row['Moving Time'] || row['Elapsed Time'];
if (durationStr && durationStr !== '--') {
if (durationStr.includes(':')) {
const parts = durationStr.split(':');
const hours = parseInt(parts[0]) || 0;
const minutes = parseInt(parts[1]) || 0;
const seconds = parseFloat(parts[2]) || 0;
duration = hours * 60 + minutes + seconds / 60;
durationSeconds = hours * 3600 + minutes * 60 + seconds;
} else {
const parsed = parseFloat(durationStr);
if (!isNaN(parsed)) {
duration = parsed;
durationSeconds = parsed * 60; // Assume minutes if no colon
}
}
}
// Parse TSS - Try to calculate from power data first
let trainingStressScore: number | undefined;
// First, check if TSS is already provided in CSV
const tssStr = row['Training Stress Score®'] || row['Training Stress Score'] || row['TSS'];
if (tssStr && tssStr !== '--' && tssStr !== '0.0' && tssStr !== '0') {
const parsed = parseFloat(tssStr);
if (!isNaN(parsed) && parsed > 0) {
trainingStressScore = parsed;
}
}
// If no TSS provided, try to calculate from power data
if (!trainingStressScore && durationSeconds) {
// Parse Normalized Power (NP) - find column that starts with "Normalized Power"
let normalizedPower: number | undefined;
const npKey = Object.keys(row).find(key => key.startsWith('Normalized Power'));
if (npKey) {
const npStr = row[npKey];
if (npStr && npStr !== '--' && npStr !== '0.0' && npStr !== '0') {
normalizedPower = parseFloat(npStr);
if (isNaN(normalizedPower) || normalizedPower <= 0) {
normalizedPower = undefined;
}
}
}
// Use FTP from CSV if available, otherwise use user-provided FTP
const ftpStr = row['FTP'] || row['Functional Threshold Power'];
const ftp = ftpStr && ftpStr !== '--' ? parseFloat(ftpStr) : userFTP;
if (normalizedPower && normalizedPower > 0 && ftp && ftp > 0) {
const calculatedTSS = calculateTSS({
durationSeconds,
normalizedPower,
ftp,
});
if (calculatedTSS !== undefined) {
trainingStressScore = calculatedTSS;
}
}
}
// Get activity type
const activityType = row['Activity Type'] || row['Type'] || row['Sport'];
// Parse calories
const caloriesStr = row['Calories'];
let calories: number | undefined;
if (caloriesStr && caloriesStr !== '--') {
const parsed = parseFloat(caloriesStr);
if (!isNaN(parsed) && parsed > 0) {
calories = parsed;
}
}
const activity: Activity = {
date,
activityType,
distance,
duration,
trainingStressScore,
calories,
};
return activity;
}).filter((activity): activity is Activity => activity !== null);
// Sort by date
activities.sort((a, b) => a.date.getTime() - b.date.getTime());
resolve(activities);
} catch (error) {
reject(new Error('Failed to parse CSV data'));
}
},
error: (error) => {
reject(new Error(`CSV parsing error: ${error.message}`));
},
});
});
}