Files
memory-infrastructure-palace/code/websites/pokedex.online/src/utilities/csv-utils.js

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);
}