waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src shared gesetzesuche.ts

6,3 KB Rohdatei
app/src/shared/gesetzesuche.ts — 182 Zeilen
1 /**
2 * In den mitgelieferten Gesetzen blättern und suchen.
3 *
4 * ## Was diese Datei beantwortet
5 *
6 * „Was steht eigentlich in § 13 WaffG?“ Bis Fassung 0.26.7 war der amtliche
7 * Wortlaut nur über eine Erklärung erreichbar: Wer eine Frage bearbeitete,
8 * konnte die Fundstelle darunter aufklappen. Wer eine Vorschrift **suchte**,
9 * ohne die passende Frage zu kennen, kam nicht an sie heran – obwohl 148
10 * Normen aus sieben Gesetzen im Paket liegen.
11 *
12 * ## Warum die Suche hier steht und nicht in der Ansicht
13 *
14 * Damit sie prüfbar ist. Was gesucht wird und in welcher Reihenfolge die
15 * Treffer stehen, ist eine Aussage über den Inhalt – kein Aussehen. Die
16 * Ansicht bekommt fertige Listen und entscheidet nur noch über die Darstellung.
17 *
18 * ## Was nicht gesucht wird
19 *
20 * Der Wortlaut wird nicht verändert, nicht gekürzt und nicht umsortiert
21 * (§ 5 Abs. 1 UrhG, und der Grundsatz dieses Projekts dazu). Gesucht wird
22 * über Bezeichnung, Überschrift und Text; ausgegeben wird der Fundort, nicht
23 * ein zurechtgeschnittener Ausschnitt.
24 */
25
26 import { GESETZ_BEZEICHNUNG, type Gesetzeskuerzel } from './erklaerungen';
27 import type { Normtext, Normtexte } from './normtexte';
28
29 /** Ein Gesetz, wie die Ansicht es in der Auswahl braucht. */
30 export interface Gesetzeseintrag {
31 readonly kuerzel: Gesetzeskuerzel;
32 readonly bezeichnung: string;
33 /** Änderungsstand, wörtlich aus dem amtlichen XML. */
34 readonly stand: string;
35 readonly quelle: string;
36 readonly normenGesamt: number;
37 }
38
39 /** Eine Norm in der Liste. */
40 export interface Normeintrag {
41 readonly gesetz: Gesetzeskuerzel;
42 /** „§ 12“ oder „Anlage 1“. */
43 readonly norm: string;
44 readonly titel: string;
45 readonly istAnlage: boolean;
46 /**
47 * Warum diese Norm in der Trefferliste steht.
48 *
49 * Die Ansicht sagt es dazu: Ein Treffer, der nur im Fließtext steckt, ist
50 * für den Suchenden etwas anderes als einer in der Überschrift – und wer
51 * nicht sieht, warum etwas gefunden wurde, sucht weiter.
52 */
53 readonly treffer: 'bezeichnung' | 'ueberschrift' | 'text' | null;
54 }
55
56 /** Die Gesetze in der Reihenfolge, in der sie angeboten werden. */
57 export function gesetzeAuflisten(normtexte: Normtexte): Gesetzeseintrag[] {
58 return (
59 Object.entries(normtexte.gesetze)
60 .map(([kuerzel, gesetz]) => ({
61 kuerzel: kuerzel as Gesetzeskuerzel,
62 bezeichnung: gesetz.bezeichnung,
63 stand: gesetz.stand,
64 quelle: gesetz.quelle,
65 normenGesamt: Object.keys(gesetz.normen).length,
66 }))
67 /* Nach der ausgeschriebenen Bezeichnung, nicht nach dem Kürzel: In der
68 Auswahl steht die Bezeichnung, und eine Liste, die anders sortiert ist
69 als sie aussieht, ist keine Liste, sondern ein Rätsel. */
70 .sort((a, b) => a.bezeichnung.localeCompare(b.bezeichnung, 'de'))
71 );
72 }
73
74 /**
75 * Die Sortiernummer einer Norm.
76 *
77 * „§ 2“ muss vor „§ 10“ stehen – alphabetisch stünde es dahinter. Gelesen
78 * wird die erste Zahlengruppe; „§ 13a“ reiht sich hinter „§ 13“ ein, weil der
79 * Rest als Zeichenkette nachentscheidet. Anlagen kommen nach den Paragrafen,
80 * so wie sie auch im Gesetz stehen.
81 */
82 function sortierschluessel(eintrag: Normeintrag): [number, number, string] {
83 const zahl = /\d+/u.exec(eintrag.norm);
84 return [eintrag.istAnlage ? 1 : 0, zahl === null ? 0 : Number(zahl[0]), eintrag.norm];
85 }
86
87 function nachOrdnung(a: Normeintrag, b: Normeintrag): number {
88 const [aA, aZ, aT] = sortierschluessel(a);
89 const [bA, bZ, bT] = sortierschluessel(b);
90 return aA - bA || aZ - bZ || aT.localeCompare(bT, 'de');
91 }
92
93 /** Der ganze Text einer Norm, für die Suche zusammengelegt. */
94 function volltext(norm: Normtext): string {
95 const teile: string[] = [norm.titel];
96 if (norm.text !== undefined) {
97 teile.push(norm.text);
98 }
99 for (const absatz of Object.values(norm.absaetze ?? {})) {
100 teile.push(absatz);
101 }
102 for (const block of norm.bloecke ?? []) {
103 teile.push(block.text);
104 }
105 return teile.join('\n');
106 }
107
108 /**
109 * Vereinheitlicht einen Suchbegriff.
110 *
111 * Kleinschreibung und das Paragrafenzeichen weg: Wer „§13“ eingibt, meint
112 * „§ 13“, und wer „13“ eingibt, meint es auch. Ein Suchfeld, das an einem
113 * Leerzeichen scheitert, wird nicht wieder benutzt.
114 */
115 function falten(text: string): string {
116 return text.toLocaleLowerCase('de').replace(/§/gu, ' ').replace(/\s+/gu, ' ').trim();
117 }
118
119 /**
120 * Die Normen eines Gesetzes – gefiltert, wenn gesucht wird.
121 *
122 * Ohne Suchbegriff steht die vollständige Liste in der Ordnung des Gesetzes.
123 * Mit Suchbegriff stehen die Treffer in derselben Ordnung: Wer in einem
124 * Gesetz sucht, will wissen, **wo** etwas steht – eine nach Trefferqualität
125 * umsortierte Liste zerrisse den Zusammenhang, den die Nummerierung stiftet.
126 */
127 export function normenAuflisten(
128 normtexte: Normtexte,
129 gesetz: Gesetzeskuerzel,
130 suche = '',
131 ): Normeintrag[] {
132 const eintrag = normtexte.gesetze[gesetz];
133 if (eintrag === undefined) {
134 return [];
135 }
136 const gesucht = falten(suche);
137
138 const alle = Object.entries(eintrag.normen).map(([norm, text]) => {
139 const treffer: Normeintrag['treffer'] =
140 gesucht.length === 0
141 ? null
142 : falten(norm).includes(gesucht)
143 ? 'bezeichnung'
144 : falten(text.titel).includes(gesucht)
145 ? 'ueberschrift'
146 : falten(volltext(text)).includes(gesucht)
147 ? 'text'
148 : null;
149 return {
150 gesetz,
151 norm,
152 titel: text.titel,
153 istAnlage: text.istAnlage,
154 treffer,
155 };
156 });
157
158 const gefiltert = gesucht.length === 0 ? alle : alle.filter((n) => n.treffer !== null);
159 return gefiltert.sort(nachOrdnung);
160 }
161
162 /**
163 * Wie viele Normen jedes Gesetz zu einem Suchbegriff beisteuert.
164 *
165 * Damit die Gesetzesauswahl sagen kann, wo etwas zu finden ist, statt den
166 * Suchenden sieben Gesetze der Reihe nach durchklicken zu lassen.
167 */
168 export function trefferJeGesetz(
169 normtexte: Normtexte,
170 suche: string,
171 ): ReadonlyMap<Gesetzeskuerzel, number> {
172 const zahlen = new Map<Gesetzeskuerzel, number>();
173 for (const kuerzel of Object.keys(normtexte.gesetze) as Gesetzeskuerzel[]) {
174 zahlen.set(kuerzel, normenAuflisten(normtexte, kuerzel, suche).length);
175 }
176 return zahlen;
177 }
178
179 /** Die ausgeschriebene Bezeichnung – auch für ein Kürzel ohne Normtext. */
180 export function bezeichnungVon(kuerzel: Gesetzeskuerzel): string {
181 return GESETZ_BEZEICHNUNG[kuerzel];
182 }