"""
=============================================================================
CMU 17-630 · 共享小工具：console.py —— 在终端里打印对齐的表格、百分比和置信区间
=============================================================================

【为什么需要它】
    1. 对齐：f"{s:<10}" 按"字符个数"补空格，可一个汉字在终端里占 **2 个**英文字符的宽度，
       所以含中文的表格用普通 f-string 对齐会歪。pad() 按"显示宽度"补空格。
    2. 统计：这门课里几乎每个实验都是"在 N 道题上数对了几道"。只报一个准确率是不够的——
       100 道题上的 60% 和 65% 很可能没有区别。wilson() 给出准确率的 95% 置信区间，
       让你一眼看出两个数字的差别是不是"在噪声里"。（前置讲 P3 会系统地讲提示实验里的统计。）

【Python 小知识】
    unicodedata.east_asian_width(ch) 返回字符的"东亚宽度"类别：
        "W"（Wide）/ "F"（Fullwidth）：汉字、全角标点 → 占 2 格；其他 → 占 1 格。

【运行】python3 console.py   # 小自检：打印一张中英文混排的表和几个置信区间
【依赖】只用 Python 标准库。后面各讲都会 import 这个文件。
"""

import math
import unicodedata


def display_width(s):
    """字符串在终端里占几格：汉字和全角符号算 2，其他算 1。"""
    return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in str(s))


def pad(s, width, align="<"):
    """
    把字符串 s 补空格到显示宽度 width。

    【参数】s：任意对象（先转成字符串）；width：目标显示宽度；align："<" 左对齐，">" 右对齐
    【返回】补好空格的字符串
    【边界】s 本身比 width 宽时原样返回（不截断）：表格最多"不齐"，不会丢信息。
    """
    s = str(s)
    gap = max(0, width - display_width(s))
    return s + " " * gap if align == "<" else " " * gap + s


def row(cells, widths, aligns=None):
    """
    把一行单元格拼成一个字符串。

    【参数】cells：单元格列表；widths：每列显示宽度；aligns：每列对齐方式（默认第一列左对齐，其余右对齐）
    【返回】拼好的一行（列之间两个空格）
    【边界】cells 和 widths 长度不同时以较短者为准（这是 zip 的行为）。
    """
    if aligns is None:
        aligns = ["<"] + [">"] * (len(widths) - 1)
    return "  ".join(pad(c, w, a) for c, w, a in zip(cells, widths, aligns))


def print_table(headers, rows, widths=None, aligns=None):
    """
    打印一整张表：表头、一条分隔线、然后每一行。

    【参数】
        headers：表头字符串列表
        rows   ：二维列表，每个元素是一行单元格（会自动转成字符串）
        widths ：每列宽度；None 表示自动取"表头和所有单元格里最宽的那个"
        aligns ：每列对齐方式，同 row()
    【边界】rows 为空时只打印表头和分隔线。
    【Python 小知识】[str(c) for c in r] 是"列表推导式"，等价于一个 for 循环把每个元素转成字符串
        放进新列表；外面再套一层 [... for r in rows]，就是对每一行都做一遍。
    """
    rows = [[str(c) for c in r] for r in rows]
    if widths is None:
        widths = []
        for j, h in enumerate(headers):
            col = [display_width(h)] + [display_width(r[j]) for r in rows if j < len(r)]
            widths.append(max(col))
    print(row(headers, widths, aligns))
    print("-" * (sum(widths) + 2 * (len(widths) - 1)))
    for r in rows:
        print(row(r, widths, aligns))


def print_header(title):
    """打印一个醒目的分隔标题（用来分开同一个脚本里的不同演示）。"""
    print("\n" + "=" * 96)
    print(title)
    print("=" * 96)


def print_observations(lines):
    """
    打印"观察 Observations"：每个演示最后用几句话总结应该从表格里看到什么。

    【参数】lines：字符串列表，每个元素是一条观察
    """
    print("\n观察 Observations：")
    for i, s in enumerate(lines, 1):
        print(f"  {i}. {s}")


def pct(k, n, digits=1):
    """
    把"k / n"写成百分比字符串，例如 pct(57, 200) → "28.5%"。

    【参数】k：成功次数；n：总次数；digits：小数位数
    【返回】字符串
    【边界】n == 0 时返回 "-"（没有数据就不要假装有一个百分比）。
    【Python 小知识】f"{x:.1f}" 表示保留 1 位小数；f"{x:.{digits}f}" 是把位数也放进花括号里。
    """
    if n == 0:
        return "-"
    return f"{100.0 * k / n:.{digits}f}%"


def wilson(k, n, z=1.96):
    """
    Wilson 置信区间：在 n 次独立试验里成功 k 次，真实成功率 p 的 95% 置信区间。

    【思路】最朴素的区间是 p̂ ± 1.96·sqrt(p̂(1−p̂)/n)（"Wald 区间"），但当 p̂ 接近 0 或 1、
        或 n 很小时它很糟（例如 0/10 会给出 [0, 0]，好像"绝对不可能成功"）。Wilson 区间
        把公式反过来解，结果总在 [0, 1] 里，小样本时也靠谱。公式：
            center = (p̂ + z²/2n) / (1 + z²/n)
            half   = z · sqrt(p̂(1−p̂)/n + z²/4n²) / (1 + z²/n)
    【参数】k：成功次数；n：试验次数；z：正态分位数（1.96 对应 95%）
    【返回】(low, high)，都是 0–1 之间的小数
    【边界】n == 0 时返回 (0.0, 1.0)：没有数据，什么都可能。
    【提示工程联系】评估一个提示时，题目通常只有几十到几百道，区间宽度常常有 ±5–10 个百分点。
        两个提示的区间大幅重叠时，不要急着说谁更好（第 2 讲、第 17 讲会反复用到）。
    """
    if n == 0:
        return 0.0, 1.0
    p = k / n
    denom = 1.0 + z * z / n
    center = (p + z * z / (2 * n)) / denom
    half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
    return max(0.0, center - half), min(1.0, center + half)


def pct_ci(k, n):
    """
    把准确率和 95% 置信区间写在一起，例如 pct_ci(57, 200) → "28.5% [22.7, 35.1]"。

    【参数】k：成功次数；n：总次数
    【返回】字符串；n == 0 时返回 "-"
    """
    if n == 0:
        return "-"
    lo, hi = wilson(k, n)
    return f"{100.0 * k / n:.1f}% [{100 * lo:.1f}, {100 * hi:.1f}]"


if __name__ == "__main__":
    print_table(
        ["提示 prompt", "对了 / 总数", "准确率 [95% CI]"],
        [
            ["提示 A", "60 / 100", pct_ci(60, 100)],
            ["提示 B", "65 / 100", pct_ci(65, 100)],
            ["小样本 C", "3 / 10", pct_ci(3, 10)],
            ["全错 D", "0 / 10", pct_ci(0, 10)],
        ],
    )
    print_observations([
        "A 和 B 差了 5 个百分点，但两个区间大幅重叠：100 道题不足以分出高下。",
        "10 道题时区间宽达几十个百分点；0/10 的上界也不是 0。",
    ])
