File size: 2,703 Bytes
1187856
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/** CSV/TXT parser for participantEmail + modelName batch score uploads. */
export interface ParsedCsvRow {
  participantEmail: string;
  modelName: string;
  scores: Record<string, number>;
}

export interface CsvParseResult {
  rows: ParsedCsvRow[];
  errors: string[];
  metricCodes: string[];
}

export function parseCsv(text: string, validMetricCodes: string[]): CsvParseResult {
  const errors: string[] = [];
  const rows: ParsedCsvRow[] = [];
  const lines = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n")
    .split("\n").filter((line) => line.trim());
  if (lines.length < 2) {
    return { rows, errors: ["The file must include a header row and at least one data row"], metricCodes: [] };
  }

  const delimiter = lines[0].includes("\t") ? "\t" : ",";
  const headers = lines[0].split(delimiter).map((header) => header.trim());
  const emailIndex = headers.findIndex((header) => header.toLowerCase() === "participantemail");
  const modelNameIndex = headers.findIndex((header) => header.toLowerCase() === "modelname");
  if (emailIndex < 0 || modelNameIndex < 0) {
    return { rows, errors: ["The header must include participantEmail and modelName"], metricCodes: [] };
  }

  const columns = new Map<number, string>();
  headers.forEach((header, index) => {
    if (index === emailIndex || index === modelNameIndex) return;
    const metric = validMetricCodes.find((code) => code.toLowerCase() === header.toLowerCase());
    if (metric) columns.set(index, metric);
    else errors.push(`Column ${index + 1} "${header}" is not a valid metric code and was skipped`);
  });
  if (!columns.size) {
    return { rows, errors: [...errors, "No valid metric code columns were found"], metricCodes: [] };
  }

  lines.slice(1).forEach((line, rowIndex) => {
    const cells = line.split(delimiter).map((cell) => cell.trim());
    const participantEmail = cells[emailIndex] || "";
    const modelName = cells[modelNameIndex] || "";
    if (!participantEmail || !modelName) {
      errors.push(`Row ${rowIndex + 2}: participantEmail and modelName are required`);
      return;
    }
    const scores: Record<string, number> = {};
    columns.forEach((metricCode, columnIndex) => {
      const raw = cells[columnIndex];
      if (!raw) return;
      const value = Number(raw);
      if (Number.isNaN(value)) errors.push(`Row ${rowIndex + 2}: ${metricCode} value "${raw}" is not numeric`);
      else scores[metricCode] = value;
    });
    if (Object.keys(scores).length) rows.push({ participantEmail, modelName, scores });
    else errors.push(`Row ${rowIndex + 2}: no valid scores were found`);
  });

  return { rows, errors, metricCodes: [...new Set(columns.values())] };
}