monish563/NU-KIOSK-API
0
1"""Helper utilities for normalising and searching catalog data."""2 3from __future__ import annotations4 5import re6from typing import Iterable, List7 8 9def canonicalize_name(raw: str | None) -> str:10 """Lowercase and strip punctuation/spaces for stable matching."""11 if not raw:12 return ""13 lowered = raw.strip().lower()14 cleaned = "".join(ch for ch in lowered if ch.isalnum() or ch.isspace())15 # Collapse duplicate spaces16 return " ".join(part for part in cleaned.split() if part)17 18 19def tokenize_name(raw: str | None) -> set[str]:20 """Break names into normalized token sets for fuzzy comparisons."""21 if not raw:22 return set()23 lowered = raw.lower()24 return set(re.findall(r"[a-z0-9]+", lowered))25 26 27def generate_name_variants(raw: str | None) -> Iterable[str]:28 """Yield common name permutations used across CSV sources."""29 if not raw:30 return []31 cleaned = raw.strip()32 yield cleaned33 if "," in cleaned:34 last, _, first = cleaned.partition(",")35 first = first.strip()36 last = last.strip()37 if first and last:38 yield f"{first} {last}"39 yield f"{last} {first}"40 yield f"{last}, {first}"41 yield f"{last},{first}"42 else:43 parts = cleaned.split()44 if len(parts) >= 2:45 first = " ".join(parts[:-1])46 last = parts[-1]47 yield f"{last} {first}"48 yield f"{last}, {first}"49 yield f"{last},{first}"50 51 52def extract_leadership_names(raw: str | None) -> List[str]:53 """54 Parse leadership strings from centers.csv and extract individual names.55 56 Examples:57 - "Director: Kristian Hammond"58 - "Co-directors: Michael Horn, Chris Riesbeck, Uri Wilensky"59 - "Director: Diego Klabjan; Associate Director: Lauren Smith"60 """61 if not raw:62 return []63 64 text = raw.replace("\xa0", " ").strip()65 # Discard role labels (e.g., "Director:", "Co-directors:")66 if ":" in text:67 _, _, text = text.partition(":")68 # Normalize coordinators69 text = text.replace(" and ", ",")70 # Remove role labels repeated later in the string71 text = re.sub(r"\b[A-Za-z ]*Director[s]?\b", "", text, flags=re.IGNORECASE)72 text = re.sub(r"\bCo-PI\b", "", text, flags=re.IGNORECASE)73 text = re.sub(r"\bAssociate\b", "", text, flags=re.IGNORECASE)74 75 # Remove parentheses content (e.g., titles)76 text = re.sub(r"\([^)]*\)", "", text)77 78 names = []79 for chunk in re.split(r"[,/;]+", text):80 cleaned = chunk.strip()81 if not cleaned:82 continue83 # Strip lingering prefixes like "Co-" or trailing descriptors84 cleaned = re.sub(r"^(co-)?director(s)?\b", "", cleaned, flags=re.IGNORECASE).strip()85 # Collapse internal multiple spaces86 cleaned = " ".join(cleaned.split())87 if cleaned:88 names.append(cleaned)89 return names90 91 92def centers_for_faculty(source_row: dict, centers: List[dict]) -> List[dict]:93 """Find centers led by the faculty member described in ``source_row``."""94 name = source_row.get("Name")95 if not name:96 return []97 lookup = canonicalize_name(name)98 matches: List[dict] = []99 for center in centers:100 leaders = extract_leadership_names(center.get("Leadership"))101 if not leaders:102 continue103 for leader in leaders:104 if canonicalize_name(leader) == lookup:105 matches.append(center)106 break107 return matches108 109 110def extract_advisor_names(raw: str | None) -> List[str]:111 """Normalize advisor lists from students.csv."""112 if not raw:113 return []114 text = raw.strip()115 if not text or text.lower() in {"none", "na", "n/a"}:116 return []117 118 names: List[str] = []119 120 # First capture "Last, First" patterns so we can rebuild full names.121 comma_matches = re.findall(r"([A-Za-z.'\- ]+),\s*([A-Za-z.'\- ]+)", text)122 for last, first in comma_matches:123 first = first.strip()124 last = last.strip()125 if first and last:126 names.append(f"{first} {last}")127 # Remove the matched patterns to avoid double counting when splitting later.128 text = re.sub(r"([A-Za-z.'\- ]+),\s*([A-Za-z.'\- ]+)", "", text)129 130 text = text.replace(" and ", ",")131 segments = re.split(r"[,/;]+", text)132 for segment in segments:133 cleaned = segment.strip().strip('"').strip()134 if not cleaned:135 continue136 cleaned = re.sub(r"\(.*?\)$", "", cleaned).strip()137 if cleaned:138 names.append(cleaned)139 return [name for name in names if name]140 