"""Parser for the station daily-Tx text files (data/obs/obs_stn_daily/Tx_*.txt). File format (see docs/DATA_INVENTORY.md): line 1: STATION_NAME line 2: lon lat elevation_m region_code(1-7) then, repeating per year: 31 rows: ... # day x month matrix, degC Missing / non-existent day = -99.0. Values are daily maximum temperature in degC. """ from __future__ import annotations import os, glob, re import numpy as np import pandas as pd MISSING = -99.0 _YEAR_RE = re.compile(r"^\s*(19|20)\d{2}\s*$") def load_station(path: str) -> dict: """Return {name, lon, lat, elev, region, series(pd.Series daily Tx, DatetimeIndex)}.""" with open(path) as f: lines = [ln.rstrip("\n") for ln in f] name = lines[0].strip() hdr = lines[1].split() lon, lat, elev = float(hdr[0]), float(hdr[1]), float(hdr[2]) region = int(float(hdr[3])) if len(hdr) > 3 else -1 dates, vals = [], [] i, n = 2, len(lines) while i < n: if _YEAR_RE.match(lines[i]): year = int(lines[i].strip()); i += 1 for _ in range(31): if i >= n or _YEAR_RE.match(lines[i]): break parts = lines[i].split() i += 1 if len(parts) < 13: continue day = int(float(parts[0])) month_vals = [float(x) for x in parts[1:13]] for month, v in enumerate(month_vals, start=1): if v == MISSING: continue try: d = pd.Timestamp(year=year, month=month, day=day) except ValueError: continue # e.g. 31 Feb padding dates.append(d); vals.append(v) else: i += 1 s = pd.Series(vals, index=pd.DatetimeIndex(dates), name="tx").sort_index() s = s[~s.index.duplicated(keep="first")] return dict(name=name, lon=lon, lat=lat, elev=elev, region=region, series=s) def list_tx_stations(station_dir: str, prefix: str = "Tx_") -> list[str]: return sorted(glob.glob(os.path.join(station_dir, f"{prefix}*.txt"))) def station_metadata(station_dir: str, prefix: str = "Tx_") -> pd.DataFrame: rows = [] for p in list_tx_stations(station_dir, prefix): st = load_station(p) s = st["series"] rows.append(dict(name=st["name"], file=os.path.basename(p), lon=st["lon"], lat=st["lat"], elev=st["elev"], region=st["region"], start=None if s.empty else s.index.min().date(), end=None if s.empty else s.index.max().date(), n_days=int(s.size), frac_missing_in_span=None if s.empty else round( 1 - s.size / (( s.index.max() - s.index.min()).days + 1), 3))) return pd.DataFrame(rows)