mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:17:02 +01:00
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>
This commit is contained in:
parent
e3c7bbfe23
commit
c195e6b4c1
@ -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
|
||||
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
|
||||
|
||||
Nothing is ever deleted, and the last day reported is recorded per group.
|
||||
|
||||
@ -64,13 +64,13 @@ def personal_record(facts: DayFacts) -> Highlight | None:
|
||||
)
|
||||
if previous is None:
|
||||
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(
|
||||
kind="personal_record",
|
||||
family=RECORD,
|
||||
priority=100,
|
||||
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
|
||||
|
||||
@ -100,7 +100,7 @@ def media_record(facts: DayFacts) -> Highlight | None:
|
||||
family=RECORD,
|
||||
priority=90,
|
||||
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ń",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ def _media(count: int) -> str:
|
||||
def person_line(row: DayRow, place: int) -> str:
|
||||
"""One person's line: a crown for first place, otherwise a rank."""
|
||||
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:
|
||||
|
||||
@ -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."""
|
||||
if record is 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(
|
||||
@ -91,7 +91,8 @@ def render_records(
|
||||
lines.append("")
|
||||
lines.append("Rekordy życiowe:")
|
||||
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)
|
||||
|
||||
@ -161,7 +162,9 @@ def render_me(
|
||||
"""One person's own numbers."""
|
||||
lines = [ME_TITLE, "", f"{name} — {text.messages(total)} w historii"]
|
||||
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}%")
|
||||
if favourite_hour is not None:
|
||||
lines.append(
|
||||
|
||||
@ -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."""
|
||||
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} — {row.messages} {bar(share)} {share}%"
|
||||
return f"{lead} {row.display_name} — {text.marked(row.messages)} {bar(share)} {share}%"
|
||||
|
||||
|
||||
def render_period(
|
||||
|
||||
@ -11,6 +11,21 @@ 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
|
||||
@ -38,9 +53,19 @@ def plural(count: int, one: str, few: str, many: str) -> str:
|
||||
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"{count} {plural(count, 'wiadomość', 'wiadomości', 'wiadomości')}"
|
||||
return f"{marked(count)} {plural(count, 'wiadomość', 'wiadomości', 'wiadomości')}"
|
||||
|
||||
|
||||
def characters(count: int) -> str:
|
||||
@ -50,14 +75,14 @@ def characters(count: int) -> str:
|
||||
|
||||
def commands(count: int) -> str:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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')}"
|
||||
|
||||
@ -226,3 +226,12 @@ def test_nothing_to_say_is_an_empty_list() -> None:
|
||||
def test_ratio_survives_a_zero_baseline() -> None:
|
||||
assert ratio(5, 0) == 5.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
|
||||
|
||||
@ -120,3 +120,11 @@ def test_a_custom_heading_replaces_the_date() -> None:
|
||||
day = rows([("kuhy", 1, 0)])
|
||||
rendered = render_day("2026-09-01", day, day_totals(day), DayView(heading="Dziś, na razie"))
|
||||
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 📷)")
|
||||
|
||||
@ -127,3 +127,27 @@ def test_me_reports_everything_it_does_know() -> None:
|
||||
assert "Rekord życiowy: 12 (1 września)" in rendered
|
||||
assert "Najczęściej piszesz między 23:00 a 00:00" 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
|
||||
|
||||
@ -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")])
|
||||
def test_days_agree(count: int, expected: str) -> None:
|
||||
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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user