""" filter_model_3.py OR-composed BallFilter: - A candidate ball is ACCEPTED if it passes EITHER filter_model_1 OR filter_model_2. - A candidate ball is REJECTED only if it fails BOTH. This keeps the same public interface used across the project: BallFilter(lottoHistoryFileName, ruleset_path=..., ruleset=...) .filter(ball, no, until_end=False, df=None, filter_ball=None) -> set[str] .extract_final_candidates(ball, no=None, until_end=False, df=None) -> set[str] Notes: - The underlying filters return a non-empty set of failure reasons when rejected. - Callers treat "len(result) == 0" as PASS. """ from __future__ import annotations from typing import Any, Dict, Optional import filter_model_1 as fm1 import filter_model_2 as fm2 class BallFilter: """ OR composition of filter_model_1.BallFilter and filter_model_2.BallFilter. - If model1 PASSES OR model2 PASSES -> return empty set() - If both FAIL -> return union of reasons (prefixed for debugging) """ def __init__( self, lottoHistoryFileName: Optional[str] = None, # Backward compatible single ruleset knobs (applied to both if specific ones not provided) ruleset_path: Optional[str] = None, ruleset: Optional[Dict[str, Any]] = None, # Optional per-model overrides ruleset_path_1: Optional[str] = None, ruleset_path_2: Optional[str] = None, ruleset_1: Optional[Dict[str, Any]] = None, ruleset_2: Optional[Dict[str, Any]] = None, ): rp1 = ruleset_path_1 if ruleset_path_1 is not None else ruleset_path rp2 = ruleset_path_2 if ruleset_path_2 is not None else ruleset_path r1 = ruleset_1 if ruleset_1 is not None else ruleset r2 = ruleset_2 if ruleset_2 is not None else ruleset self.m1 = fm1.BallFilter(lottoHistoryFileName, ruleset_path=rp1, ruleset=r1) self.m2 = fm2.BallFilter(lottoHistoryFileName, ruleset_path=rp2, ruleset=r2) # # Delegate common helper methods (both models expose the same API) # def getBall(self, no): return self.m1.getBall(no) def getLastNo(self, YMD): return self.m1.getLastNo(YMD) def getNextNo(self, YMD): return self.m1.getNextNo(YMD) def getYMD(self, no): return self.m1.getYMD(no) def _prefixed(self, prefix: str, reasons: set) -> set: # keep stable, readable debug strings return {f"{prefix}{r}" for r in reasons} def extract_final_candidates(self, ball, no=None, until_end: bool = False, df=None): """ OR-pass semantics: - If either model returns empty set -> PASS (return empty set) - Else -> FAIL (return union of reasons) """ r1 = self.m1.extract_final_candidates(ball=ball, no=no, until_end=until_end, df=df) if len(r1) == 0: return set() r2 = self.m2.extract_final_candidates(ball=ball, no=no, until_end=until_end, df=df) if len(r2) == 0: return set() # both failed return self._prefixed("m1:", set(r1)) | self._prefixed("m2:", set(r2)) def filter(self, ball, no, until_end: bool = False, df=None, filter_ball=None): """ Keep signature compatible with existing callers. - filter_ball is ignored here (callers typically pre-filter before calling .filter()). """ return self.extract_final_candidates(ball=ball, no=no, until_end=until_end, df=df)