diff --git a/src/utils/create-class.ts b/src/utils/create-class.ts index 11ee7fc..dfd221a 100644 --- a/src/utils/create-class.ts +++ b/src/utils/create-class.ts @@ -1,18 +1,68 @@ import { CSVRow, ProcessedClassData, StudentRecord } from "./types"; -export const processRawData = (rawData: CSVRow[]): ProcessedClassData => { - const processStudent = (row: CSVRow): StudentRecord => ({ - givenName: row["Given Name"] || "Unknown", - surname: row["Surname"] || "Student", - utorid: row["UTORid"] || row["Person ID"] || "Missing UTORid", - ...row, - }); +function splitCsvLine(line: string): string[] { + const fields: string[] = []; + let current = ""; + let inQuotes = false; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (inQuotes) { + if (ch === '"') { + if (line[i + 1] === '"') { + current += '"'; + i++; + } // escaped quote + else inQuotes = false; + } else { + current += ch; + } + } else { + if (ch === '"') { + inQuotes = true; + } else if (ch === ",") { + fields.push(current.trim()); + current = ""; + } else if (ch === "\r") { + /* skip */ + } else { + current += ch; + } + } + } + fields.push(current.trim()); + return fields; +} + +const findHeader = (headers: string[], name: string): string => + headers.find((h) => h.toLowerCase() === name.toLowerCase()) ?? name; + +export const processRawData = (rawData: CSVRow[], headers: string[] = []): ProcessedClassData => { + const utoridKey = findHeader(headers, "UTORid"); + const personIdKey = findHeader(headers, "Person ID"); + const givenKey = findHeader(headers, "Given Name"); + const surnameKey = findHeader(headers, "Surname"); + const acadKey = findHeader(headers, "Acad_act"); + + const isLoginLike = (s: string) => /[a-zA-Z]/.test(s); + + const processStudent = (row: CSVRow): StudentRecord => { + const rawUtorid = row[utoridKey] || ""; + const rawPersonId = row[personIdKey] || ""; + const utorid = rawUtorid || (isLoginLike(rawPersonId) ? rawPersonId : "") || "Missing UTORid"; + + return { + ...row, + givenName: row[givenKey] || "Unknown", + surname: row[surnameKey] || "Student", + utorid, + }; + }; const students = rawData .map(processStudent) .filter((s) => s.givenName !== "Unknown" && s.utorid !== "Missing UTORid"); - const firstRowCourseCode = rawData.length > 0 ? rawData[0]["Acad_act"] || "" : ""; + const firstRowCourseCode = rawData.length > 0 ? rawData[0][acadKey] || "" : ""; const extractedCourseCode = firstRowCourseCode ? firstRowCourseCode.split(",")[0].trim() : "Unknown Course"; @@ -28,23 +78,25 @@ export const parseAndProcessCSV = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { - const text = e.target?.result as string; + let text = e.target?.result as string; if (text) { - let lines = text.split("\n").filter((line) => line.trim() !== ""); - lines = lines.map((line) => line.replace(/\r/g, "")); + // Strip UTF-8 BOM if present (common in Excel exports) + if (text.startsWith("\uFEFF")) text = text.slice(1); + + const lines = text.split("\n").filter((line) => line.trim() !== ""); if (lines.length > 0) { - const headers = lines[0].split(","); + const headers = splitCsvLine(lines[0]); const data: CSVRow[] = lines.slice(1).map((line) => { - const values = line.split(","); + const values = splitCsvLine(line); const row: CSVRow = {}; - headers.forEach((headerElement, index) => { - row[headerElement.trim()] = values[index]?.trim() || ""; + headers.forEach((header, index) => { + row[header] = values[index] ?? ""; }); return row; }); - const processed = processRawData(data); + const processed = processRawData(data, headers); resolve(processed); } else { reject(new Error("CSV file is empty"));