diff --git a/gen_cons.py b/gen_cons.py index aa08094..76d45fd 100644 --- a/gen_cons.py +++ b/gen_cons.py @@ -31,6 +31,44 @@ ROTA_START_DATE = "2026-08-03" sites = ("rota a", "rota b", "rota c") +def _normalize_worker_key(value): + text = str(value or "").strip() + if not text: + return "" + return re.sub(r"\s+", " ", text).lower() + + +def _validate_worker_alignment(base_workers, other_workers, base_label, other_label): + """Validate that two worker collections contain the same members. + + Comparison is case-insensitive and whitespace-normalized. + Raises ValueError with a clear diff when the sets do not match. + """ + base_clean = [str(w).strip() for w in base_workers if str(w).strip()] + other_clean = [str(w).strip() for w in other_workers if str(w).strip()] + + base_norm_to_raw = {_normalize_worker_key(w): w for w in base_clean} + other_norm_to_raw = {_normalize_worker_key(w): w for w in other_clean} + + base_norm = set(base_norm_to_raw.keys()) + other_norm = set(other_norm_to_raw.keys()) + + only_in_base_norm = sorted(base_norm - other_norm) + only_in_other_norm = sorted(other_norm - base_norm) + + if not only_in_base_norm and not only_in_other_norm: + return + + only_in_base = [base_norm_to_raw[k] for k in only_in_base_norm] + only_in_other = [other_norm_to_raw[k] for k in only_in_other_norm] + + raise ValueError( + "Worker mismatch detected between sources. " + f"{base_label} only: {only_in_base}. " + f"{other_label} only: {only_in_other}." + ) + + def _parse_sheet_date(value): if pd.isna(value): return None @@ -185,7 +223,7 @@ def extract_leave_and_rota_from_calender(calender_df): #print(leave_requests) - return leave_requests, work_requests#, rota_data + return leave_requests, work_requests, worker_names def extract_shift_assignments_from_rota(rota_df): """ @@ -319,11 +357,43 @@ def load_workers(): workers_days.append({"initial": initial, "name": name, "availability": availability, "requests": requests}) - leave_requests, work_requests = extract_leave_and_rota_from_calender(calender_df) + leave_requests, work_requests, calendar_workers = extract_leave_and_rota_from_calender(calender_df) assignments, assignments_by_worker = extract_shift_assignments_from_rota(rota_df) print(assignments) + # Validate cross-source worker matching before constructing Worker objects. + days_workers = [w["name"] for w in workers_days] + days_initials = [w["initial"] for w in workers_days] + + _validate_worker_alignment( + base_workers=days_workers, + other_workers=calendar_workers, + base_label="Days tab worker names", + other_label="Calendar tab worker names", + ) + + _validate_worker_alignment( + base_workers=days_initials, + other_workers=list(assignments_by_worker.keys()), + base_label="Days tab worker initials", + other_label="Rota tab assigned initials", + ) + + _validate_worker_alignment( + base_workers=days_initials, + other_workers=list(rota_b_assignments.keys()), + base_label="Days tab worker initials", + other_label="Rota B assignments initials", + ) + + _validate_worker_alignment( + base_workers=days_initials, + other_workers=list(priors_map.keys()), + base_label="Days tab worker initials", + other_label="Running totals worker initials", + ) + logger.debug("Using Running totals priors for oncall/twilight/weekend A only") workers = [] @@ -455,7 +525,7 @@ def main( weeks_to_rota=weeks, balance_offset_modifier=bom, use_previous_shifts=True, - name="cons rota 2026 july", + name="cons rota test", allow_force_assignment_with_leave_conflict=True, constraint_options=RotaConstraintOptions( balance_weekends=True,