#!/usr/bin/env python3 """ Validate submission CSV per challenge rules (sections 2–3). Row 1 = header. Rows 2–101 = exactly 100 data rows. CSV only. """ import csv import re import sys from pathlib import Path REQUIRED_HEADER = ["candidate_id", "rank", "score", "reasoning"] CANDIDATE_ID_PATTERN = re.compile(r"^CAND_[0-9]{7}$") DATA_ROW_START = 2 EXPECTED_DATA_ROWS = 100 def validate_submission(csv_path): errors = [] path = Path(csv_path) if path.suffix.lower() != ".csv": errors.append("Filename must use a .csv extension.") elif not path.stem: errors.append("Filename must be your registered participant ID (e.g. team_xxx.csv).") try: with open(path, "r", encoding="utf-8", newline="") as f: reader = csv.reader(f) try: header = next(reader) except StopIteration: errors.append("Row 1 must be the header row; file is empty.") return errors # Row 1: column names and their order come from this line only if header != REQUIRED_HEADER: errors.append( "Row 1 (header) must be exactly:\n" f" {','.join(REQUIRED_HEADER)}\n" f"Found:\n" f" {','.join(header)}" ) data_rows = [] for row in reader: if any(cell.strip() for cell in row): data_rows.append(row) except UnicodeDecodeError: errors.append("File must be UTF-8 encoded.") return errors except OSError as e: errors.append(f"Cannot read file: {e}") return errors n = len(data_rows) if n != EXPECTED_DATA_ROWS: errors.append( f"After the header (row 1), there must be exactly {EXPECTED_DATA_ROWS} " f"data rows (rows {DATA_ROW_START}–{DATA_ROW_START + EXPECTED_DATA_ROWS - 1}); " f"found {n}." ) seen_ids = set() seen_ranks = set() by_rank = [] for i, cells in enumerate(data_rows): row_num = DATA_ROW_START + i if len(cells) != len(REQUIRED_HEADER): errors.append( f"Row {row_num}: expected {len(REQUIRED_HEADER)} columns " f"({','.join(REQUIRED_HEADER)}), got {len(cells)}." ) continue row = dict(zip(REQUIRED_HEADER, cells)) cid = row["candidate_id"].strip() rank_s = row["rank"].strip() score_s = row["score"].strip() if not cid: errors.append(f"Row {row_num}: candidate_id is required.") elif not CANDIDATE_ID_PATTERN.match(cid): errors.append( f"Row {row_num}: candidate_id must be CAND_XXXXXXX (7 digits)." ) elif cid in seen_ids: errors.append(f"Row {row_num}: duplicate candidate_id '{cid}'.") else: seen_ids.add(cid) try: rank = int(rank_s) if str(rank) != rank_s: raise ValueError if not 1 <= rank <= 100: errors.append(f"Row {row_num}: rank must be between 1 and 100.") elif rank in seen_ranks: errors.append(f"Row {row_num}: duplicate rank {rank}.") else: seen_ranks.add(rank) except ValueError: errors.append(f"Row {row_num}: rank must be an integer (1–100).") rank = None try: score = float(score_s) except ValueError: errors.append(f"Row {row_num}: score must be a float.") score = None if rank is not None and score is not None and cid: by_rank.append((rank, score, cid)) missing = set(range(1, 101)) - seen_ranks if missing: errors.append( f"Each rank 1–100 must appear exactly once; missing: {sorted(missing)}" ) by_rank.sort(key=lambda x: x[0]) for i in range(len(by_rank) - 1): r1, s1, _ = by_rank[i] r2, s2, _ = by_rank[i + 1] if s1 < s2: errors.append( f"score must be non-increasing by rank: " f"rank {r1} ({s1}) < rank {r2} ({s2})." ) for i in range(len(by_rank) - 1): r1, s1, c1 = by_rank[i] r2, s2, c2 = by_rank[i + 1] if s1 == s2 and c1 > c2: errors.append( f"Equal scores at ranks {r1} and {r2}: " f"tie-break requires candidate_id ascending " f"({c1!r} > {c2!r})." ) return errors def main(): if len(sys.argv) != 2: print("Usage: python validate_submission.py .csv") sys.exit(1) errors = validate_submission(sys.argv[1]) if errors: print(f"Validation failed ({len(errors)} issue(s)):\n") for e in errors: print(f"- {e}") sys.exit(1) print("Submission is valid.") if __name__ == "__main__": main()