拆分 analysis_service:时钟工具和概率桶函数移至 analysis_utils.py

抽取 clock_minutes/format_clock_minutes/next_observation_clock 和 bucket_label/top_probability_bucket/add_signal 到独立模块。analysis_service 2152→2080 行。
This commit is contained in:
2569718930@qq.com
2026-05-25 17:40:50 +08:00
parent c82c33ec53
commit 812a4b2d32
2 changed files with 102 additions and 78 deletions
+8 -78
View File
@@ -39,6 +39,14 @@ from web.services.observation_freshness import (
build_observation_freshness as _build_observation_freshness,
observation_age_min as _observation_age_min,
)
from web.services.analysis_utils import (
add_signal as _add_signal,
bucket_label as _bucket_label,
bucket_label_from_value as _bucket_label_from_value,
format_clock_minutes as _format_clock_minutes,
next_observation_clock as _next_observation_clock,
top_probability_bucket as _top_probability_bucket,
)
from web.services.analysis_signals import (
_build_deviation_monitor,
_build_taf_signal,
@@ -305,84 +313,6 @@ def _maybe_enrich_dynamic_commentary_with_groq(
def _clock_minutes(value: Any) -> Optional[int]:
text = str(value or "").strip()
match = re.search(r"\b(\d{1,2}):(\d{2})\b", text)
if not match:
return None
hour = int(match.group(1))
minute = int(match.group(2))
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
return None
return hour * 60 + minute
def _format_clock_minutes(value: int) -> str:
value = max(0, min(23 * 60 + 59, int(value)))
return f"{value // 60:02d}:{value % 60:02d}"
def _next_observation_clock(local_time: Any) -> str:
minutes = _clock_minutes(local_time)
if minutes is None:
return "--"
next_slot = ((minutes // 30) + 1) * 30
if next_slot > 23 * 60 + 59:
return "23:59"
return _format_clock_minutes(next_slot)
def _bucket_label_from_value(value: Optional[float], unit: str) -> Optional[str]:
if value is None:
return None
try:
return f"{int(round(float(value)))}{unit or '°C'}"
except Exception:
return None
def _top_probability_bucket(distribution: Any) -> Optional[Dict[str, Any]]:
if not isinstance(distribution, list):
return None
candidates = [row for row in distribution if isinstance(row, dict)]
if not candidates:
return None
return max(candidates, key=lambda row: _sf(row.get("probability")) or -1.0)
def _bucket_label(row: Optional[Dict[str, Any]], unit: str) -> Optional[str]:
if not isinstance(row, dict):
return None
for key in ("label", "bucket", "range"):
raw = str(row.get(key) or "").strip()
if raw:
return raw
return _bucket_label_from_value(_sf(row.get("value")), unit)
def _add_signal(
signals: list,
*,
label: str,
direction: str,
strength: str,
summary: str,
label_en: Optional[str] = None,
summary_en: Optional[str] = None,
) -> None:
signals.append(
{
"label": label,
"label_en": label_en or label,
"direction": direction,
"strength": strength,
"summary": summary,
"summary_en": summary_en or summary,
}
)
def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]:
"""Build a paid-product intraday meteorology read from existing layers."""
current = data.get("current") or {}
+94
View File
@@ -0,0 +1,94 @@
"""Analysis utility functions extracted from analysis_service.py.
Pure helpers: clock arithmetic, bucket labelling, signal packaging.
"""
from __future__ import annotations
import re
from typing import Any, Dict, Optional
from web.core import _sf
# ── Clock / time-slot helpers ──────────────────────────────────────────
def clock_minutes(value: Any) -> Optional[int]:
text = str(value or "").strip()
match = re.search(r"\b(\d{1,2}):(\d{2})\b", text)
if not match:
return None
hour = int(match.group(1))
minute = int(match.group(2))
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
return None
return hour * 60 + minute
def format_clock_minutes(value: int) -> str:
value = max(0, min(23 * 60 + 59, int(value)))
return f"{value // 60:02d}:{value % 60:02d}"
def next_observation_clock(local_time: Any) -> str:
minutes = clock_minutes(local_time)
if minutes is None:
return "--"
next_slot = ((minutes // 30) + 1) * 30
if next_slot > 23 * 60 + 59:
return "23:59"
return format_clock_minutes(next_slot)
# ── Probability bucket helpers ─────────────────────────────────────────
def bucket_label_from_value(value: Optional[float], unit: str) -> Optional[str]:
if value is None:
return None
try:
return f"{int(round(float(value)))}{unit or '°C'}"
except Exception:
return None
def top_probability_bucket(distribution: Any) -> Optional[Dict[str, Any]]:
if not isinstance(distribution, list):
return None
candidates = [row for row in distribution if isinstance(row, dict)]
if not candidates:
return None
return max(candidates, key=lambda row: _sf(row.get("probability")) or -1.0)
def bucket_label(row: Optional[Dict[str, Any]], unit: str) -> Optional[str]:
if not isinstance(row, dict):
return None
for key in ("label", "bucket", "range"):
raw = str(row.get(key) or "").strip()
if raw:
return raw
return bucket_label_from_value(_sf(row.get("value")), unit)
# ── Signal packaging ───────────────────────────────────────────────────
def add_signal(
signals: list,
*,
label: str,
direction: str,
strength: str,
summary: str,
label_en: Optional[str] = None,
summary_en: Optional[str] = None,
) -> None:
signals.append(
{
"label": label,
"label_en": label_en or label,
"direction": direction,
"strength": strength,
"summary": summary,
"summary_en": summary_en or summary,
}
)