Spaces:
Sleeping
Sleeping
File size: 6,615 Bytes
f63ce21 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
/**
* CSV Validation utilities
* Checks for required fields and data quality
*/
export interface ValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}
export interface FieldValidation {
found: boolean;
fieldName?: string;
nonNullCount: number;
nonZeroCount: number;
totalRows: number;
}
/**
* Validate CSV structure and required fields
* @param data - Parsed CSV data array
* @returns Validation result with errors and warnings
*/
export function validateCSV(data: any[]): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!data || data.length === 0) {
errors.push('CSV file is empty or contains no valid data rows.');
return { isValid: false, errors, warnings };
}
const totalRows = data.length;
// Required fields validation
const dateField = validateField(data, ['Date', 'Activity Date', 'date']);
const timeField = validateField(data, ['Time', 'Duration', 'Moving Time', 'Elapsed Time']);
const activityTypeField = validateField(data, ['Activity Type', 'Type', 'Sport']);
// Check required fields
if (!dateField.found) {
errors.push('Missing required field: "Date". Expected column names: Date, Activity Date, or date.');
} else if (dateField.nonNullCount === 0) {
errors.push(`Field "${dateField.fieldName}" has no valid values (all rows are empty or "--").`);
} else if (dateField.nonNullCount < totalRows) {
warnings.push(`Field "${dateField.fieldName}": ${totalRows - dateField.nonNullCount} out of ${totalRows} rows have missing dates.`);
}
if (!timeField.found) {
errors.push('Missing required field: "Time" or "Duration". Expected column names: Time, Duration, Moving Time, or Elapsed Time.');
} else if (timeField.nonNullCount === 0) {
errors.push(`Field "${timeField.fieldName}" has no valid values (all rows are empty or "--").`);
} else if (timeField.nonNullCount < totalRows) {
warnings.push(`Field "${timeField.fieldName}": ${totalRows - timeField.nonNullCount} out of ${totalRows} rows have missing duration.`);
}
if (!activityTypeField.found) {
errors.push('Missing required field: "Activity Type". Expected column names: Activity Type, Type, or Sport.');
} else if (activityTypeField.nonNullCount === 0) {
errors.push(`Field "${activityTypeField.fieldName}" has no valid values (all rows are empty or "--").`);
}
// Optional but important fields validation
const distanceField = validateField(data, ['Distance', 'distance']);
const normalizedPowerField = validateFieldByPrefix(data, 'Normalized Power');
// Distance warnings
if (!distanceField.found) {
warnings.push('Optional field "Distance" is missing. Distance-based ACWR chart will be empty.');
} else if (distanceField.nonZeroCount === 0) {
warnings.push('Field "Distance" exists but all values are 0 or empty. Distance-based ACWR chart will be empty.');
} else if (distanceField.nonZeroCount < totalRows * 0.5) {
warnings.push(`Field "Distance": Only ${distanceField.nonZeroCount} out of ${totalRows} activities have distance values.`);
}
// Power/TSS warnings
const tssField = validateFieldByPrefix(data, 'Training Stress Score');
if (!normalizedPowerField.found && !tssField.found) {
warnings.push('Neither "Normalized Power" nor "Training Stress Score" fields found. TSS will be calculated using default FTP (343W). Ensure activities have Normalized Power data for accurate TSS calculation.');
} else if (normalizedPowerField.found && normalizedPowerField.nonZeroCount === 0) {
warnings.push('Field "Normalized Power" exists but all values are 0 or empty. TSS calculation may be limited.');
} else if (normalizedPowerField.found && normalizedPowerField.nonZeroCount < totalRows * 0.3) {
warnings.push(`Field "Normalized Power": Only ${normalizedPowerField.nonZeroCount} out of ${totalRows} activities have power data. TSS-based ACWR will have limited data.`);
}
// Summary
if (errors.length === 0 && warnings.length === 0) {
warnings.push(`✓ CSV validation passed. Successfully found ${totalRows} activities with all required fields.`);
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate a field by checking multiple possible column names
*/
function validateField(data: any[], possibleNames: string[]): FieldValidation {
for (const name of possibleNames) {
if (data[0] && name in data[0]) {
const nonNullCount = data.filter(row => {
const value = row[name];
return value && value !== '--' && value !== '';
}).length;
const nonZeroCount = data.filter(row => {
const value = row[name];
if (!value || value === '--' || value === '') return false;
const parsed = parseFloat(value);
return !isNaN(parsed) && parsed !== 0;
}).length;
return {
found: true,
fieldName: name,
nonNullCount,
nonZeroCount,
totalRows: data.length,
};
}
}
return {
found: false,
nonNullCount: 0,
nonZeroCount: 0,
totalRows: data.length,
};
}
/**
* Validate a field by checking if any column name starts with a prefix
*/
function validateFieldByPrefix(data: any[], prefix: string): FieldValidation {
if (!data[0]) {
return {
found: false,
nonNullCount: 0,
nonZeroCount: 0,
totalRows: data.length,
};
}
const fieldName = Object.keys(data[0]).find(key => key.startsWith(prefix));
if (!fieldName) {
return {
found: false,
nonNullCount: 0,
nonZeroCount: 0,
totalRows: data.length,
};
}
const nonNullCount = data.filter(row => {
const value = row[fieldName];
return value && value !== '--' && value !== '';
}).length;
const nonZeroCount = data.filter(row => {
const value = row[fieldName];
if (!value || value === '--' || value === '') return false;
const parsed = parseFloat(value);
return !isNaN(parsed) && parsed !== 0;
}).length;
return {
found: true,
fieldName,
nonNullCount,
nonZeroCount,
totalRows: data.length,
};
}
|