/** * CSV Parsing Utilities * Functions for parsing and validating CSV files (RK9 player registrations) */ import { EXPECTED_CSV_HEADERS, CSV_HEADERS } from './constants.js'; /** * Validate CSV headers against expected format * @param {string[]} headers - Array of header names from CSV * @throws {Error} If headers are invalid or missing required fields */ export function validateCsvHeaders(headers) { if (!headers || headers.length === 0) { throw new Error('CSV file is missing headers'); } if (headers.length !== EXPECTED_CSV_HEADERS.length) { throw new Error( `Invalid CSV file headers: Expected ${EXPECTED_CSV_HEADERS.length} headers but found ${headers.length}` ); } const missingHeaders = EXPECTED_CSV_HEADERS.filter( expectedHeader => !headers.includes(expectedHeader) ); if (missingHeaders.length > 0) { throw new Error( `Invalid CSV file headers: Missing the following headers: ${missingHeaders.join(', ')}` ); } } /** * Parse CSV text content into structured player data * @param {string} csvData - Raw CSV file content * @returns {Object} Object keyed by screenname with player data * @throws {Error} If CSV format is invalid */ export function parseCsv(csvData) { const rows = csvData .split('\n') .map(row => row.split(',')) .filter(row => row.some(cell => cell.trim() !== '')); if (rows.length === 0) { throw new Error('CSV file is empty'); } const headers = rows[0].map(header => header.trim()); validateCsvHeaders(headers); // Validate row format for (let i = 1; i < rows.length; i++) { if (rows[i].length !== EXPECTED_CSV_HEADERS.length) { throw new Error(`Invalid row format at line ${i + 1}`); } } // Parse rows into objects return rows.slice(1).reduce((acc, row) => { const participant = {}; EXPECTED_CSV_HEADERS.forEach((header, idx) => { participant[header] = row[idx]?.trim(); }); acc[participant[CSV_HEADERS.SCREENNAME]] = participant; return acc; }, {}); } /** * Parse CSV file from browser File API * @param {File} file - File object from input[type=file] * @returns {Promise} Parsed player data */ export async function parsePlayerCsvFile(file) { if (!file) { throw new Error('No file provided'); } if (!file.name.endsWith('.csv')) { throw new Error('File must be a CSV file'); } const text = await file.text(); return parseCsv(text); } /** * Convert parsed CSV data to array format * @param {Object} csvObject - Object from parseCsv * @returns {Array} Array of player objects with screenname included */ export function csvObjectToArray(csvObject) { return Object.entries(csvObject).map(([screenname, data]) => ({ ...data, screenname })); } /** * Validate individual player data * @param {Object} player - Player data object * @returns {Object} Validation result {valid: boolean, errors: string[]} */ export function validatePlayerData(player) { const errors = []; if (!player[CSV_HEADERS.PLAYER_ID]) { errors.push('Missing player_id'); } if (!player[CSV_HEADERS.SCREENNAME]) { errors.push('Missing screenname'); } if (!player[CSV_HEADERS.DIVISION]) { errors.push('Missing division'); } const email = player[CSV_HEADERS.EMAIL]; if (email && !isValidEmail(email)) { errors.push('Invalid email format'); } return { valid: errors.length === 0, errors }; } /** * Simple email validation * @param {string} email - Email address to validate * @returns {boolean} True if email format is valid */ function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }