Funny numbers: 69 (nice), 2137 🙏 and five more on every count the stats print
Some checks are pending
CI / check (push) Waiting to run
CI / web (push) Waiting to run

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>
This commit is contained in:
Krzysztof Rudnicki 2026-09-04 21:55:33 +02:00
parent e3c7bbfe23
commit c195e6b4c1
10 changed files with 119 additions and 12 deletions

View File

@ -50,6 +50,13 @@ Only people whose display name has been seen are named. Somebody who has never
written is counted, not named — the group listing knows them only as a raw written is counted, not named — the group listing knows them only as a raw
uuid or a phone number, and printing either into a chat is a leak. uuid or a phone number, and printing either into a chat is a leak.
Seven numbers are funny to this group and get a marker wherever they appear
as a *count* — `69 (nice)`, `2137 🙏`, `1410 ⚔️`, `1916 🇵🇱`, `420 🌿`,
`314 🥧`, `67 🤷` — in the nightly report, every `!stats_*` and `!ja`, and the
highlights. Percentages, shares and the `(N 📷)` aside stay bare: a 69% share
is not a 69, and marking the photo count next to an already-marked message
count would be two jokes on one line. The table is `FUNNY` in `stats/text.py`.
## Outages ## Outages
Nothing is ever deleted, and the last day reported is recorded per group. Nothing is ever deleted, and the last day reported is recorded per group.

View File

@ -64,13 +64,13 @@ def personal_record(facts: DayFacts) -> Highlight | None:
) )
if previous is None: if previous is None:
continue continue
beaten = f" (poprzedni {previous.value}, {pl_day_month(previous.day)})" beaten = f" (poprzedni {text.marked(previous.value)}, {pl_day_month(previous.day)})"
return Highlight( return Highlight(
kind="personal_record", kind="personal_record",
family=RECORD, family=RECORD,
priority=100, priority=100,
magnitude=ratio(best.value, previous.value), magnitude=ratio(best.value, previous.value),
text=f"{TROPHY} {row.display_name} {best.value} — rekord życiowy{beaten}", text=f"{TROPHY} {row.display_name} {text.marked(best.value)} — rekord życiowy{beaten}",
) )
return None return None
@ -100,7 +100,7 @@ def media_record(facts: DayFacts) -> Highlight | None:
family=RECORD, family=RECORD,
priority=90, priority=90,
magnitude=float(record.value), magnitude=float(record.value),
text=f"{CAMERA} rekord zdjęć: {record.value} w jeden dzień", text=f"{CAMERA} rekord zdjęć: {text.marked(record.value)} w jeden dzień",
) )

View File

@ -37,7 +37,7 @@ def _media(count: int) -> str:
def person_line(row: DayRow, place: int) -> str: def person_line(row: DayRow, place: int) -> str:
"""One person's line: a crown for first place, otherwise a rank.""" """One person's line: a crown for first place, otherwise a rank."""
lead = CROWN if place == 1 else f"{place}." lead = CROWN if place == 1 else f"{place}."
return f"{lead} {row.display_name} — {row.messages}{_media(row.media)}" return f"{lead} {row.display_name} — {text.marked(row.messages)}{_media(row.media)}"
def silence_line(rows: list[DayRow], unnamed: int) -> str: def silence_line(rows: list[DayRow], unnamed: int) -> str:

View File

@ -64,7 +64,7 @@ def _record_line(label: str, record: DayRecord | None, unit: str) -> str | None:
"""One "label: value (date)" line, or None when there is no record yet.""" """One "label: value (date)" line, or None when there is no record yet."""
if record is None: if record is None:
return None return None
return f"{label}: {record.value} {unit} ({pl_day_month(record.day)})" return f"{label}: {text.marked(record.value)} {unit} ({pl_day_month(record.day)})"
def render_records( def render_records(
@ -91,7 +91,8 @@ def render_records(
lines.append("") lines.append("")
lines.append("Rekordy życiowe:") lines.append("Rekordy życiowe:")
lines.extend( lines.extend(
f" {name} — {record.value} ({pl_day_month(record.day)})" for name, record in personal f" {name} — {text.marked(record.value)} ({pl_day_month(record.day)})"
for name, record in personal
) )
return "\n".join(lines) if len(lines) > 1 else nothing_yet("records", since) return "\n".join(lines) if len(lines) > 1 else nothing_yet("records", since)
@ -161,7 +162,9 @@ def render_me(
"""One person's own numbers.""" """One person's own numbers."""
lines = [ME_TITLE, "", f"{name} — {text.messages(total)} w historii"] lines = [ME_TITLE, "", f"{name} — {text.messages(total)} w historii"]
if best: if best:
lines.append(f"{TROPHY} Rekord życiowy: {best.value} ({pl_day_month(best.day)})") lines.append(
f"{TROPHY} Rekord życiowy: {text.marked(best.value)} ({pl_day_month(best.day)})"
)
lines.append(f"Udział w rozmowie: {share}%") lines.append(f"Udział w rozmowie: {share}%")
if favourite_hour is not None: if favourite_hour is not None:
lines.append( lines.append(

View File

@ -51,7 +51,7 @@ 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.""" """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}." lead = MEDALS[place - 1] if podium and place <= len(MEDALS) else f"{place}."
share = round(row.messages / total * 100) if total else 0 share = round(row.messages / total * 100) if total else 0
return f"{lead} {row.display_name} — {row.messages} {bar(share)} {share}%" return f"{lead} {row.display_name} — {text.marked(row.messages)} {bar(share)} {share}%"
def render_period( def render_period(

View File

@ -11,6 +11,21 @@ from __future__ import annotations
from typing import Final 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_START: Final = 12
TEEN_END: Final = 14 TEEN_END: Final = 14
FEW_START: Final = 2 FEW_START: Final = 2
@ -38,9 +53,19 @@ def plural(count: int, one: str, few: str, many: str) -> str:
return many 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: def messages(count: int) -> str:
"""The count and its noun, agreeing: e.g. 3 wiadomości.""" """The count and its noun, agreeing: e.g. 3 wiadomości."""
return f"{count} {plural(count, 'wiadomość', 'wiadomości', 'wiadomości')}" return f"{marked(count)} {plural(count, 'wiadomość', 'wiadomości', 'wiadomości')}"
def characters(count: int) -> str: def characters(count: int) -> str:
@ -50,14 +75,14 @@ def characters(count: int) -> str:
def commands(count: int) -> str: def commands(count: int) -> str:
"""The count and its noun, agreeing: e.g. 4 komendy.""" """The count and its noun, agreeing: e.g. 4 komendy."""
return f"{count} {plural(count, 'komenda', 'komendy', 'komend')}" return f"{marked(count)} {plural(count, 'komenda', 'komendy', 'komend')}"
def people(count: int) -> str: def people(count: int) -> str:
"""The count and its noun, agreeing: e.g. 2 osoby.""" """The count and its noun, agreeing: e.g. 2 osoby."""
return f"{count} {plural(count, 'osoba', 'osoby', 'osób')}" return f"{marked(count)} {plural(count, 'osoba', 'osoby', 'osób')}"
def days(count: int) -> str: def days(count: int) -> str:
"""The count and its noun, agreeing: e.g. 3 dni.""" """The count and its noun, agreeing: e.g. 3 dni."""
return f"{count} {plural(count, 'dzień', 'dni', 'dni')}" return f"{marked(count)} {plural(count, 'dzień', 'dni', 'dni')}"

View File

@ -226,3 +226,12 @@ def test_nothing_to_say_is_an_empty_list() -> None:
def test_ratio_survives_a_zero_baseline() -> None: def test_ratio_survives_a_zero_baseline() -> None:
assert ratio(5, 0) == 5.0 assert ratio(5, 0) == 5.0
assert ratio(10, 5) == 1.0 assert ratio(10, 5) == 1.0
def test_a_record_that_lands_on_a_funny_number_carries_its_marker(store: Store) -> None:
"""The count is what is funny; the marker rides on it wherever the day's notice prints it."""
wrote(store, "2026-09-01", ALA, 69)
wrote(store, "2026-09-02", ALA, 2137)
found = curiosities.personal_record(facts_for(store, "2026-09-02"))
assert found is not None
assert "Ala 2137 🙏 — rekord życiowy (poprzedni 69 (nice), 1 września)" in found.text

View File

@ -120,3 +120,11 @@ def test_a_custom_heading_replaces_the_date() -> None:
day = rows([("kuhy", 1, 0)]) day = rows([("kuhy", 1, 0)])
rendered = render_day("2026-09-01", day, day_totals(day), DayView(heading="Dziś, na razie")) rendered = render_day("2026-09-01", day, day_totals(day), DayView(heading="Dziś, na razie"))
assert rendered.startswith("Dziś, na razie\n") assert rendered.startswith("Dziś, na razie\n")
def test_a_funny_count_is_marked_on_the_person_and_the_total_but_not_the_photos() -> None:
"""The (N 📷) aside stays bare; 67 photos is a count too, but marking it doubles the noise."""
day = rows([("Ala", 69, 67), ("kuhy", 2, 0)])
rendered = render_day("2026-09-01", day, day_totals(day))
assert "👑 Ala — 69 (nice) (67 📷)" in rendered
assert rendered.startswith("📊 Wtorek, 1 września — 71 wiadomości (67 📷)")

View File

@ -127,3 +127,27 @@ def test_me_reports_everything_it_does_know() -> None:
assert "Rekord życiowy: 12 (1 września)" in rendered assert "Rekord życiowy: 12 (1 września)" in rendered
assert "Najczęściej piszesz między 23:00 a 00:00" in rendered assert "Najczęściej piszesz między 23:00 a 00:00" in rendered
assert "Korona: 2 dni z rzędu" in rendered assert "Korona: 2 dni z rzędu" in rendered
def test_funny_counts_are_marked_wherever_a_count_is_printed() -> None:
"""The number carries its marker in a row, a record and the me block alike.
Shares are left alone: the 69% here is the rounding of 2137 / 3106, not
something anybody did.
"""
period = render_period(WEEK, rows([("Ala", 2137), ("kuhy", 969)]), Trend(0, 0))
assert "Ala — 2137 🙏 " in period
assert "3106 wiadomości" in period
assert "69%" in period
assert "69 (nice)%" not in period
records = render_records(
{"2026-09-01": 420}, None, None, [("Ala", DayRecord("2026-09-01", 1410))]
)
assert "Najwięcej w jeden dzień: 420 🌿 wiadomości" in records
assert "Ala — 1410 ⚔️ (1 września)" in records
me = render_me("kuhy", 1916, DayRecord("2026-09-01", 314), 40, None, 67)
assert "kuhy — 1916 🇵🇱 wiadomości w historii" in me
assert "Rekord życiowy: 314 🥧 (1 września)" in me
assert "Korona: 67 🤷 dni z rzędu" in me

View File

@ -58,3 +58,34 @@ def test_people_agree(count: int, expected: str) -> None:
@pytest.mark.parametrize(("count", "expected"), [(1, "1 dzień"), (2, "2 dni"), (9, "9 dni")]) @pytest.mark.parametrize(("count", "expected"), [(1, "1 dzień"), (2, "2 dni"), (9, "9 dni")])
def test_days_agree(count: int, expected: str) -> None: def test_days_agree(count: int, expected: str) -> None:
assert text.days(count) == expected assert text.days(count) == expected
@pytest.mark.parametrize(
("count", "expected"),
[
(67, "67 🤷"),
(69, "69 (nice)"),
(314, "314 🥧"),
(420, "420 🌿"),
(1410, "1410 ⚔️"),
(1916, "1916 🇵🇱"),
(2137, "2137 🙏"),
(68, "68"),
(0, "0"),
(21370, "21370"),
],
)
def test_the_numbers_the_group_reacts_to_carry_their_marker(count: int, expected: str) -> None:
assert text.marked(count) == expected
def test_the_marker_sticks_to_the_number_in_front_of_its_noun() -> None:
assert text.messages(69) == "69 (nice) wiadomości"
assert text.commands(2137) == "2137 🙏 komend"
assert text.people(67) == "67 🤷 osób"
assert text.days(1410) == "1410 ⚔️ dni"
def test_an_average_is_not_a_count_and_stays_bare() -> None:
""" "średnio 69 znaków" is a mean: nobody wrote it, so nothing is marked."""
assert text.characters(69) == "69 znaków"