signal-bot/signal_bot/stats/render_period.py
Krzysztof kuhy Rudnicki c195e6b4c1
Some checks are pending
CI / check (push) Waiting to run
CI / web (push) Waiting to run
Funny numbers: 69 (nice), 2137 🙏 and five more on every count the stats print
Seven numbers the group finds funny get a marker wherever they show up as
a count: the nightly table, every !stats_* view, !ja, the records and the
highlights. Percentages and shares stay bare (a 69% share is not a 69), and
so does the (N 📷) aside, so one line never carries two jokes. One table,
FUNNY in stats/text.py, and one helper, marked(), so adding a number is one
line. Documented in DOCS-statistics.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 21:55:33 +02:00

86 lines
3.0 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Rendering a week, a month or a year.
Same rules as the day block: plain text, no padding, and the count separated
from the name by a dash rather than by spaces that only line up in a terminal.
Longer than the nightly post on purpose. These are asked for, not pushed, so
somebody who typed `!miesiąc` has already agreed to read a month.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Final
from signal_bot.stats import text
from signal_bot.stats.periods import pl_day_month
if TYPE_CHECKING:
from signal_bot.stats.periods import Period
from signal_bot.stats.queries_day import DayRow
from signal_bot.stats.queries_period import Trend
MEDALS: Final = ("\U0001f947", "\U0001f948", "\U0001f949")
CHART: Final = "\U0001f4ca"
BAR_WIDTH: Final = 10
MAX_ROWS: Final = 12
def bar(share: int) -> str:
"""A share of voice as a text bar. Blocks, because Signal has no charts."""
filled = round(share / 100 * BAR_WIDTH)
return "█" * filled + "░" * (BAR_WIDTH - filled)
def trend_line(trend: Trend, what: str) -> str:
"""One line comparing a period with the one before it.
Says "first one" rather than "0%" when there is no baseline: the group
should not be told a period is unchanged when there is nothing to have
changed from.
"""
if trend.percent is None:
return f"({what}: pierwszy taki okres w historii)"
if not trend.delta:
return f"({what}: dokładnie tyle samo co poprzednio)"
direction = "więcej" if trend.delta > 0 else "mniej"
return f"{trend.arrow} {abs(trend.percent)}% {direction} niż {what}"
def _row(row: DayRow, place: int, total: int, *, podium: bool) -> str:
"""One person's line, with a medal or a rank and a share bar."""
lead = MEDALS[place - 1] if podium and place <= len(MEDALS) else f"{place}."
share = round(row.messages / total * 100) if total else 0
return f"{lead} {row.display_name} — {text.marked(row.messages)} {bar(share)} {share}%"
def render_period(
period: Period,
rows: list[DayRow],
trend: Trend,
*,
since: str | None = None,
podium: bool = False,
) -> str:
"""Compose a period summary."""
total = sum(row.messages for row in rows)
media = sum(row.media for row in rows)
extra = f" ({media} \U0001f4f7)" if media else ""
lines = [f"{CHART} {period.label} — {text.messages(total)}{extra}"]
if not rows:
lines.append("Nikt nic nie napisał.")
return "\n".join(lines)
shown = rows[:MAX_ROWS]
lines.extend(_row(row, i + 1, total, podium=podium) for i, row in enumerate(shown))
if len(rows) > MAX_ROWS:
hidden = rows[MAX_ROWS:]
rest = sum(row.messages for row in hidden)
lines.append(f"…i {text.people(len(hidden))} więcej (razem {text.messages(rest)})")
lines.append(trend_line(trend, "poprzednio"))
if since:
lines.append(f"(liczymy od {pl_day_month(since)})")
return "\n".join(lines)