95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
"""
|
|
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)
|
|
|