141 lines
3.6 KiB
JavaScript
141 lines
3.6 KiB
JavaScript
/**
|
|
* 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<Object>} 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);
|
|
}
|