598 lines
19 KiB
Python
598 lines
19 KiB
Python
import datetime
|
||
import re
|
||
from typing import Any, Iterable, Optional
|
||
|
||
import requests
|
||
|
||
|
||
_CITY_ALIASES: dict[str, list[str]] = {
|
||
"北京": ["北京", "北京市", "beijing"],
|
||
"上海": ["上海", "上海市", "shanghai"],
|
||
"广州": ["广州", "广州市", "guangzhou"],
|
||
"深圳": ["深圳", "深圳市", "shenzhen"],
|
||
"杭州": ["杭州", "杭州市", "hangzhou"],
|
||
"成都": ["成都", "成都市", "chengdu"],
|
||
"三亚": ["三亚", "三亚市", "sanya"],
|
||
"京都": ["京都", "kyoto"],
|
||
}
|
||
|
||
|
||
def clean_text(text: str) -> str:
|
||
"""统一自 crossapp/tasks.py、crossapp2/tasks.py。"""
|
||
if not text:
|
||
return ""
|
||
# 去掉非单词字符、非中文(包括空格)
|
||
# \u4fdd\u7559 \w + CJK + \u5e38\u7528 emoji \u5757\uff1b\u5220\u9664\u6807\u70b9/\u5168\u534a\u89d2\u7b26\u53f7/\u7a7a\u683c\u3002
|
||
# \u4fdd\u7559 emoji \u662f\u4e3a\u4e86\u8ba9"\u590d\u5236 title"\u7c7b\u4efb\u52a1\u80fd\u5224\u5b9a Agent \u662f\u5426\u5b8c\u6574\u590d\u5236\u5e26 emoji \u7684\u6807\u9898
|
||
# (emoji \u662f\u6570\u636e\u8bed\u4e49\u7684\u4e00\u90e8\u5206\uff0c\u4e0d\u5e94\u88ab norm \u541e\u6389)\u3002
|
||
# U+1F300-1FAFF: \u4e3b emoji \u533a (Pictographs / Emoticons / Symbols Extended-A/B)
|
||
# U+2600-27BF: Misc Symbols + Dingbats (\u2600 \u2728 \u2713 \u2b50 etc.)
|
||
text = re.sub(
|
||
r"[^\w\u4e00-\u9fff\U0001F300-\U0001FAFF\u2600-\u27bf]",
|
||
"",
|
||
text,
|
||
).replace("_", "").strip()
|
||
return text
|
||
|
||
|
||
def norm(s: str) -> str:
|
||
"""统一自 crossapp/tasks.py、crossapp2/tasks.py、spotify/tasks.py。"""
|
||
return clean_text(str(s or "")).lower()
|
||
|
||
|
||
# ── Traditional → Simplified Chinese mapping (opencc) ─────────────────
|
||
_t2s_converter = None
|
||
|
||
def _get_t2s():
|
||
global _t2s_converter
|
||
if _t2s_converter is None:
|
||
try:
|
||
import opencc
|
||
except ModuleNotFoundError as e:
|
||
raise ModuleNotFoundError(
|
||
"Missing dependency 'opencc-python-reimplemented'. Install:\n"
|
||
" pip install opencc-python-reimplemented"
|
||
) from e
|
||
_t2s_converter = opencc.OpenCC('t2s')
|
||
return _t2s_converter
|
||
|
||
|
||
def to_simplified(s: str) -> str:
|
||
"""Convert traditional Chinese to simplified via opencc t2s."""
|
||
if not s:
|
||
return s
|
||
return _get_t2s().convert(s)
|
||
|
||
|
||
def to_float(v: Any) -> Optional[float]:
|
||
"""统一自 weather/tasks.py、crossapp/tasks.py、crossapp2/tasks.py。"""
|
||
try:
|
||
return float(str(v).strip())
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def extract_numbers(text: str) -> list[float]:
|
||
"""统一自 crossapp3/tasks.py、spe_tasks/tasks.py、ebay/tasks.py。"""
|
||
# 仅把 ASCII 单词字符视作数字边界,允许“余额是100.23元”这类中文包裹的数值被识别。
|
||
nums = re.findall(
|
||
r"(?<![0-9A-Za-z_.])(-?(?:\d{1,3}(?:[,,]\d{3})+|\d+)(?:\.\d+)?)(?![0-9A-Za-z_.])",
|
||
text or "",
|
||
)
|
||
out: list[float] = []
|
||
for n in nums:
|
||
try:
|
||
out.append(float(n.replace(",", "").replace(",", "")))
|
||
except Exception:
|
||
continue
|
||
return out
|
||
|
||
|
||
def city_aliases(city_name: str) -> list[str]:
|
||
"""返回城市名的多种匹配变体(包含本身)。"""
|
||
key = city_name.strip()
|
||
if key in _CITY_ALIASES:
|
||
return _CITY_ALIASES[key]
|
||
lowered = key.lower()
|
||
for canonical, aliases in _CITY_ALIASES.items():
|
||
if lowered in aliases or lowered == canonical.lower():
|
||
return aliases
|
||
return [key]
|
||
|
||
|
||
def amount_labels(value: float) -> list[str]:
|
||
"""生成金额的多种文本表示,用于子串匹配。"""
|
||
rounded = round(float(value), 2)
|
||
labels = {
|
||
f"{rounded:.2f}",
|
||
f"{rounded:.1f}",
|
||
f"{rounded:g}",
|
||
str(int(rounded)) if float(rounded).is_integer() else f"{rounded:g}",
|
||
}
|
||
return [label for label in labels if label]
|
||
|
||
|
||
_CN_DIGITS = "零一二三四五六七八九"
|
||
_CN_SMALL_UNITS = ["", "十", "百", "千"]
|
||
_CN_GROUP_UNITS = ["", "万", "亿", "兆"]
|
||
|
||
|
||
def _int_to_chinese_group(value: int) -> str:
|
||
if value == 0:
|
||
return "零"
|
||
|
||
parts: list[str] = []
|
||
zero_pending = False
|
||
digits = f"{value:04d}"
|
||
for index, ch in enumerate(digits):
|
||
digit = int(ch)
|
||
unit_index = 3 - index
|
||
if digit == 0:
|
||
if parts:
|
||
zero_pending = True
|
||
continue
|
||
if zero_pending:
|
||
parts.append("零")
|
||
zero_pending = False
|
||
parts.append(_CN_DIGITS[digit])
|
||
parts.append(_CN_SMALL_UNITS[unit_index])
|
||
return "".join(parts) or "零"
|
||
|
||
|
||
def int_to_chinese(value: int) -> str:
|
||
"""把整数转换为中文数字,如 33 -> 三十三,36650 -> 三万六千六百五十。"""
|
||
number = int(value)
|
||
if number == 0:
|
||
return "零"
|
||
if number < 0:
|
||
return f"负{int_to_chinese(-number)}"
|
||
|
||
groups: list[int] = []
|
||
while number > 0:
|
||
groups.append(number % 10000)
|
||
number //= 10000
|
||
|
||
parts: list[str] = []
|
||
zero_between = False
|
||
for group_index in range(len(groups) - 1, -1, -1):
|
||
group = groups[group_index]
|
||
if group == 0:
|
||
zero_between = bool(parts)
|
||
continue
|
||
if parts and (zero_between or group < 1000):
|
||
parts.append("零")
|
||
parts.append(_int_to_chinese_group(group))
|
||
parts.append(_CN_GROUP_UNITS[group_index])
|
||
zero_between = False
|
||
|
||
result = "".join(parts)
|
||
if result.startswith("一十"):
|
||
return result[1:]
|
||
return result
|
||
|
||
|
||
def integer_labels(value: int) -> list[str]:
|
||
"""生成整数的多种文本表示,用于 judge 匹配。"""
|
||
number = int(value)
|
||
labels = {
|
||
str(number),
|
||
f"{number:,}",
|
||
f"{number:,}".replace(",", ","),
|
||
int_to_chinese(number),
|
||
}
|
||
return [label for label in labels if label]
|
||
|
||
|
||
def count_titles_in_text(text: str, titles: Iterable[str]) -> int:
|
||
"""统计 titles 中有多少个标题出现在 text 中。"""
|
||
return sum(1 for title in titles if title and title in text)
|
||
|
||
|
||
def parse_distance_to_meters(distance_text: Any) -> Optional[float]:
|
||
"""统一自 crossapp/tasks.py、crossapp3/tasks.py。"""
|
||
s = str(distance_text or "").strip().lower()
|
||
if not s:
|
||
return None
|
||
m = re.search(r"(\d+(?:\.\d+)?)", s)
|
||
if not m:
|
||
return None
|
||
try:
|
||
v = float(m.group(1))
|
||
except Exception:
|
||
return None
|
||
if "mi" in s or "mile" in s:
|
||
return v * 1609.344
|
||
if "km" in s or "公里" in s:
|
||
return v * 1000.0
|
||
if "m" in s or "米" in s:
|
||
return v
|
||
return v
|
||
|
||
|
||
def parse_duration_to_minutes(duration_text: Any) -> Optional[float]:
|
||
"""统一自 crossapp/tasks.py。"""
|
||
s = str(duration_text or "").strip().lower()
|
||
if not s:
|
||
return None
|
||
nums = re.findall(r"(\d+(?:\.\d+)?)", s)
|
||
if not nums:
|
||
return None
|
||
vals: list[float] = []
|
||
for x in nums:
|
||
try:
|
||
vals.append(float(x))
|
||
except Exception:
|
||
continue
|
||
if not vals:
|
||
return None
|
||
if "小时" in s or "hr" in s or "hour" in s:
|
||
if len(vals) >= 2 and ("分钟" in s or "min" in s):
|
||
return vals[0] * 60.0 + vals[1]
|
||
return vals[0] * 60.0
|
||
return vals[0]
|
||
|
||
|
||
def parse_duration_to_seconds(duration_text: Any) -> Optional[int]:
|
||
"""Parse a human-readable duration string to total seconds.
|
||
|
||
Recognises Chinese (小时/分钟/秒) and English (h/hr/min/sec/…) units.
|
||
Returns None when the text is empty or contains no recognisable units.
|
||
"""
|
||
text = str(duration_text or "").strip()
|
||
if not text:
|
||
return None
|
||
total = 0
|
||
for pattern, multiplier in (
|
||
(r"(\d+(?:\.\d+)?)\s*(?:小时|hr|hours?|h)", 3600),
|
||
(r"(\d+(?:\.\d+)?)\s*(?:分钟|mins?|minutes?|m)", 60),
|
||
(r"(\d+(?:\.\d+)?)\s*(?:秒|secs?|seconds?|s)", 1),
|
||
):
|
||
m = re.search(pattern, text, re.I)
|
||
if m:
|
||
total += int(float(m.group(1)) * multiplier)
|
||
return total or None
|
||
|
||
|
||
def now_ms(os_state: dict) -> int:
|
||
"""从 os_state 中提取模拟时间戳(毫秒)。禁止 fallback 到本地时间。"""
|
||
t = (os_state or {}).get("time")
|
||
if isinstance(t, (int, float)):
|
||
return int(t)
|
||
if isinstance(t, dict):
|
||
ts = t.get("timestamp")
|
||
if isinstance(ts, (int, float)):
|
||
return int(ts)
|
||
raise ValueError(
|
||
"os_state 中缺少 time 或 time.timestamp 字段,无法获取模拟时间。"
|
||
" 请确保 bench_env 正确读取了 __SIM__.getState() 的 os.time。"
|
||
)
|
||
|
||
|
||
def sim_today(os_state: dict) -> datetime.date:
|
||
"""从 os_state 中提取模拟日期。禁止 fallback 到本地时间。"""
|
||
return datetime.date.fromtimestamp(now_ms(os_state) / 1000.0)
|
||
|
||
|
||
def sim_datetime(os_state: dict) -> datetime.datetime:
|
||
"""从 os_state 中提取模拟 datetime。禁止 fallback 到本地时间。"""
|
||
return datetime.datetime.fromtimestamp(now_ms(os_state) / 1000.0)
|
||
|
||
|
||
def today_ymd(os_state: dict) -> str:
|
||
"""统一自 spe_tasks/tasks.py。"""
|
||
d = sim_today(os_state)
|
||
return f"{d.year:04d}-{d.month:02d}-{d.day:02d}"
|
||
|
||
|
||
def tomorrow_ymd(os_state: dict) -> str:
|
||
"""统一自 spe_tasks/tasks.py。"""
|
||
d = sim_today(os_state) + datetime.timedelta(days=1)
|
||
return f"{d.year:04d}-{d.month:02d}-{d.day:02d}"
|
||
|
||
|
||
def default_tomorrow() -> str:
|
||
"""Callable default: 基于真实时间的明天 ISO 日期,用于 parameters default。"""
|
||
return (datetime.date.today() + datetime.timedelta(days=1)).isoformat()
|
||
|
||
|
||
def sample_future_date(env_state: dict, rng) -> str:
|
||
"""Callable sampler: 基于模拟时间采样未来 1-13 天的 ISO 日期。"""
|
||
base = sim_today((env_state.get("os") or {}))
|
||
offset = rng.choice(range(1, 14))
|
||
return (base + datetime.timedelta(days=offset)).isoformat()
|
||
|
||
|
||
_WEEKDAY_ZH = ["一", "二", "三", "四", "五", "六", "日"]
|
||
|
||
|
||
def format_date_natural(value: str, env_state: dict) -> str:
|
||
"""Display formatter: 明天/后天/这周X/下周X/X月X号(基于模拟时间)。"""
|
||
parts = str(value).split("-")
|
||
if len(parts) != 3:
|
||
return str(value)
|
||
target = datetime.date(int(parts[0]), int(parts[1]), int(parts[2]))
|
||
today = sim_today(env_state.get("os") or {})
|
||
delta = (target - today).days
|
||
if delta == 1:
|
||
return "明天"
|
||
if delta == 2:
|
||
return "后天"
|
||
target_wd = target.weekday()
|
||
today_wd = today.weekday()
|
||
days_to_next_monday = 7 - today_wd
|
||
if 0 < delta <= (6 - today_wd):
|
||
return f"这周{_WEEKDAY_ZH[target_wd]}"
|
||
if days_to_next_monday <= delta <= days_to_next_monday + 6:
|
||
return f"下周{_WEEKDAY_ZH[target_wd]}"
|
||
return f"{target.month}月{target.day}号"
|
||
|
||
|
||
def normalize_price(value: float) -> int | float:
|
||
"""统一金额显示:保留两位小数,整数则返回 int。"""
|
||
rounded = round(float(value), 2)
|
||
return int(rounded) if rounded == int(rounded) else rounded
|
||
|
||
|
||
def day_bounds_ms(d: datetime.date) -> tuple[int, int]:
|
||
"""统一自 crossapp3/tasks.py。"""
|
||
start = datetime.datetime(d.year, d.month, d.day, 0, 0, 0)
|
||
end = start + datetime.timedelta(days=1)
|
||
return int(start.timestamp() * 1000), int(end.timestamp() * 1000)
|
||
|
||
|
||
def day_time_ms(d: datetime.date, time_text: str = "00:00") -> int:
|
||
"""返回指定模拟本地日期和 HH:MM 时间对应的毫秒时间戳。"""
|
||
hour, minute = [int(part) for part in time_text.split(":")]
|
||
dt = datetime.datetime(d.year, d.month, d.day, hour, minute)
|
||
return int(dt.timestamp() * 1000)
|
||
|
||
|
||
def opened_app_since_init(os_state: dict, os_init_state: dict, app_id: str) -> bool:
|
||
"""Check whether *app_id* was opened after task start."""
|
||
current_running = os_state["runningApps"]
|
||
initial_running = os_init_state["runningApps"]
|
||
return app_id in current_running and app_id not in initial_running
|
||
|
||
|
||
def norm_city_key(s: Any) -> str:
|
||
"""统一自 weather/tasks.py、crossapp/tasks.py、crossapp2/tasks.py。"""
|
||
s = str(s or "").strip().lower()
|
||
s = re.sub(r"\s+", "", s)
|
||
if s.endswith("市"):
|
||
s = s[:-1]
|
||
return s
|
||
|
||
|
||
def parse_date(value: str) -> datetime.date | None:
|
||
"""统一自 crossapp3/tasks.py。"""
|
||
text = str(value or "").strip()
|
||
if not text:
|
||
return None
|
||
try:
|
||
return datetime.date.fromisoformat(text)
|
||
except Exception:
|
||
pass
|
||
matched = re.search(r"(\d{4})\D+(\d{1,2})\D+(\d{1,2})", text)
|
||
if not matched:
|
||
return None
|
||
try:
|
||
return datetime.date(
|
||
int(matched.group(1)),
|
||
int(matched.group(2)),
|
||
int(matched.group(3)),
|
||
)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
_RELATIVE_DAY_NAMES = {0: "今天", 1: "明天", 2: "后天", 3: "大后天"}
|
||
|
||
|
||
def date_match_labels(
|
||
date_value: str,
|
||
os_state: dict | None = None,
|
||
) -> list[str]:
|
||
"""Generate all plausible textual representations of a date for fuzzy matching.
|
||
|
||
Returns labels in order: ISO, M月D日, M月D号, D日, D号, 周X, 星期X,
|
||
plus relative labels (今天/明天/后天/大后天/这周X/下周X) when *os_state* is provided.
|
||
"""
|
||
parsed = parse_date(date_value)
|
||
if parsed is None:
|
||
return [str(date_value)]
|
||
|
||
wd_zh = _WEEKDAY_ZH[parsed.weekday()]
|
||
labels = [
|
||
parsed.isoformat(),
|
||
f"{parsed.month}月{parsed.day}日",
|
||
f"{parsed.month}月{parsed.day}号",
|
||
f"{parsed.day}日",
|
||
f"{parsed.day}号",
|
||
f"周{wd_zh}",
|
||
f"星期{wd_zh}",
|
||
]
|
||
|
||
if os_state is not None:
|
||
try:
|
||
today = sim_today(os_state)
|
||
delta = (parsed - today).days
|
||
rel = _RELATIVE_DAY_NAMES.get(delta)
|
||
if rel is not None:
|
||
labels.append(rel)
|
||
# 这周X / 下周X(与 format_date_natural 逻辑对齐)
|
||
if delta > 0:
|
||
today_wd = today.weekday()
|
||
if delta <= (6 - today_wd):
|
||
labels.append(f"这周{wd_zh}")
|
||
else:
|
||
days_to_next_monday = 7 - today_wd
|
||
if days_to_next_monday <= delta <= days_to_next_monday + 6:
|
||
labels.append(f"下周{wd_zh}")
|
||
except ValueError:
|
||
pass
|
||
|
||
return labels
|
||
|
||
|
||
def format_fixed(value: float, decimal_places: int) -> str:
|
||
"""统一自 crossapp3/tasks.py。"""
|
||
decimal_places = max(0, min(10, int(decimal_places)))
|
||
return f"{value:.{decimal_places}f}"
|
||
|
||
|
||
def extract_urls(text: str) -> list[str]:
|
||
"""统一自 crossapp3/tasks.py。"""
|
||
return [
|
||
url.strip().rstrip(").,,。")
|
||
for url in re.findall(r"https?://\S+", text or "")
|
||
]
|
||
|
||
|
||
def basename(path: Any) -> str:
|
||
"""统一自 crossapp3/tasks.py。"""
|
||
text = str(path or "").strip()
|
||
if not text:
|
||
return ""
|
||
text = text.split("?", 1)[0].split("#", 1)[0].replace("\\", "/")
|
||
return text.rsplit("/", 1)[-1]
|
||
|
||
|
||
def strip_postal_suffix(text: str) -> str:
|
||
"""统一自 crossapp/tasks.py、crossapp2/tasks.py。"""
|
||
raw = str(text or "").strip()
|
||
matched = re.search(r"(邮政编码|邮编)\s*[::]?", raw)
|
||
if matched:
|
||
raw = raw[: matched.start()].strip()
|
||
# Strip Plus Code suffix (e.g. "275X+67P", "W9CW+7PQ")
|
||
raw = re.sub(r"\s*[A-Z0-9]{2,6}\+[A-Z0-9]{2,4}\s*$", "", raw)
|
||
return raw
|
||
|
||
|
||
def parse_hhmm(value: str) -> int | None:
|
||
"""统一自 spe_tasks/tasks.py。"""
|
||
matched = re.fullmatch(r"(\d{1,2}):(\d{2})", str(value or "").strip())
|
||
if not matched:
|
||
return None
|
||
hour = int(matched.group(1))
|
||
minute = int(matched.group(2))
|
||
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||
return None
|
||
return hour * 60 + minute
|
||
|
||
|
||
def format_hhmm(hour: int, minute: int) -> str:
|
||
"""格式化小时和分钟为 HH:MM 字符串。"""
|
||
return f"{int(hour):02d}:{int(minute):02d}"
|
||
|
||
|
||
def new_items_by_id(
|
||
current: list[dict[str, Any]], initial: list[dict[str, Any]]
|
||
) -> list[dict[str, Any]]:
|
||
"""统一自 spe_tasks/tasks.py。"""
|
||
initial_ids = {str((item or {}).get("id", "")) for item in (initial or [])}
|
||
result: list[dict[str, Any]] = []
|
||
for item in current or []:
|
||
item_id = str((item or {}).get("id", ""))
|
||
if item_id and item_id not in initial_ids:
|
||
result.append(item)
|
||
result.sort(key=lambda item: int((item or {}).get("timestamp", 0) or 0))
|
||
return result
|
||
|
||
|
||
def numbers_contain_all(
|
||
haystack: list[float], expected: list[float], tol: float = 0.02
|
||
) -> bool:
|
||
"""统一自 spe_tasks/tasks.py。"""
|
||
for value in expected:
|
||
if not any(abs(float(item) - float(value)) <= tol for item in haystack):
|
||
return False
|
||
return True
|
||
|
||
|
||
def subsequence_contains_numbers(
|
||
numbers: list[float], expected: list[float], tol: float = 0.02
|
||
) -> bool:
|
||
"""统一自 spe_tasks/tasks.py。"""
|
||
if not expected:
|
||
return True
|
||
index = 0
|
||
for number in numbers:
|
||
if abs(float(number) - float(expected[index])) <= tol:
|
||
index += 1
|
||
if index >= len(expected):
|
||
return True
|
||
return False
|
||
|
||
|
||
def has_close_number(text: str, expected: float, tol: float = 0.01) -> bool:
|
||
"""统一自 ebay/tasks.py。"""
|
||
for number in extract_numbers(text):
|
||
if abs(number - expected) <= tol:
|
||
return True
|
||
return False
|
||
|
||
|
||
def has_int(text: str, expected: int) -> bool:
|
||
"""统一自 ebay/tasks.py。"""
|
||
ints = re.findall(r"(?<!\d)(\d+)(?!\d)", text)
|
||
return any(int(value) == expected for value in ints)
|
||
|
||
|
||
def url_content_contains(url: str, keyword: str) -> bool:
|
||
"""统一自 crossapp3/tasks.py。"""
|
||
url = str(url or "").strip()
|
||
keyword = str(keyword or "").strip()
|
||
if not url or not keyword:
|
||
return False
|
||
try:
|
||
response = requests.get(
|
||
url,
|
||
timeout=10,
|
||
headers={
|
||
"User-Agent": (
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||
"Chrome/91.0.4472.124 Safari/537.36"
|
||
),
|
||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||
},
|
||
)
|
||
if not getattr(response, "ok", False):
|
||
return False
|
||
if response.encoding == "ISO-8859-1":
|
||
response.encoding = response.apparent_encoding
|
||
body = str(getattr(response, "text", "") or "")
|
||
return keyword in body or norm(keyword) in norm(body)
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def check_alternatives(
|
||
*check_arrays: list[dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
"""多组按位置对应的 check 结果,返回第一组全通过的;都不过返回第一组。
|
||
|
||
每个数组长度相同,同一下标的元素属于同一个候选。
|
||
示例::
|
||
|
||
places = Map.resolve_places("故宫")
|
||
return check_alternatives(
|
||
[wechat.check_sent(addr(p), ...) for p in places],
|
||
[wechat.check_sent(weather(p), ...) for p in places],
|
||
)
|
||
"""
|
||
if not check_arrays:
|
||
return []
|
||
lengths = [len(arr) for arr in check_arrays]
|
||
if any(length == 0 for length in lengths):
|
||
raise ValueError("check_alternatives got empty candidate list")
|
||
if len(set(lengths)) != 1:
|
||
raise ValueError(f"check_alternatives got mismatched lengths: {lengths}")
|
||
n = lengths[0]
|
||
for i in range(n):
|
||
group = [arr[i] for arr in check_arrays]
|
||
if all(c.get("passed") for c in group):
|
||
return group
|
||
return [arr[0] for arr in check_arrays]
|