waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | // @vitest-environment node |
| 2 | /** |
| 3 | * Begriffserkennung im Fragetext. |
| 4 | * |
| 5 | * Der heikle Teil ist die Wortgrenze. `\b` kennt nur ASCII-Wortzeichen – |
| 6 | * in „Schießstätte" gälte damit jedes ß und ä als Grenze. Und „Waffe" darf |
| 7 | * nicht in „Waffenbesitzkarte" treffen, sonst hängt an jeder zweiten Frage |
| 8 | * ein Begriff, der dort gar nicht steht. |
| 9 | */ |
| 10 | |
| 11 | import { describe, expect, it } from 'vitest'; |
| 12 | |
| 13 | import { begriffssucheBauen, type Glossar, type Glossareintrag } from '../src/shared/glossar'; |
| 14 | |
| 15 | function eintrag(begriff: string, varianten: string[]): Glossareintrag { |
| 16 | return { |
| 17 | begriff, |
| 18 | art: 'begriff', |
| 19 | kurz: `Bedeutung von ${begriff} in einem Satz, lang genug zum Anzeigen.`, |
| 20 | varianten, |
| 21 | fundstellen: [], |
| 22 | ohneFundstelleGrund: 'Prüfeintrag ohne gesetzliche Grundlage.', |
| 23 | }; |
| 24 | } |
| 25 | |
| 26 | function glossar(...eintraege: Glossareintrag[]): Glossar { |
| 27 | return { |
| 28 | meta: { version: 1, stand: '2026-08-21', gesetzesstand: {}, hinweis: 'Prüfstand' }, |
| 29 | eintraege, |
| 30 | }; |
| 31 | } |
| 32 | |
| 33 | describe('Begriffe im Text finden', () => { |
| 34 | it('findet die Grundform', () => { |
| 35 | const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt', 'geführt']))); |
| 36 | |
| 37 | expect(suche.imText('Wer eine Waffe führt, braucht einen Waffenschein.')).toHaveLength(1); |
| 38 | }); |
| 39 | |
| 40 | it('findet alle angegebenen Wortformen', () => { |
| 41 | const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt', 'geführt']))); |
| 42 | |
| 43 | for (const satz of ['Darf man führen?', 'Er führt sie.', 'Sie wurde geführt.']) { |
| 44 | expect(suche.imText(satz), satz).toHaveLength(1); |
| 45 | } |
| 46 | }); |
| 47 | |
| 48 | it('achtet nicht auf Groß- und Kleinschreibung', () => { |
| 49 | const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen']))); |
| 50 | |
| 51 | expect(suche.imText('Das Führen ist erlaubnispflichtig.')).toHaveLength(1); |
| 52 | }); |
| 53 | |
| 54 | it('trifft nicht innerhalb eines längeren Wortes', () => { |
| 55 | /* Der wichtigste Fall: Sonst hinge an fast jeder Frage ein Begriff, der |
| 56 | dort gar nicht steht. */ |
| 57 | const suche = begriffssucheBauen(glossar(eintrag('Waffe', ['Waffe', 'Waffen']))); |
| 58 | |
| 59 | expect(suche.imText('Die Waffenbesitzkarte wird ausgestellt.')).toHaveLength(0); |
| 60 | expect(suche.imText('Eine Schusswaffe liegt vor.')).toHaveLength(0); |
| 61 | expect(suche.imText('Die Waffe ist geladen.')).toHaveLength(1); |
| 62 | }); |
| 63 | |
| 64 | it('behandelt Umlaute und ß als Wortzeichen', () => { |
| 65 | /* `\\b` würde in „Schießstätte" an jedem ß und ä eine Grenze sehen. |
| 66 | Dann fände „Stätte" einen Treffer, den es nicht gibt. */ |
| 67 | const suche = begriffssucheBauen( |
| 68 | glossar( |
| 69 | eintrag('Schießstätte', ['Schießstätte', 'Schießstätten']), |
| 70 | eintrag('Stätte', ['Stätte']), |
| 71 | ), |
| 72 | ); |
| 73 | const gefunden = suche.imText('Auf einer Schießstätte darf geschossen werden.'); |
| 74 | |
| 75 | expect(gefunden.map((e) => e.begriff)).toEqual(['Schießstätte']); |
| 76 | }); |
| 77 | |
| 78 | it('findet mehrteilige Begriffe', () => { |
| 79 | const suche = begriffssucheBauen( |
| 80 | glossar(eintrag('Wesentliche Teile', ['wesentliche Teile', 'wesentlichen Teile'])), |
| 81 | ); |
| 82 | |
| 83 | expect(suche.imText('Auch die wesentlichen Teile sind erfasst.')).toHaveLength(1); |
| 84 | }); |
| 85 | |
| 86 | /* |
| 87 | Deutsch schreibt Substantive groß, und daran hängt hier eine Bedeutung: |
| 88 | „Geschossen" ist der Dativ Plural von „Geschoss", „geschossen" das |
| 89 | Partizip von „schießen". Bis Fassung 0.24.1 suchte die Begriffssuche |
| 90 | ausnahmslos ohne Rücksicht auf Groß- und Kleinschreibung; am Katalog |
| 91 | nachgezählt bekamen zwölf Fragen mit dem Verb den Substantiveintrag |
| 92 | angeboten und sieben mit dem Substantiv den Verbeintrag. |
| 93 | */ |
| 94 | it('unterscheidet groß notierte Varianten von klein geschriebenen Wörtern', () => { |
| 95 | const suche = begriffssucheBauen( |
| 96 | glossar( |
| 97 | eintrag('Geschoss', ['Geschoss', 'Geschosse', 'Geschossen']), |
| 98 | eintrag('Schießen', ['schießen', 'geschossen', 'schießt']), |
| 99 | ), |
| 100 | ); |
| 101 | |
| 102 | expect( |
| 103 | suche.imText('Darf unter Alkoholeinfluss geschossen werden?').map((e) => e.begriff), |
| 104 | ).toEqual(['Schießen']); |
| 105 | expect( |
| 106 | suche.imText('Der Gefährdungsbereich von Geschossen der Patrone.').map((e) => e.begriff), |
| 107 | ).toEqual(['Geschoss']); |
| 108 | }); |
| 109 | |
| 110 | it('findet eine klein notierte Variante auch am Satzanfang', () => { |
| 111 | /* Ein Verb steht dort groß, und die Variante ist klein notiert – hier |
| 112 | muss die Suche weiterhin ohne Rücksicht arbeiten. */ |
| 113 | const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt']))); |
| 114 | |
| 115 | expect(suche.imText('Führt jemand eine Waffe, braucht er einen Schein.')).toHaveLength(1); |
| 116 | }); |
| 117 | |
| 118 | it('gibt die Begriffe in deutscher Sortierung zurück', () => { |
| 119 | // Ohne Locale stünde „Überlassen" hinter „Zuverlässigkeit". |
| 120 | const suche = begriffssucheBauen( |
| 121 | glossar( |
| 122 | eintrag('Zuverlässigkeit', ['Zuverlässigkeit']), |
| 123 | eintrag('Überlassen', ['überlässt']), |
| 124 | eintrag('Bedürfnis', ['Bedürfnis']), |
| 125 | ), |
| 126 | ); |
| 127 | const text = 'Wer die Zuverlässigkeit besitzt und ein Bedürfnis hat, dem überlässt man sie.'; |
| 128 | |
| 129 | expect(suche.imText(text).map((e) => e.begriff)).toEqual([ |
| 130 | 'Bedürfnis', |
| 131 | 'Überlassen', |
| 132 | 'Zuverlässigkeit', |
| 133 | ]); |
| 134 | }); |
| 135 | |
| 136 | it('meldet jeden Begriff höchstens einmal', () => { |
| 137 | const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt']))); |
| 138 | |
| 139 | expect(suche.imText('Wer führt, muss führen dürfen.')).toHaveLength(1); |
| 140 | }); |
| 141 | |
| 142 | it('übergeht Einträge ohne Wortformen', () => { |
| 143 | /* Ein Ausdruck aus einer leeren Alternativenliste passt auf jede Stelle |
| 144 | und hängte das ganze Glossar an jede Frage. */ |
| 145 | const suche = begriffssucheBauen(glossar(eintrag('Ohne Formen', []))); |
| 146 | |
| 147 | expect(suche.imText('Beliebiger Text.')).toHaveLength(0); |
| 148 | expect(suche.eintraege).toHaveLength(1); |
| 149 | }); |
| 150 | |
| 151 | it('nimmt Sonderzeichen in einer Wortform wörtlich', () => { |
| 152 | // „F im Fünfeck" enthält keine Regex-Zeichen, aber § und Punkte kommen vor. |
| 153 | const suche = begriffssucheBauen(glossar(eintrag('Paragraf', ['§ 1', 'Abs.']))); |
| 154 | |
| 155 | expect(suche.imText('Siehe § 1 des Gesetzes.')).toHaveLength(1); |
| 156 | expect(suche.imText('Siehe Abs. 2.')).toHaveLength(1); |
| 157 | expect(suche.imText('Siehe § 12.')).toHaveLength(0); |
| 158 | }); |
| 159 | |
| 160 | it('findet in einem leeren Text nichts', () => { |
| 161 | const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen']))); |
| 162 | |
| 163 | expect(suche.imText('')).toHaveLength(0); |
| 164 | }); |
| 165 | }); |