From 9e804cd349705e45fc4cbcb8da264f61adb706538a78a76b8c043a428910c909 Mon Sep 17 00:00:00 2001 From: wlfb Date: Tue, 29 Sep 2026 16:16:49 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Torte=20=E2=80=93=20gro=C3=9Fe=20Segment?= =?UTF-8?q?e=20als=20zwei=20Teilb=C3=B6gen=20(A13-T3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- finance/app/engine/pie.py | 12 +++++++++--- finance/tests/test_pie.py | 22 +++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/finance/app/engine/pie.py b/finance/app/engine/pie.py index 9d23832..fb3d9dc 100644 --- a/finance/app/engine/pie.py +++ b/finance/app/engine/pie.py @@ -2,7 +2,8 @@ Beträge und Anteile bleiben Decimal; nur die Geometrie (Winkel, SVG- Koordinaten im Einheitskreis) rechnet mit float. Start 12 Uhr, im -Uhrzeigersinn (SVG-y zeigt nach unten). +Uhrzeigersinn (SVG-y zeigt nach unten). Große Segmente (> 50 %) werden als +zwei Teilbögen durch Mittelpunkt gezogen, um Rundungsfehler zu vermeiden. """ import math from dataclasses import dataclass @@ -35,8 +36,13 @@ def _point(fraction: float) -> tuple[float, float]: def _arc(start: float, end: float) -> str: x0, y0 = _point(start) x1, y1 = _point(end) - large = 1 if end - start > 0.5 else 0 - return f"M 0 0 L {x0:.4f} {y0:.4f} A 1 1 0 {large} 1 {x1:.4f} {y1:.4f} Z" + if end - start > 0.5: + # Großes Segment: zwei Teilbögen durch Mittelpunkt, um Rundungsfehler zu vermeiden + mid = (start + end) / 2 + xm, ym = _point(mid) + return f"M 0 0 L {x0:.4f} {y0:.4f} A 1 1 0 0 1 {xm:.4f} {ym:.4f} A 1 1 0 0 1 {x1:.4f} {y1:.4f} Z" + else: + return f"M 0 0 L {x0:.4f} {y0:.4f} A 1 1 0 0 1 {x1:.4f} {y1:.4f} Z" def pie_slices(values: list[tuple[str, Decimal]], rest: Decimal | None = None, diff --git a/finance/tests/test_pie.py b/finance/tests/test_pie.py index 7657d1f..cbdbe7a 100644 --- a/finance/tests/test_pie.py +++ b/finance/tests/test_pie.py @@ -23,10 +23,26 @@ def test_segmente_lueckenlos_und_anteile(): assert sl[0].path.startswith("M 0 0 L 0.0000 -1.0000 A 1 1 0 0 1 ") -def test_large_arc_flag(): +def test_grosses_segment_als_zwei_teilboegen(): a, b = pie_slices([("A", D("70")), ("B", D("30"))]) - assert " A 1 1 0 1 1 " in a.path - assert " A 1 1 0 0 1 " in b.path + assert a.path.count(" A 1 1 0 0 1 ") == 2 + assert b.path.count(" A 1 1 0 0 1 ") == 1 + assert " 0 1 1 " not in a.path + + +def test_fast_volles_segment_bleibt_sichtbar(): + gross, klein = pie_slices([("Gross", D("4999.99"))], rest=D("0.01"), + rest_label="Frei verfügbar") + # Zwei Teilbögen: der Zwischenpunkt liegt gegenüber (unten), der Bogen + # kann also nie zu einem Punkt zusammenfallen. + assert gross.path.count(" A 1 1 0 0 1 ") == 2 + assert " A 1 1 0 0 1 0.0000 1.0000 " in gross.path or " A 1 1 0 0 1 -0.0000 1.0000 " in gross.path + assert klein.path is not None + + +def test_letztes_segment_schliesst_bei_zwoelf_uhr(): + sl = pie_slices([("A", D("50")), ("B", D("30")), ("C", D("20"))]) + assert sl[-1].path.endswith(" 0.0000 -1.0000 Z") or sl[-1].path.endswith(" -0.0000 -1.0000 Z") def test_einzelsegment_ist_vollkreis():