94
filter_model_3.py
Normal file
94
filter_model_3.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user