deeep-sec's picture
feat: institutional campus placement eligibility & interview scheduling engine
788eab4 verified
Raw History Blame Contribute Delete
9.67 kB
"""
Regex patterns, dictionaries, synonyms, and parsing helpers for deterministic requirement extraction.
"""
from __future__ import annotations
import datetime
import re
from backend.app.domain.models import DriveMode, RoundType
# Canonical branch dictionary and equivalence mapping
BRANCH_ALIASES: dict[str, str] = {
"cse": "CSE",
"cs": "CSE",
"computer science": "CSE",
"computer science and engineering": "CSE",
"computer science & engineering": "CSE",
"it": "IT",
"information technology": "IT",
"data science": "Data Science",
"ds": "Data Science",
"cs - data science": "Data Science",
"cse (data science)": "Data Science",
"ai & ml": "AI & ML",
"aiml": "AI & ML",
"artificial intelligence": "AI & ML",
"artificial intelligence and machine learning": "AI & ML",
"ece": "ECE",
"electronics and communication": "ECE",
"electronics and communication engineering": "ECE",
"electronics & communication": "ECE",
"eee": "EEE",
"electrical and electronics": "EEE",
"electrical and electronics engineering": "EEE",
"electrical & electronics": "EEE",
"electrical": "EEE",
"mechanical": "Mechanical Engineering",
"mechanical engineering": "Mechanical Engineering",
"mech": "Mechanical Engineering",
"civil": "Civil Engineering",
"civil engineering": "Civil Engineering",
"chemical": "Chemical Engineering",
"chemical engineering": "Chemical Engineering",
"chem": "Chemical Engineering",
"biotechnology": "Biotechnology",
"biotech": "Biotechnology",
"bio tech": "Biotechnology",
}
CIRCUIT_BRANCHES = ["CSE", "IT", "ECE", "EEE"]
CS_ALLIED_BRANCHES = ["CSE", "IT", "Data Science", "AI & ML"]
COMPOSITE_BRANCH_PHRASES: list[tuple[re.Pattern[str], str]] = [
(re.compile(r"\bcomputer\s+science\s+(?:and|&)\s+engineering\b", re.IGNORECASE), "CSE"),
(re.compile(r"\belectronics\s+(?:and|&)\s+communication(?:\s+engineering)?\b", re.IGNORECASE), "ECE"),
(re.compile(r"\belectrical\s+(?:and|&)\s+electronics(?:\s+engineering)?\b", re.IGNORECASE), "EEE"),
(re.compile(r"\bdata\s+science\b", re.IGNORECASE), "Data Science"),
(re.compile(r"\bartificial\s+intelligence(?:\s+(?:and|&)\s+machine\s+learning)?\b", re.IGNORECASE), "AI & ML"),
(re.compile(r"\bai\s+(?:and|&)\s+ml\b", re.IGNORECASE), "AI & ML"),
(re.compile(r"\bmechanical\s+engineering\b", re.IGNORECASE), "Mechanical Engineering"),
(re.compile(r"\bcivil\s+engineering\b", re.IGNORECASE), "Civil Engineering"),
(re.compile(r"\bchemical\s+engineering\b", re.IGNORECASE), "Chemical Engineering"),
]
def normalize_branch_token(token: str) -> str:
"""Normalizes an individual branch token to its canonical name."""
clean = token.strip().lower().rstrip(".,;")
# Strip common prefixes like 'b.tech -', 'be -' and trailing suffixes like 'only'
clean = re.sub(r"^(?:b\.?tech|b\.?e\.?|m\.?tech)\s*[-:]?\s*", "", clean).strip()
clean = re.sub(r"\b(?:only|students|candidates)\b", "", clean).strip()
return BRANCH_ALIASES.get(clean, token.strip())
def parse_branches_string(branch_str: str) -> list[str]:
"""Parses a raw branch specification into canonical branch list."""
clean_lower = branch_str.strip().lower()
if any(k in clean_lower for k in ("all branches", "any branch", "all disciplines", "open to all")):
return ["All Branches"]
if "circuit branches" in clean_lower or "all circuit" in clean_lower:
return list(CIRCUIT_BRANCHES)
if "cs-allied" in clean_lower or "cs allied" in clean_lower or "all cs" in clean_lower:
return list(CS_ALLIED_BRANCHES)
# Protect composite phrases before splitting
normalized_text = branch_str
for pattern, canonical in COMPOSITE_BRANCH_PHRASES:
normalized_text = pattern.sub(canonical, normalized_text)
# Split by comma, slash, vertical bar, or standalone 'and' / '&'
raw_tokens = re.split(r"[,/|]|(?:\s+\band\b\s+)|(?:\s*&\s*)", normalized_text)
result: list[str] = []
for token in raw_tokens:
tok_clean = token.strip()
if not tok_clean:
continue
norm = normalize_branch_token(tok_clean)
if norm and norm not in result:
result.append(norm)
return result if result else ["All Branches"]
# ---------------------------------------------------------
# Regex Pattern Definitions
# ---------------------------------------------------------
# CGPA Patterns
RE_CGPA_EXPLICIT = re.compile(
r"(?:cgpa(?:\s+cut-?off)?|minimum\s+cgpa)(?:\s+required)?\s*[:=]?\s*(?:of\s+|is\s+)?(?:>=|at\s+least|min)?\s*(\d+(?:\.\d+)?)\s*(?:or\s+above)?(?:\s*(?:in|for)\s*[A-Za-z.]+)?(?:\s*/\s*(\d+(?:\.\d+)?))?",
re.IGNORECASE,
)
RE_CGPA_INVERTED = re.compile(
r"(\d+(?:\.\d+)?)\s*(?:or\s+above)?\s*(?:cgpa|in\s+b\.?tech)",
re.IGNORECASE,
)
RE_CGPA_NO_CUTOFF = re.compile(
r"no\s+(?:cgpa\s+cut-?off|minimum\s+cgpa|cgpa\s+criteria)",
re.IGNORECASE,
)
# Percentage Patterns
RE_TENTH_AND_TWELFTH = re.compile(
r"(?:10th\s*(?:&|and)\s*12th|x\s*(?:&|and)\s*xii)\s*(?:percentage)?\s*[:=]?\s*(?:minimum\s+)?(\d+(?:\.\d+)?)\s*%",
re.IGNORECASE,
)
RE_TENTH_EXPLICIT = re.compile(
r"(?:10th|class\s+10|ssc|matriculation)\s*(?:percentage)?\s*[:=]?\s*(?:minimum\s+)?(\d+(?:\.\d+)?)\s*%",
re.IGNORECASE,
)
RE_TWELFTH_EXPLICIT = re.compile(
r"(?:12th|class\s+12|hsc|intermediate)\s*(?:percentage)?\s*[:=]?\s*(?:minimum\s+)?(\d+(?:\.\d+)?)\s*%",
re.IGNORECASE,
)
RE_THROUGHOUT_PERCENTAGE = re.compile(
r"(?:minimum\s+)?(\d+(?:\.\d+)?)\s*%\s*(?:throughout|in\s+academics\s+throughout|aggregate\s+throughout)",
re.IGNORECASE,
)
# Backlogs Patterns
RE_BACKLOG_LINE = re.compile(
r"(?:backlog\s+policy|backlogs?|standing\s+arrears?|arrears?)[ \t]*[:=]?[ \t]*([^\r\n]+)",
re.IGNORECASE,
)
# Gap Years Patterns
RE_GAP_YEARS = re.compile(
r"(?:education\s+gap|gap\s+years?|gap\s+limit)[ \t]*[:=]?[ \t]*([^\r\n]+)",
re.IGNORECASE,
)
RE_MAX_GAP_NUM = re.compile(
r"(?:maximum|max|up\s+to)?\s*(\d+)\s*(?:year[s]?|yr[s]?)\s+(?:education\s+)?gap",
re.IGNORECASE,
)
# CTC / Package Patterns
RE_CTC_LINE = re.compile(
r"(?:ctc(?:\s+offered)?|package|salary|stipend)[ \t]*[:=]?[ \t]*([^\r\n]+)",
re.IGNORECASE,
)
RE_CTC_LPA = re.compile(
r"(?:inr|rs\.?|₹)?\s*(\d+(?:\.\d+)?)\s*(?:lpa|lakhs?(?:\s+per\s+annum)?|lac)",
re.IGNORECASE,
)
RE_CTC_NUMERIC_INR = re.compile(
r"(?:inr|rs\.?|₹)\s*(\d{1,2}(?:,\d{2})*,\d{3})(?:\s*(?:per\s+annum|\/-\s*ctc))?",
re.IGNORECASE,
)
# Company, Date, Mode
RE_COMPANY = re.compile(
r"^(?:company(?:\s+name)?|client)[ \t]*[:=][ \t]*([^\r\n]+)",
re.IGNORECASE | re.MULTILINE,
)
RE_SUBJECT_COMPANY = re.compile(
r"Subject:\s*(?:Campus\s+Recruitment\s+(?:Announcement|Drive):\s*)?([^(\n]+?)(?:\s*\([^)]*\))?$",
re.IGNORECASE | re.MULTILINE,
)
RE_DRIVE_DATE = re.compile(
r"(?:drive\s+date|date\s+of\s+drive)[ \t]*[:=][ \t]*([0-9]{4}-[0-9]{2}-[0-9]{2}|\d{1,2}(?:st|nd|rd|th)?\s+[A-Za-z]+\s+\d{4})",
re.IGNORECASE,
)
RE_DRIVE_MODE = re.compile(
r"(?:mode(?:\s+of\s+drive)?)[ \t]*[:=][ \t]*([^\r\n]+)",
re.IGNORECASE,
)
# Degree & Batch
RE_DEGREE = re.compile(
r"(?:degree|program|course|qualification)[ \t]*[:=][ \t]*([^\r\n]+)",
re.IGNORECASE,
)
RE_BATCH = re.compile(
r"(?:batch\s+of\s+|batch\s*[:=]?\s*|graduating\s+in\s+)(\d{4})",
re.IGNORECASE,
)
# Selection Process & Rounds
RE_ROUND_LINE = re.compile(
r"Round\s*(\d+)[ \t]*[:\-][ \t]*([^\r\n]+)",
re.IGNORECASE,
)
RE_DURATION_MINS = re.compile(
r"(\d+)\s*(?:mins|min|minutes)",
re.IGNORECASE,
)
# Bond / Service Agreement
RE_BOND = re.compile(
r"(?:service\s+agreement|bond(?:\s+period)?)[ \t]*[:=][ \t]*([^\r\n]+)",
re.IGNORECASE,
)
def parse_date_string(date_str: str) -> datetime.date | None:
"""Parses ISO date or verbal date (e.g. '2026-11-01' or '1st Nov 2026')."""
clean = date_str.strip()
# Try ISO
try:
return datetime.date.fromisoformat(clean)
except ValueError:
pass
# Try common formats
clean_verbal = re.sub(r"(st|nd|rd|th)", "", clean)
for fmt in ("%d %b %Y", "%d %B %Y", "%Y/%m/%d", "%d-%m-%Y"):
try:
return datetime.datetime.strptime(clean_verbal, fmt).date()
except ValueError:
pass
return None
def parse_mode_string(mode_str: str) -> DriveMode:
"""Parses drive mode string into DriveMode enum."""
clean = mode_str.strip().lower()
if "hybrid" in clean:
return DriveMode.HYBRID
elif "virtual" in clean or "online" in clean or "remote" in clean:
return DriveMode.VIRTUAL
else:
return DriveMode.ON_CAMPUS
def classify_round_type(round_name: str) -> RoundType:
"""Classifies round text into canonical RoundType."""
rn = round_name.lower()
if any(k in rn for k in ("coding", "online test", "assessment", "cognitive", "written", "aptitude")):
return RoundType.ONLINE_TEST
elif any(k in rn for k in ("technical", "system design", "tech interview", "coding interview")):
return RoundType.TECHNICAL_INTERVIEW
elif any(k in rn for k in ("hr", "leadership", "behavioral", "cultural", "managerial")):
return RoundType.HR_INTERVIEW
elif any(k in rn for k in ("gd", "group discussion")):
return RoundType.GROUP_DISCUSSION
return RoundType.TECHNICAL_INTERVIEW