signal-bot/signal_bot/stats/text.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

89 lines
2.9 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Polish number agreement, and the small text pieces the renderers share.
Polish nouns take three forms after a numeral, not two. "1 znak", "2 znaki",
"5 znaków" -- and the rule is on the last *two* digits, so 12, 13 and 14 take
the many-form while 22, 23 and 24 do not. Getting this wrong is the kind of
thing the group notices immediately and mentions every time.
"""
from __future__ import annotations
from typing import Final
# Numbers the group reacts to whenever one comes up, and what it says. They
# go on counts -- messages, records, days, people -- and never on a share:
# a 69% share of voice is a rounding artefact, not something anybody did.
# The parenthesised photo aside in the day block stays bare too, because
# "(69 (nice) 📷)" nests brackets and reads as a typo.
FUNNY: Final[dict[int, str]] = {
67: "\U0001f937",
69: "(nice)",
314: "\U0001f967",
420: "\U0001f33f",
1410: "\u2694\ufe0f",
1916: "\U0001f1f5\U0001f1f1",
2137: "\U0001f64f",
}
TEEN_START: Final = 12
TEEN_END: Final = 14
FEW_START: Final = 2
FEW_END: Final = 4
def plural(count: int, one: str, few: str, many: str) -> str:
"""Pick the Polish form that agrees with a count.
Args:
count: The number the noun follows.
one: The form for exactly 1 ("znak").
few: The form for 2-4 and most numbers ending 2-4 ("znaki").
many: Every other case, including the 12-14 teens ("znaków").
Returns:
The bare noun, without the number.
"""
if count == 1:
return one
if TEEN_START <= count % 100 <= TEEN_END:
return many
if FEW_START <= count % 10 <= FEW_END:
return few
return many
def marked(count: int) -> str:
"""The count as printed: bare, or with its marker when the group has one.
The marker sticks to the number ("69 (nice) wiadomości"), so the same
count reads the same whether a noun follows it or not.
"""
marker = FUNNY.get(count)
return f"{count} {marker}" if marker else str(count)
def messages(count: int) -> str:
"""The count and its noun, agreeing: e.g. 3 wiadomości."""
return f"{marked(count)} {plural(count, 'wiadomość', 'wiadomości', 'wiadomości')}"
def characters(count: int) -> str:
"""The count and its noun, agreeing: e.g. 44 znaki."""
return f"{count} {plural(count, 'znak', 'znaki', 'znaków')}"
def commands(count: int) -> str:
"""The count and its noun, agreeing: e.g. 4 komendy."""
return f"{marked(count)} {plural(count, 'komenda', 'komendy', 'komend')}"
def people(count: int) -> str:
"""The count and its noun, agreeing: e.g. 2 osoby."""
return f"{marked(count)} {plural(count, 'osoba', 'osoby', 'osób')}"
def days(count: int) -> str:
"""The count and its noun, agreeing: e.g. 3 dni."""
return f"{marked(count)} {plural(count, 'dzień', 'dni', 'dni')}"