Files
GuessTheRAL/app/ral.py
2026-10-26 21:56:23 +01:00

190 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import random
import re
from typing import Any
SALARY_PATTERNS = [
r"ral\s*(?:lorda|annua|annuale)?\s*[:\-]?\s*",
r"retribuzione\s*(?:annua\s*)?lorda\s*[:\-]?\s*",
r"stipendio\s*(?:annuo|lordo)?\s*[:\-]?\s*",
r"compensation\s*[:\-]?\s*",
r"salary\s*(?:range)?\s*[:\-]?\s*",
r"ctc\s*[:\-]?\s*",
r"package\s*[:\-]?\s*",
]
NUMBER = r"(?:€|eur|euro)?\s*(\d{1,3}(?:[.\s]\d{3})+|\d{2,6})\s*(?:€|eur|k|mila)?"
RANGE = (
r"(?:tra|from|between)?\s*"
+ NUMBER
+ r"\s*(?:-|–|—|/|e|and|to|a)\s*"
+ NUMBER
)
BANDS = [
(0, 25000, "Less than 25k"),
(25000, 30000, "Between 25k and 30k"),
(30000, 35000, "Between 30k and 35k"),
(35000, 40000, "Between 35k and 40k"),
(40000, 45000, "Between 40k and 45k"),
(45000, 50000, "Between 45k and 50k"),
(50000, 55000, "Between 50k and 55k"),
(55000, 60000, "Between 55k and 60k"),
(60000, 70000, "Between 60k and 70k"),
(70000, 80000, "Between 70k and 80k"),
(80000, 90000, "Between 80k and 90k"),
(90000, 100000, "Between 90k and 100k"),
(100000, 120000, "Between 100k and 120k"),
(120000, 150000, "Between 120k and 150k"),
(150000, 10**9, "Above 150k"),
]
def parse_amount(raw: str) -> int:
compact = raw.lower().replace(" ", "").replace("€", "").replace("eur", "")
multiplier = 1000 if compact.endswith("k") or compact.endswith("mila") else 1
compact = compact.replace("k", "").replace("mila", "").replace(".", "")
digits = re.sub(r"[^\d]", "", compact)
if not digits:
return 0
value = int(digits)
if multiplier == 1000 and value < 1000:
value *= 1000
if 1000 <= value < 10000 and not raw.lower().endswith(("k", "mila")):
# 35.000 style already handled; 35000 stays
pass
return value
def extract_salary(text: str) -> dict[str, int | None]:
blob = text.lower().replace("\xa0", " ")
ral_min = ral_max = ral_exact = None
for match in re.finditer(RANGE, blob, flags=re.I):
a, b = parse_amount(match.group(1)), parse_amount(match.group(2))
if 15000 <= a <= 400000 and 15000 <= b <= 400000:
ral_min, ral_max = min(a, b), max(a, b)
break
if ral_min is None:
labeled = re.search(
r"(?:ral|retribuzione|salary|stipendio|package)[^\n]{0,40}" + NUMBER,
blob,
flags=re.I,
)
if labeled:
value = parse_amount(labeled.group(1))
if 15000 <= value <= 400000:
ral_exact = value
if ral_min is None and ral_exact is None:
above = re.search(r"(?:above|oltre|maggiore di|>)\s*" + NUMBER, blob, flags=re.I)
if above:
value = parse_amount(above.group(1))
if 15000 <= value <= 400000:
ral_min = value
ral_max = value + 20000
less = re.search(r"(?:less than|fino a|inferiore a|<)\s*" + NUMBER, blob, flags=re.I)
if less and ral_min is None:
value = parse_amount(less.group(1))
if 15000 <= value <= 400000:
ral_min = max(15000, value - 15000)
ral_max = value
return {"ral_min": ral_min, "ral_max": ral_max, "ral_exact": ral_exact}
def hide_salary(text: str) -> str:
cleaned = text
replacements = [
RANGE,
r"(?:ral|retribuzione\s*annua\s*lorda|stipendio|salary|compensation|ctc)\s*[:\-]?\s*(?:€|eur|euro)?\s*\d[\d.\sKkmila€euro\-–toaeand/]{1,48}",
r"(?:€|eur)\s*\d{2,3}(?:[.\s]\d{3})+(?:\s*(?:-|–|to|a)\s*(?:€|eur)?\s*\d{2,3}(?:[.\s]\d{3})+)?",
r"\b\d{2,3}\s*k(?:\s*(?:-|–|to|a)\s*\d{2,3}\s*k)?\b",
r"\b\d{5,6}\s*(?:€|eur|euro)?\b",
]
for pattern in replacements:
cleaned = re.sub(pattern, "[RAL HIDDEN]", cleaned, flags=re.I)
cleaned = re.sub(r"(\[RAL HIDDEN\]\s*){2,}", "[RAL HIDDEN] ", cleaned)
return cleaned.strip()
def midpoint(job: Any) -> int:
if job.ral_exact:
return job.ral_exact
if job.ral_min and job.ral_max:
return (job.ral_min + job.ral_max) // 2
return job.ral_min or job.ral_max or 40000
def correct_band(value: int) -> tuple[int, int, str]:
for low, high, label in BANDS:
if low <= value < high:
return low, high, label
return BANDS[-1]
def generate_options(job: Any) -> tuple[list[dict], str]:
value = midpoint(job)
idx = next(i for i, (low, high, _) in enumerate(BANDS) if low <= value < high)
start = max(0, idx - 2)
end = min(len(BANDS), start + 4)
start = max(0, end - 4)
chosen = BANDS[start:end]
if len(chosen) < 4:
chosen = BANDS[:4]
options = []
correct = ""
for low, high, label in chosen:
if high >= 10**9:
key = f"above_{low}"
text = f"Above {low // 1000}k"
elif low == 0:
key = f"less_{high}"
text = f"Less than {high // 1000}k"
else:
key = f"between_{low}_{high}"
text = f"Between {low // 1000}k and {high // 1000}k"
is_correct = low <= value < high
if is_correct:
correct = key
options.append({"key": key, "label": text})
random.shuffle(options)
return options, correct
def option_payload(job: Any) -> str:
options, correct = generate_options(job)
return json.dumps({"options": options, "correct": correct})
def detect_work_type(text: str) -> str:
blob = text.lower()
if any(w in blob for w in ["full remote", "fully remote", "100% remote", "full-remote", "da remoto"]):
return "remote"
if any(w in blob for w in ["hybrid", "ibrido", "smart working"]):
return "hybrid"
if any(w in blob for w in ["on-site", "onsite", "in sede", "in office", "presenza"]):
return "onsite"
return "hybrid"
def detect_subject(title: str, description: str) -> str:
blob = f"{title} {description}".lower()
mapping = {
"technology": ["developer", "engineer", "software", "data", "devops", "it ", "python", "java", "cloud"],
"financial": ["bank", "finance", "fintech", "accounting", "auditor", "investment"],
"business": ["sales", "account manager", "business", "marketing", "product manager"],
"healthcare": ["nurse", "medical", "pharma", "clinic", "health"],
"legal": ["lawyer", "legal", "compliance", "avvocato"],
"hr": ["recruiter", "human resources", "hr ", "people"],
"design": ["designer", "ux", "ui", "graphic"],
}
for subject, words in mapping.items():
if any(w in blob for w in words):
return subject
return "other"