Sechster und siebter Fund: die letzten handgeschriebenen Nummern

Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**",
saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es
Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt.

Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie
gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die
Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen
Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist
keine Tabellenzelle.

Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten
Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n -
nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel
machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also
nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile
schon taten.

nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT
aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte
gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus
{#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23.
Im Quelltext steht "**{loesung} — Titel.**".

Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie
"siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das
QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie
stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden
einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler}
umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12
den Kapitelnamen statt eines Verweises.

Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer.
Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er
steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie
variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch
das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit
dem Titel des Raetsels ankuendigen.

--check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M —
im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele
{loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene
oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit
vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau
null.

Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher
verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118
Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig
schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12
Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder
inhaltlich veraenderte Zeile.

Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz
tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327
Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie
Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit
des Anbieters, nicht an dieser Aenderung).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 09:52:41 +02:00
commit cc990c2998
45 changed files with 1056 additions and 876 deletions

View file

@ -1142,7 +1142,7 @@ Ein Programm, das abstürzt, ist harmlos — Sie sehen sofort, dass etwas nicht
Gefährlich sind Programme, die fehlerfrei durchlaufen, einen Status `OPTIMAL` melden und
eine Zahl liefern, die betriebswirtschaftlich Unsinn ist. Genau solche Fälle üben wir hier.
> **🐛 Finde den Denkfehler 1.1: Die Schreinerei verdoppelt ihren Gewinn**
> **🐛 Finde den Denkfehler: Die Schreinerei verdoppelt ihren Gewinn**
>
> Eine Kollegin hat das Schreinerei-Modell aus dem Kapitelanfang leicht umgebaut — sie
> fand die Schleife übersichtlicher als zwei einzelne `Add`-Aufrufe. Das Programm läuft,
@ -1191,7 +1191,7 @@ eine Zahl liefern, die betriebswirtschaftlich Unsinn ist. Genau solche Fälle ü
## Micro-Quiz {#sec:einfuehrung-quiz}
> **❓ Micro-Quiz 1: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-einfuehrung).

View file

@ -1243,7 +1243,7 @@ Prüfen Sie das Ergebnis und vergleichen Sie es mit dem Original.
## Finde den Denkfehler {#sec:fundament-denkfehler}
> **🐛 Finde den Denkfehler 2.1: Die unauffällige Transposition**
> **🐛 Finde den Denkfehler: Die unauffällige Transposition**
>
> Ein Kollege überträgt ein Produktionsmodell in Matrixform. Zwei Produkte, zwei
> Ressourcen — die Technologiematrix ist quadratisch:
@ -1288,7 +1288,7 @@ Prüfen Sie das Ergebnis und vergleichen Sie es mit dem Original.
## Micro-Quiz {#sec:fundament-quiz}
> **❓ Micro-Quiz 2: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-fundament).

View file

@ -1162,7 +1162,7 @@ passende Empfehlungen.
## Finde den Denkfehler {#sec:oekosystem-denkfehler}
> **🐛 Finde den Denkfehler 3.1: Der Solver, der angeblich dreimal schneller ist**
> **🐛 Finde den Denkfehler: Der Solver, der angeblich dreimal schneller ist**
>
> Ein Team vergleicht zwei Bibliotheken für sein Zuordnungsproblem und schreibt ins
> Protokoll: *„CVXPY braucht 1,9 s, SciPy nur 0,6 s — wir setzen auf SciPy.“* Gemessen
@ -1199,7 +1199,7 @@ passende Empfehlungen.
## Micro-Quiz {#sec:oekosystem-quiz}
> **❓ Micro-Quiz 3: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-oekosystem).

View file

@ -1317,7 +1317,7 @@ $$\max\ 3x_1+2x_2 \quad\text{u. d. N.}\quad x_1+x_2 \ge 4,\quad x_1+3x_2 \le 12,
## Finde den Denkfehler {#sec:lp-denkfehler}
> **🐛 Finde den Denkfehler 4.1: Die 380 000-Euro-Maschine**
> **🐛 Finde den Denkfehler: Die 380 000-Euro-Maschine**
>
> Ein Werksleiter lässt die Engpässe seiner Fertigung analysieren. Das Skript liefert
> für den Lackierofen einen Schattenpreis von **50 €/Stunde**. Er rechnet:
@ -1356,7 +1356,7 @@ $$\max\ 3x_1+2x_2 \quad\text{u. d. N.}\quad x_1+x_2 \ge 4,\quad x_1+3x_2 \le 12,
## Micro-Quiz {#sec:lp-quiz}
> **❓ Micro-Quiz 4: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-lp).

View file

@ -1471,7 +1471,7 @@ eröffnete Lager hat Kapazität 80. Minimieren Sie Fix- plus Transportkosten.
Die Warnung „$M$ so klein wie möglich“ wird meist mit der Laufzeit begründet: schwache
Relaxation, mehr Knoten. Das stimmt — und ist die **harmlosere** Hälfte der Wahrheit.
> **🐛 Finde den Denkfehler 5.1: Elf Lager, die keine Fixkosten kosten**
> **🐛 Finde den Denkfehler: Elf Lager, die keine Fixkosten kosten**
>
> Ein Team plant Standorte: 12 mögliche Lager, 40 Kunden, Fixkosten je eröffnetem Lager.
> Für das Big-M in der Kopplung
@ -1758,7 +1758,7 @@ funktioniert immer.
## Micro-Quiz {#sec:milp-quiz}
> **❓ Micro-Quiz 5: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-milp).

View file

@ -1196,7 +1196,7 @@ Wünsche als weiche Ziele. Geben Sie den Plan als Wochentabelle aus.
## Finde den Denkfehler {#sec:cpsat-denkfehler}
> **🐛 Finde den Denkfehler 6.1: Die Betriebsvereinbarung, die niemanden interessiert**
> **🐛 Finde den Denkfehler: Die Betriebsvereinbarung, die niemanden interessiert**
>
> Ein Klinikteam lässt seinen Schichtplan optimieren. Zwei Wünsche kommen aus dem Betrieb:
>
@ -1428,7 +1428,7 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
## Micro-Quiz {#sec:cpsat-quiz}
> **❓ Micro-Quiz 6: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-cpsat).

View file

@ -751,7 +751,7 @@ OR-Tools. Was beobachten Sie ab 12 Städten?
## Finde den Denkfehler {#sec:graphen-denkfehler}
> **🐛 Finde den Denkfehler 7.1: Die vergessene Dimension**
> **🐛 Finde den Denkfehler: Die vergessene Dimension**
>
> Eine Spedition lässt ihre Tagestouren optimieren: 16 Kunden, 4 Fahrzeuge zu je 10
> Paletten, Gesamtbedarf 37 Paletten. Das erste Ergebnis begeistert alle — **326 km**.
@ -1018,7 +1018,7 @@ plausibel aussieht.
## Micro-Quiz {#sec:graphen-quiz}
> **❓ Micro-Quiz 7: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-graphen).

View file

@ -1211,7 +1211,7 @@ groß ist der Unterschied zwischen bestem und schlechtestem Ergebnis? Kann die
## Finde den Denkfehler {#sec:qp-nlp-denkfehler}
> **🐛 Finde den Denkfehler 8.1: Die Kovarianzmatrix aus dem Controlling**
> **🐛 Finde den Denkfehler: Die Kovarianzmatrix aus dem Controlling**
>
> Ein Analyst soll das Risiko eines Portfolios aus drei Anlagen minimieren. Die
> Korrelationen hat er aus drei verschiedenen Quartalsberichten zusammengetragen — jede
@ -1279,7 +1279,7 @@ groß ist der Unterschied zwischen bestem und schlechtestem Ergebnis? Kann die
## Micro-Quiz {#sec:qp-nlp-quiz}
> **❓ Micro-Quiz 8: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-qp-nlp).

View file

@ -1278,7 +1278,7 @@ das lässt sich in CVXPY mit `cp.sum_largest` formulieren.)
## Finde den Denkfehler {#sec:unsicherheit-denkfehler}
> **🐛 Finde den Denkfehler 9.1: Warum jedes Projekt zu spät fertig wird**
> **🐛 Finde den Denkfehler: Warum jedes Projekt zu spät fertig wird**
>
> Ein Projektleiter plant eine Umbaumaßnahme. Fünf Gewerke arbeiten **parallel** und
> unabhängig voneinander; fertig ist der Umbau, wenn das letzte fertig ist. Für jedes
@ -1334,7 +1334,7 @@ das lässt sich in CVXPY mit `cp.sum_largest` formulieren.)
## Micro-Quiz {#sec:unsicherheit-quiz}
> **❓ Micro-Quiz 9: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-unsicherheit).

View file

@ -739,7 +739,7 @@ Bei dynamischer Programmierung entscheidet eine einzige Frage über Erfolg oder
**Was gehört in den Zustand?** Ist er zu klein, rechnet das Verfahren völlig korrekt — nur
eben an einem anderen Problem.
> **🐛 Finde den Denkfehler 10.1: Der Zustand, der zu wenig weiß**
> **🐛 Finde den Denkfehler: Der Zustand, der zu wenig weiß**
>
> Eine Werkstatt plant vier Perioden. Der Bedarf beträgt 3, 1, 4, 2 Stück; produziert werden
> können höchstens 5 je Periode, gelagert höchstens 6 Stück.
@ -796,7 +796,7 @@ eben an einem anderen Problem.
## Micro-Quiz {#sec:dynamische-programmierung-quiz}
> **❓ Micro-Quiz 10: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-dynamische-programmierung).

View file

@ -680,7 +680,7 @@ Erweitern Sie `Schaetzrauschen_Demo.py` um eine dritte Variante: die
## Finde den Denkfehler {#sec:finanzdaten-denkfehler}
> **🐛 Finde den Denkfehler 11.1: Das risikofreie Portfolio**
> **🐛 Finde den Denkfehler: Das risikofreie Portfolio**
>
> Ein Analyst stellt ein Portfolio aus **30 Titeln** zusammen. Für die Kovarianzmatrix
> nimmt er das letzte Quartal — bei Wochendaten sind das **20 Beobachtungen**. Dann
@ -760,7 +760,8 @@ def minimales_risiko(kovarianz: np.ndarray) -> tuple[np.ndarray, float]:
exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so
gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im
Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man
nachgehen muss (siehe Denkfehler 8.1).
nachgehen muss (siehe den Denkfehler im Kapitel Quadratische und
nichtlineare Optimierung).
"""
gewichte = cp.Variable(ANZAHL_ANLAGEN)
problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),
@ -885,7 +886,7 @@ e 0: \mathbf{w}^ op\mathbf{S}\mathbf{w} = 0$ | „In genau diesen Richtungen mis
## Micro-Quiz {#sec:finanzdaten-quiz}
> **❓ Micro-Quiz 11: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-finanzdaten).

View file

@ -763,7 +763,7 @@ zweiten. Vergleichen Sie GMV, Max Sharpe und 1/N.
anrichtet. Bei den erwarteten **Renditen** ist es schlimmer — und weil sie in der
Zielfunktion stehen statt in der Nebenbedingung, fällt es schwerer auf.
> **🐛 Finde den Denkfehler 12.1: Zwölf gleiche Anlagen, ein sehr ungleiches Portfolio**
> **🐛 Finde den Denkfehler: Zwölf gleiche Anlagen, ein sehr ungleiches Portfolio**
>
> Ein Analyst optimiert ein Portfolio aus zwölf Anlagen. Erwartete Renditen und
> Kovarianzen schätzt er aus einem Jahr Tagesdaten — 250 Beobachtungen, in der Praxis
@ -986,7 +986,7 @@ Drei praktische Konsequenzen:
## Micro-Quiz {#sec:markowitz-quiz}
> **❓ Micro-Quiz 12: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-markowitz).

View file

@ -720,7 +720,7 @@ $\tau \in \{0{,}05;\ 0{,}1;\ 0{,}25;\ 0{,}5;\ 1{,}0\}$.
## Finde den Denkfehler {#sec:cvar-denkfehler}
> **🐛 Finde den Denkfehler 13.1: Das Risikobudget, das durch Diversifikation stieg**
> **🐛 Finde den Denkfehler: Das Risikobudget, das durch Diversifikation stieg**
>
> Eine Bank vergibt Risikobudgets je Abteilung. Zwei Kreditabteilungen halten je eine
> Unternehmensanleihe über 100 € Nominal mit **4 % Ausfallwahrscheinlichkeit** (bei
@ -779,7 +779,7 @@ $\tau \in \{0{,}05;\ 0{,}1;\ 0{,}25;\ 0{,}5;\ 1{,}0\}$.
## Micro-Quiz {#sec:cvar-quiz}
> **❓ Micro-Quiz 13: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-cvar).

View file

@ -754,7 +754,7 @@ sondern in der **Arbeitsweise davor** — und hinterlassen im Programm keine Spu
Backtest, der zwanzigmal nachjustiert wurde, sieht am Ende genauso sauber aus wie einer,
der beim ersten Versuch funktioniert hat.
> **🐛 Finde den Denkfehler 14.1: Die Strategie mit dem hochsignifikanten Ergebnis**
> **🐛 Finde den Denkfehler: Die Strategie mit dem hochsignifikanten Ergebnis**
>
> Eine Analystin entwickelt über mehrere Wochen eine Handelsstrategie. Sie probiert
> verschiedene Signalfenster, Schwellenwerte, Filter und Haltedauern durch — insgesamt
@ -980,7 +980,7 @@ von allen Fehlversuchen genau einen: den erfolgreichsten.
## Micro-Quiz {#sec:handelsmaschine-quiz}
> **❓ Micro-Quiz 14: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-handelsmaschine).

View file

@ -1637,7 +1637,7 @@ eigene Projekte ab?
Falle 5 — die Laufzeitexplosion — zeigt sich im Betrieb selten als Absturz. Sie zeigt
sich als **gar nichts**.
> **🐛 Finde den Denkfehler 15.1: Das Modell, das seit einem Jahr nicht mehr optimiert**
> **🐛 Finde den Denkfehler: Das Modell, das seit einem Jahr nicht mehr optimiert**
>
> Ein Standortplanungsmodell geht 2024 in Betrieb. Es läuft jede Nacht, hat drei Sekunden
> Zeitbudget und liefert zuverlässig die Kosten des Tagesplans. Das Betriebsteam
@ -1924,7 +1924,7 @@ sichtbar wird, und gibt Zeit zum Handeln.
## Micro-Quiz {#sec:praxisfallen-quiz}
> **❓ Micro-Quiz 15: Drei Fragen zum Selbstcheck**
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
>
> Genau eine Antwort ist jeweils richtig. Auflösung in
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-praxisfallen).

View file

@ -253,6 +253,61 @@ def pruefe_dateien() -> list[str]:
f"Dorthin gehoert ein {{ref:...}}-Verweis.")
fehlend.append(name)
# Die beiden uebrigen Markenfamilien. 'Denkfehler N.M' und 'Micro-Quiz N'
# standen bis zum sechsten Fund von Hand im Quelltext und waren in zwoelf
# von fuenfzehn Kapiteln falsch; die Loesungsmarke '**9.2 — Titel.**' war
# es in allen 98 Faellen und bezeichnete obendrein eine echte, ANDERE
# Aufgabe. Vergeben werden sie jetzt beim Bauen.
familien = [
(re.compile(r"Denkfehler\s+\d+\.\d+"),
"auf den Denkfehler-Abschnitt verweist {ref:sec:<kapitel>-denkfehler}"),
(re.compile(r"Micro-Quiz\s+\d+"),
"die Nummer vergibt nummeriere_marken()"),
(re.compile(r"^\*\*\d{1,2}\.\d{1,2} — ", re.M),
"im Quelltext steht dort '**{loesung} — Titel.**'"),
]
for name in DATEIEN:
with open(os.path.join(HIER, name), encoding="utf-8") as f:
inhalt = f.read()
for muster, hinweis in familien:
for treffer in muster.finditer(inhalt):
zeile = inhalt[:treffer.start()].count("\n") + 1
gefunden = " ".join(treffer.group(0).split())
print(f"FEHLER: {name}:{zeile} enthaelt die harte Nummer "
f"'{gefunden}' - {hinweis}.")
fehlend.append(name)
# Und die Gegenprobe, die keine Textsuche leisten kann: Zu jeder Aufgabe
# eines Kapitels muss es im Loesungsanhang genau eine {loesung}-Marke
# geben. Das faengt eine vergessene oder doppelte Loesung - der Fall, der
# beim Einfuegen einer neuen Aufgabe am leichtesten passiert.
kapitel_aufgaben: dict[str, int] = {}
for name in DATEIEN:
with open(os.path.join(HIER, name), encoding="utf-8") as f:
inhalt = f.read()
kopf = re.search(r"^# Kapitel: .*?\{#kap:([\w-]+)\}", inhalt, re.M)
if kopf:
kapitel_aufgaben[kopf.group(1)] = len(
re.findall(r"^\*\*Aufgabe [⭐*]", inhalt, re.M))
anhang = "90_Anhang_Loesungen.md"
if anhang in DATEIEN:
with open(os.path.join(HIER, anhang), encoding="utf-8") as f:
inhalt = f.read()
stuecke = re.split(r"^##\s.*\{#sec:loesungen-([\w-]+)\}", inhalt, flags=re.M)
for i in range(1, len(stuecke), 2):
label, block = stuecke[i], stuecke[i + 1]
marken = block.count("**{loesung}")
erwartet = kapitel_aufgaben.get(label)
if erwartet is None:
print(f"FEHLER: {anhang} hat '{{#sec:loesungen-{label}}}', "
f"aber es gibt kein Kapitel '{{#kap:{label}}}'.")
fehlend.append(anhang)
elif marken != erwartet:
print(f"FEHLER: {anhang}: Abschnitt 'sec:loesungen-{label}' hat "
f"{marken} Loesungen, das Kapitel aber {erwartet} Aufgaben.")
fehlend.append(anhang)
return fehlend
@ -534,11 +589,39 @@ def nummeriere_abschnitte(text: str, labels: dict,
AUFGABE_MARKE_RE = re.compile(r"^\*\*Aufgabe (?=[⭐*])")
HANDRECHNUNG_MARKE_RE = re.compile(r"^(> \*\*✏️ Handrechnung): ")
ABBILDUNG_MARKE_RE = re.compile(r"^!\[(?!Abb\. )")
QUIZ_MARKE_RE = re.compile(r"^(> \*\*❓ Micro-Quiz): ")
# Der Denkfehler bekommt BEWUSST keine Nummer. Es gibt je Kapitel genau einen,
# und er steht unter der bereits nummerierten Ueberschrift '## 12.8 Finde den
# Denkfehler'. Eine Marke '12.1', deren zweite Stelle immer 1 ist, traegt
# nichts bei und holte nur die Fragilitaet zurueck, die hier gerade beseitigt
# wurde. Verwiesen wird deshalb auf den Abschnitt: {ref:sec:<kapitel>-denkfehler}
# - dieses Label haben alle 23 Kapitel.
#
# Die acht in Phase 3 ergaenzten Kapitel bestaetigen das: Sie schreiben nur den
# Titel des Raetsels ('> **🐛 „Unsere Heuristik ist 6 % besser“**') und
# wiederholen die Ueberschrift gar nicht erst.
# Der Loesungsanhang ist der Sonderfall: Er ist EINE Anhang-Ueberschrift, aber
# jeder seiner Abschnitte gehoert zu einem anderen Kapitel. Der Praefix kann
# also nicht aus '# Anhang A:' kommen, sondern muss aus dem Kapitel stammen,
# zu dem der Abschnitt die Loesungen enthaelt.
#
# Die Zuordnung ist herleitbar und ohne Sonderfall: '{#sec:loesungen-X}'
# gehoert zu '{#kap:X}'. Geprueft fuer alle 23 Loesungsabschnitte.
#
# Das war der sechste Fund harter Nummern in diesem Projekt - und der
# schlimmste: Alle 98 Loesungsmarken trugen die Kapitelnummer aus Version 03.
# '9.2' bezeichnete danach eine echte, ANDERE Aufgabe (Metaheuristiken statt
# Unsicherheit), wer die Loesung nachschlug, landete beim falschen Thema.
LOESUNGS_ABSCHNITT_RE = re.compile(r"^##\s.*\{#(sec:loesungen-[\w-]+)\}")
LOESUNGS_MARKE_RE = re.compile(r"^\*\*\{loesung\} — ")
def nummeriere_marken(text: str) -> str:
"""Vergibt die laufenden Nummern fuer Aufgaben, Handrechnungen und
Abbildungen, jeweils kapitelweise und in Lesereihenfolge.
def nummeriere_marken(text: str, labels: dict | None = None) -> str:
"""Vergibt die laufenden Nummern fuer Aufgaben, Handrechnungen,
Abbildungen, Denkfehler, Micro-Quiz und Loesungen - jeweils kapitelweise
und in Lesereihenfolge.
Den Praefix ('5' bzw. 'C') liest die Funktion aus den bereits gesetzten
'# Kapitel N:'/'# Anhang X:'-Ueberschriften - genau wie
@ -549,13 +632,21 @@ def nummeriere_marken(text: str) -> str:
Marken VOR der ersten Kapitel-/Anhang-Ueberschrift (Vorwort, Notation, die
Entscheidungsdiagramme unter den Teil-Ueberschriften) bleiben unnummeriert.
Loesungen im Anhang bekommen ihren Praefix NICHT aus '# Anhang A:', sondern
aus dem Kapitel, zu dem der jeweilige '{#sec:loesungen-X}'-Abschnitt
gehoert - nachgeschlagen ueber 'labels' (siehe LOESUNGS_ABSCHNITT_RE).
Ohne 'labels' bleiben Loesungsmarken unveraendert stehen.
Codebloecke werden uebersprungen: Was in einem Programm steht, wird von
extract_programme_04.py als Datei herausgeloest und darf deshalb keine
Nummer enthalten, die nur der Buchsatz kennt (Regel 12).
"""
leer = {"aufgabe": 0, "handrechnung": 0, "abbildung": 0, "quiz": 0}
zeilen = text.split("\n")
praefix: str | None = None
zaehler = {"aufgabe": 0, "handrechnung": 0, "abbildung": 0}
zaehler = dict(leer)
loesungs_praefix: str | None = None
loesungs_zaehler = {"loesung": 0}
im_codeblock = False
for i, zeile in enumerate(zeilen):
@ -568,8 +659,30 @@ def nummeriere_marken(text: str) -> str:
kopf = GESETZTE_UEBERSCHRIFT_RE.match(zeile)
if kopf:
praefix = kopf.group("kapitel") or kopf.group("anhang")
zaehler = {"aufgabe": 0, "handrechnung": 0, "abbildung": 0}
zaehler = dict(leer)
loesungs_praefix = None
continue
# Ein neuer Loesungsabschnitt: Praefix aus dem zugehoerigen Kapitel.
# Der Anzeigetext in labels lautet 'Kapitel 12'; gebraucht wird '12'.
loesungskopf = LOESUNGS_ABSCHNITT_RE.match(zeile)
if loesungskopf and labels is not None:
kap_label = "kap:" + loesungskopf.group(1).removeprefix("sec:loesungen-")
anzeige = labels.get(kap_label, ("", ""))[0]
loesungs_praefix = (anzeige.removeprefix("Kapitel ")
if anzeige.startswith("Kapitel ") else None)
if loesungs_praefix is None:
print(f"WARNUNG: Zu '{loesungskopf.group(1)}' gibt es kein "
f"'{kap_label}' - Loesungen bleiben unnummeriert.")
loesungs_zaehler = {"loesung": 0}
continue
if loesungs_praefix is not None and LOESUNGS_MARKE_RE.match(zeile):
loesungs_zaehler["loesung"] += 1
zeilen[i] = LOESUNGS_MARKE_RE.sub(
f"**{loesungs_praefix}.{loesungs_zaehler['loesung']}", zeile)
continue
if praefix is None:
continue
@ -585,6 +698,11 @@ def nummeriere_marken(text: str) -> str:
zaehler["abbildung"] += 1
zeilen[i] = ABBILDUNG_MARKE_RE.sub(
f"![Abb. {praefix}.{zaehler['abbildung']}: ", zeile)
elif QUIZ_MARKE_RE.match(zeile):
# Micro-Quiz gibt es je Kapitel nur eines, deshalb ohne laufende
# Nummer - 'Micro-Quiz 12', nicht 'Micro-Quiz 12.1'.
zaehler["quiz"] += 1
zeilen[i] = QUIZ_MARKE_RE.sub(rf"\1 {praefix}: ", zeile)
return "\n".join(zeilen)
@ -633,7 +751,7 @@ def resolve_numbering(text: str) -> str:
# sonst zeigt der Verweis ins Leere.
text, registriert = nummeriere_abschnitte(text, labels)
abschnitte = len(registriert)
text = nummeriere_marken(text)
text = nummeriere_marken(text, labels)
def ref_repl(m: re.Match) -> str:
label = m.group(1)
@ -1045,7 +1163,7 @@ def resolve_numbering_seite(text: str, aktuelle_seite: str,
return f"## {nummer} {m.group('titel')} {{#{m.group('label')}}}"
text = ABSCHNITT_RE.sub(abschnitt_sub, text)
text = nummeriere_marken(text)
text = nummeriere_marken(text, labels)
def ref_sub(m: re.Match) -> str:
label = m.group(1)