Wichtungskorrekturen.
[wortliste.git] / skripte / lua / helper_words.lua
blobe5ceb59dcc916e7203e9b3226f3297b11ceb7c5a
1 -- -*- coding: utf-8 -*-
3 --[[
4 Copyright 2012, 2013 Stephan Hennig
6 This program is free software: you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation, either version 3 of the License, or (at your
9 option) any later version.
11 This program is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU General Public License along
17 with this program. If not, see <http://www.gnu.org/licenses/>.
18 --]]
22 --- Dieses Modul stellt die folgende Funktionalität zur Manipulation der
23 --- Wortliste bereit:
25 -- <ul>
26 -- <li>Prüfen von Wörtern auf Wohlgeformtheit,</li>
27 -- <li>Normalisieren von Wörtern (Übertragen in ein für <a href="http://tug.org/docs/liang/">Patgen</a> geeignetes Format):<br />
28 -- <code>Lei-nen==be[t=tu-/{tt/tt=t}u.]ches</code> &emsp;&rarr;&emsp; <code>Lei-nen-bettuches</code>,</li>
29 -- </ul>
31 -- @class module
32 -- @name helper_words
33 -- @author Stephan Hennig
34 -- @copyright 2012, 2013, Stephan Hennig
36 -- Die API-Dokumentation kann mit <pre>
38 -- luadoc -d API *.lua
40 -- </pre> erstellt werden.
44 --[[ just for luadoc 3.0 compatibility
45 module "helper_words"
46 --]]
47 -- lokale Modul-Tabelle
48 local M = {}
52 -- Lade Module.
53 local lpeg = require("lpeg")
54 local unicode = require("unicode")
58 -- Kürzel für Funktionen der Standardbibliotheken.
59 local Tconcat = table.concat
60 -- Kürzel für LPEG-Funktionen.
61 local P = lpeg.P
62 local R = lpeg.R
63 local C = lpeg.C
64 local V = lpeg.V
65 -- Kürzel für Unicode-Funktionen.
66 local Ufind = unicode.utf8.find
67 local Ugsub = unicode.utf8.gsub
68 local Ulen = unicode.utf8.len
69 local Umatch = unicode.utf8.match
70 local Usub = unicode.utf8.sub
75 -- Hilfsfunktionen und -variablen für LPEG-Mustersuche.
77 -- Eine Tabelle, in der während der Mustersuche Eigenschaften des
78 -- untersuchten Wortes gespeichert werden.
79 local word_property
81 --- <strong>nicht-öffentlich</strong> Diese Funktion wird zu Beginn
82 --- einer erfolgreichen Prüfung der Wortgrammatik gegen einen String
83 --- ausgeführt.
84 -- Initialisiert Tabelle mit Worteigenschaften.
85 local function _at_word_start()
86 word_property = {}
87 end
89 --- <strong>nicht-öffentlich</strong> Diese Funktion wird am Ende einer
90 --- erfolgreichen Prüfung der Wortgrammatik gegen einen String
91 --- ausgeführt.
92 -- Verwirft alle ausstehenden Captures.
94 -- @param ... Variable Anzahl an Strings
96 -- @return Tabelle mit Worteigenschaften
97 local function _at_word_end(...)
98 word_property.norm_word = Tconcat({...})
99 return word_property
102 --- <strong>nicht-öffentlich</strong> Merke Eigenschaft
103 --- 'Spezialtrennung enthalten'.
104 -- Die Capture wird nicht verändert.
106 -- @param c Capture
108 -- @return Capture
109 local function _property_has_nonstd(c)
110 word_property.has_nonstd = true
111 return c
114 --- <strong>nicht-öffentlich</strong> Merke Eigenschaft
115 --- 'Dreikonsonantenregel für s enthalten'.
116 -- Die Capture wird nicht verändert.
118 -- @param c Capture
120 -- @return Capture
121 local function _property_has_nonstd_sss(c)
122 word_property.has_nonstd_sss = true
123 return c
126 --- <strong>nicht-öffentlich</strong> Merke Eigenschaft 'ß enthalten'.
127 local function _property_has_eszett()
128 word_property.has_eszett = true
131 --- <strong>nicht-öffentlich</strong> Füge eine beliebige Zahl von
132 --- Strings zusammen.
133 -- Diese Funktion wird in einer Funktionscapture genutzt, um Cluster
134 -- zusammenzufügen. Vorhandene normalisierte Trennzeichen werden aus
135 -- der Ergebniscapture entfernt.
137 -- @param ... Variable Anzahl an Strings
139 -- @return zusammengesetzter String
140 local function _concatenate_alt_part_word(...)
141 -- io.stderr:write("conc: ",Tconcat({...}, ","),"\n")-- For debugging purposes.
142 return ( Ugsub(Tconcat({...}), "-", "") )
145 --- <strong>nicht-öffentlich</strong> Prüfe zwei Alternativen auf
146 --- Gleichheit.
147 -- Diese Funktion wird in einer Funktionscapture verwendet, um die
148 -- Gleichheit zweier Alternativen zu prüfen. Falls die Alternativen
149 -- ungleich sind, so wird dies als Worteigenschaft vermerkt. Ergebnis
150 -- ist das erste Argument.
152 -- @param a Capture 1
154 -- @param b Capture 2
156 -- @return Capture 1
157 local function _is_equal_two_alternatives(a, b)
158 word_property.has_invalid_alt = word_property.has_invalid_alt or (a ~= b)
159 return a
165 -- Die Grammatik eines Wortes.
167 -- Diese Grammatik beschreibt die Struktur zulässiger Wörter. Durch
168 -- Prüfen eines Strings gegen diese Grammatik kann festgestellt werden,
169 -- ob dieser ein Wort im Sinne der Grammatik enthält.
171 -- Bei positivem Ergebnis ist der Rückgabewert der Funktion `match` eine
172 -- Tabelle, in welcher während der Prüfung die folgenden Eigenschaften
173 -- des betrachteten Wortes gespeichert werden.
175 -- Feld Bedeutung
177 -- has_invalid_alt Alternativen sind nicht identisch
178 -- has_eszett Wort enthält Buchstaben 'ß'
179 -- has_nonstd Wort enthält Spezialtrennung
180 -- has_nonstd_sss Wort enthält Spezialtrennung für das 's'
181 -- norm_word normalisiertes Wort
183 -- Die Grammatik beschreibt die Zulässigkeit von Wörtern nicht
184 -- erschöpfend. So wird beispielsweise nicht darauf eingegangen, ob
185 -- Trennstellen einen Mindestabstand vom Wortrand haben müssen. Die
186 -- Zulässigkeit von Wörtern sollten daher mit zusätzlichen,
187 -- nachgeschalteten Prüfungen festgestellt werden (siehe Funktion
188 -- `validate_word()`).
190 -- Weitere Informationen können der Datei `README.wortliste` entnommen
191 -- werden.
193 local word = P{
194 -- Initialregel.
195 "property_table",
197 -- Wort
199 -- Am Anfang der Prüfung wird die Tabelle mit Worteigenschaften
200 -- zurückgesetzt. Am Ende werden alle ausstehenden Captures
201 -- verworfen.
202 property_table = P(true) / _at_word_start * V"word" * -1 / _at_word_end,
203 -- Ein Wort beginnt mit einem Wortanfang. Darauf folgt ein Wortrest.
204 word = V"word_head" * V"word_tail",
205 -- Ein Wortanfang besteht aus einem Kluster.
206 word_head = V"cluster",
207 -- Ein Wortrest besteht aus einer normalisierten
208 -- Trennstellenmarkierung gefolgt von einem Kluster. Der gesamte
209 -- Ausdruck ist optional.
210 word_tail = (V"norm_hyphen" * V"cluster")^0,
212 -- Trennstellen
214 -- Die Markierung von Trennstellen erfolgt entweder ausschließlich
215 -- nach deren morphologischer Struktur. Alternativ kann eine
216 -- Trennstelle auch qualitativ bewertet werden, optional mit
217 -- vorangestellter, morphologischer Markierung.
219 -- Normalisierte Trennstelle
221 -- Während des Normalisierens werden die Zeichen einer
222 -- Trennstellenmarkierung in ein für Patgen geeignetes Zeichen
223 -- gewandelt.
225 -- Es gibt zwei Arten von normalisierten Trennstellenmarkierungen mit
226 -- unterschiedlichen Captures.
227 norm_hyphen = V"hyphen_norm_without_quality" + V"hyphen_norm_with_quality",
228 -- Trennstellenmarkierungen ohne Bewertung werden in ein einfaches
229 -- Trennzeichen umgewandelt.
230 hyphen_norm_without_quality = V"hyphen_without_quality" / "-",
231 -- Trennstellmarkierungen mit Bewertung werden in einen Leerstring
232 -- gewandelt; solche Trennungen werden also unterdrückt.
233 hyphen_norm_with_quality = V"hyphen_with_quality" / "",
234 -- Eine Trennstelle mit Bewertung kann eine optionale, vorstehende,
235 -- morphologische Markierung enthalten.
236 hyphen_with_quality = V"hyphen_morph"^0 * V"hyphen_quality",
237 -- Eine Trennstelle ohne Bewertung ist ausschließlich morphologisch
238 -- markiert.
239 hyphen_without_quality = V"hyphen_morph" * -V"hyphen_quality",
241 -- morphologische Markierungen
243 -- innerhalb von Wortstämmen, Präfixen oder Suffixen
244 hyphen_inner = V"hyphen_ch_inner"^1,
245 -- nach Präfixen oder Verbalpartikeln
246 hyphen_prefix = V"hyphen_ch_prefix"^1,
247 -- vor Suffixen
248 hyphen_suffix = V"hyphen_ch_suffix"^1,
249 -- an Wortfugen
250 hyphen_compound = V"hyphen_ch_compound"^1,
251 -- nach Präfix eines zusammengesetzten Wortes
252 hyphen_compound_prefix = V"hyphen_ch_prefix" * V"hyphen_compound",
253 -- vor Suffix eines zusammengesetzten Wortes
254 hyphen_compound_suffix = V"hyphen_compound" * V"hyphen_ch_suffix",
255 -- unkategorisiert
256 hyphen_uncategorized = V"hyphen_ch_uncategorized",
257 -- eine morphologische Markierung (Achtung: In der folgenden Regel
258 -- ist die Reihenfolge der Prüfung relevant. Gemischte Trennzeichen
259 -- müssen vor reinen Trennzeichen geprüft werden.)
260 hyphen_morph =
261 V"hyphen_compound_prefix"
262 + V"hyphen_compound_suffix"
263 + V"hyphen_inner"
264 + V"hyphen_prefix"
265 + V"hyphen_suffix"
266 + V"hyphen_compound"
267 + V"hyphen_uncategorized"
270 -- qualitative Markierungen (Bewertung)
272 -- Eine Bewertung besteht aus ein bis drei Bewertungszeichen.
273 hyphen_quality = V"hyphen_ch_quality" * V"hyphen_ch_quality"^-2,
274 -- Eine beliebige Trennstellenmarkierung.
275 hyphen = V"hyphen_without_quality" + V"hyphen_with_quality",
276 -- Die folgenden Zeichen werden zur Trennstellenmarkierung verwendet:
277 hyphen_ch_inner = P"-",
278 hyphen_ch_prefix = P"<",
279 hyphen_ch_suffix = P">",
280 hyphen_ch_compound = P"=",
281 hyphen_ch_quality = P".",
282 hyphen_ch_uncategorized = P"·",
284 -- Kluster
286 -- Ein Kluster besteht aus einem oder mehreren aufeinanderfolgenden
287 -- `fundamentalen Klustern` ohne Unterbrechung durch Trennzeichen.
288 -- Es gibt drei Arten von fundamentalen Klustern:
290 -- * Buchstabenkluster,
291 -- * Ausdrücke für Alternativen,
292 -- * Ausdrücke für Spezialtrennungen,
294 -- Bü-cher E{ck/k-k}e Sto{ff/ff-f}et-zen Wach[-s/s-]tu-be
295 -- Kluster 11-2222 1111111111 11111111111111-222 1111111111111-22
296 -- fund. Kluster 11-2222 1222222223 11122222222233-444 1111222222233-44
298 cluster = (V"cl_letter" + V"cl_nonstd" + V"cl_alt")^1,
300 -- Buchstabenkluster
302 -- Ein Buchstabenkluster besteht aus aufeinanderfolgenden Buchstaben.
303 -- Die Capture sammelt alle Buchstaben.
304 cl_letter = C(V"letter"^1),
305 -- Die folgende Liste der zulässigen Buchstaben sollte mit der
306 -- Translate-Datei daten/german.tr für Patgen synchron gehalten
307 -- werden. Die explizite Liste der akzentuierten und
308 -- Sonderbuchstaben wurde mit dem Skript tr2lpeg.awk aus der Datei
309 -- german.tr erstellt. Dieser Vorgang ist bei Änderung der Datei
310 -- german.tr zu wiederholen. Zum Aufruf siehe tr2lpeg.awk.
312 -- Die folgenden Buchstaben sind zulässig.
313 letter = R("az", "AZ")
314 + P"ä" + P"Ä"
315 + P"â" + P"Â"
316 + P"à" + P"À"
317 + P"á" + P"Á"
318 + P"ã" + P"Ã"
319 + P"å" + P"Å"
320 + P"æ" + P"Æ"
321 + P"ç" + P"Ç"
322 + P"é" + P"É"
323 + P"è" + P"È"
324 + P"ë" + P"Ë"
325 + P"ê" + P"Ê"
326 + P"í" + P"Í"
327 + P"ì" + P"Ì"
328 + P"î" + P"Î"
329 + P"ï" + P"Ï"
330 + P"ñ" + P"Ñ"
331 + P"ó" + P"Ó"
332 + P"ò" + P"Ò"
333 + P"ô" + P"Ô"
334 + P"ö" + P"Ö"
335 + P"ø" + P"Ø"
336 + P"œ" + P"Œ"
337 + P"š" + P"Š"
338 + P"ß" / _property_has_eszett
339 + P"ú" + P"Ú"
340 + P"ü" + P"Ü"
341 + P"û" + P"Û"
342 + P"ÿ" + P"Ÿ"
343 + P"ž" + P"Ž"
346 -- Ausdrücke für Alternativen
348 -- Eine Alternative beschreibt mehrdeutige Wortteile, die
349 -- unterschiedlich getrennt werden können.
351 -- Ausdrücke für Alternativen werden in begrenzende Zeichen
352 -- (Klammern) eingeschlossen.
353 cl_alt = V"alt_open" * V"alt_rule" * V"alt_close",
354 -- Teilausdrücke einer Alternative werden durch ein spezielles
355 -- Zeichen voneinander getrennt. Beide Alternativen werden auf
356 -- Buchstabengleichheit geprüft. Die Capture enthält abschließend
357 -- die Buchstaben der ersten Alternative.
358 alt_rule = (V"alt_1st" * V"alt_sep" * V"alt_2nd") / _is_equal_two_alternatives,
359 -- Alternativen enthalten Alternativteilwörter. Die Unterschiede
360 -- zwischen Wörtern und Alternativteilwörtern sind:
362 -- * Alternativteilwörter können leer sein.
363 -- * Sie können eine führende und/oder abschließende
364 -- Trennstellenmarkierung haben.
365 -- * Die Capture von Alternativteilwörter enthält keine Trennzeichen.
366 alt_1st = V"alt_part_word",
367 alt_2nd = V"alt_part_word",
368 -- Ein Alternativteilwort ist ein Wort, welches eine führende und
369 -- abschließende Trennstellenmarkierung enthalten kann. Oder es ist
370 -- ein leeres Wort, wahlweise mit einem Trennzeichen. Die
371 -- Reihenfolge der Prüfung beider Möglichkeiten ist relevant. Auf
372 -- ein leeres Wort darf erst zuletzt geprüft werden.
373 alt_part_word = V"ophyphen" * V"word" / _concatenate_alt_part_word * V"ophyphen"
374 + C"" * V"ophyphen"
376 -- Eine optionale Trennstellenmarkierung.
377 ophyphen = V"hyphen"^-1,
378 -- Zeichen, welches einen Ausdruck für Alternativen einführt.
379 alt_open = P"[",
380 -- Zeichen, welches einen Ausdruck für Alternativen abschließt.
381 alt_close = P"]",
382 -- Zeichen, welches Teilausdrücke einer Alternative voneinander
383 -- trennt.
384 alt_sep = P"/",
386 -- Ausdrücke für Spezialtrennungen
388 -- Spezialtrennungen (non-standard hyphenation) beschreiben
389 -- Trennregeln, die über das bloße Einfügen eines Trennzeichens
390 -- hinausgehen. Dazu gehören die ck-Trennung und die
391 -- Dreikonsonantenregel(n). Spezialtrennungen sind jeweils
392 -- eingeschlossen in Klammern. Die Capture enthält eine
393 -- Zeichenkette, die der jeweils ungetrennten Spezialtrennung
394 -- entspricht (z. B. 'ck' für die ck-Trennung).
396 -- Ausdrücke für Spezialtrennungen werden in begrenzende Zeichen
397 -- (Klammern) eingeschlossen.
398 cl_nonstd = V"nonstd_open" * V"nonstd_rule" * V"nonstd_close",
399 -- Aufzählung sämtlicher Spezialtrennungen. Das Auftreten von
400 -- Spezialtrennungen wird als Worteigenschaft vermerkt.
401 nonstd_rule = (V"ck" + V"bbb" + V"fff" + V"lll" + V"mmm" + V"nnn" + V"ppp" + V"rrr" + V"ttt" + V"nonstd_sss") / _property_has_nonstd,
402 -- ck-Trennung: Die Capture enthält die Zeichenfolge 'ck'.
403 ck = C(P"ck") * V"nonstd_sep" * P"k" * V"hyphen" * P"k",
404 -- Dreikonsonantenregel: Die Capture enthält die Zeichenfolge für die
405 -- ungetrennte Konsonantenfolge.
406 bbb = C(P"bb") * V"nonstd_sep" * P"bb" * V"hyphen" * P"b",
407 fff = C(P"ff") * V"nonstd_sep" * P"ff" * V"hyphen" * P"f",
408 lll = C(P"ll") * V"nonstd_sep" * P"ll" * V"hyphen" * P"l",
409 mmm = C(P"mm") * V"nonstd_sep" * P"mm" * V"hyphen" * P"m",
410 nnn = C(P"nn") * V"nonstd_sep" * P"nn" * V"hyphen" * P"n",
411 ppp = C(P"pp") * V"nonstd_sep" * P"pp" * V"hyphen" * P"p",
412 rrr = C(P"rr") * V"nonstd_sep" * P"rr" * V"hyphen" * P"r",
413 ttt = C(P"tt") * V"nonstd_sep" * P"tt" * V"hyphen" * P"t",
414 sss = C(P"ss") * V"nonstd_sep" * P"ss" * V"hyphen" * P"s",
415 -- Das Auftreten der Dreikonsonantenregel für 's' wird als
416 -- Worteigenschaft vermerkt.
417 nonstd_sss = V"sss" / _property_has_nonstd_sss,
418 -- Zeichen, welches einen Ausdruck für Spezialtrennungen einführt.
419 nonstd_open = P"{",
420 -- Zeichen, welches einen Ausdruck für Spezialtrennungen abschließt.
421 nonstd_close = P"}",
422 -- Zeichen, welches Teilausdrücke einer Spezialtrennung voneinander
423 -- trennt.
424 nonstd_sep = P"/",
429 --- Prüfe String gegen Wortgrammatik.
430 -- Falls der String ein zulässiges Wort darstellt, wird eine Tabelle mit
431 -- Eigenschaften des Wortes zurückgegeben. Die Tabelle enthält im Feld
432 -- `norm_word` das Wort in einem für Patgen geeigneten Format.
433 -- Sämtliche Trennzeichen werden durch '<code>-</code>' ersetzt.
434 -- Spezialtrennungen und Alternativen werden zur ersten Altenative
435 -- aufgelöst.
437 -- @param rawword unbehandeltes Wort
439 -- @return <code>nil</code>, falls das Wort eine unzulässige Struktur
440 -- hat;<br /> eine Tabelle mit Eigenschaften des betrachteten Wortes,
441 -- sonst.
442 local function normalize_word(rawword)
443 -- Prüfung der Wortstruktur und Ermittlung der Worteigenschaften.
444 return word:match(rawword)
446 M.normalize_word = normalize_word
450 --- Prüfe ein Wort auf Wohlgeformtheit.
452 -- @param rawword Wort (normiert oder unbehandelt)
454 -- @return <code>nil, msg</code>, falls das Wort eine unzulässige
455 -- Struktur hat;<br /> eine Tabelle mit Eigenschaften des betrachteten
456 -- Wortes, sonst.<br /> <code>msg</code> ist ein String, der die
457 -- Unzulässigkeit näher beschreibt.
458 local function validate_word(rawword)
459 -- Normalisiere Wort.
460 local props = normalize_word(rawword)
461 -- Zulässiges Wort?
462 if not props then return nil, "ungültiges Wort" end
463 -- Prüfe auf unzulässige Alternativen.
464 if props.has_invalid_alt then return nil, "ungleiche Alternativen" end
465 -- Ermittle normalisiertes Wort.
466 local word = props.norm_word
467 -- Prüfe minimale Wortlänge.
468 local len = Ulen(Ugsub(word, "-", ""))
469 if len < 3 then return nil, "weniger als drei Buchstaben" end
470 if len == 3 and not props.has_eszett then return nil, "dreibuchstabig ohne Eszett" end
471 -- Prüfe Wortenden auf unzulässige Trennzeichen.
472 local len = Ulen(word)
473 -- Am Anfang.
474 local ch = Usub(word, 2, 2)
475 if ch == "-" then
476 -- Randtrennungen sind im Notentext zulässig.
478 -- Achtung: Unsaubere Implementierung! Dem Test auf
479 -- Randtrennungen für Notentext steht die Normalisierung aller
480 -- Trennzeichen (Umwandlung in das Zeichen -) in
481 -- normalize_word() entgegen. Hier wird daher das originale
482 -- Wort rawword geprüft.
484 -- Idee: Sinnvoller wäre es, die Grammatik eines Wortes so zu
485 -- erweitern, dass ein zusammengesetztes Wort aus, gegebenfalls
486 -- affigierten, einfachen oder zusammengesetzten Wörtern besteht,
487 -- die durch Wortfugentrennzeichen = verbunden sind.
488 -- Zusammengesetzte Wörter können wiederum affigiert sein
489 -- (Trennzeichen <, >, <=, =>). Einfache Wörter und Affixe können
490 -- Randtrennungen · enthalten. Randtrennzeichen werden bei der
491 -- Normalisierung entfernt (statt sie durch - zu ersetzen).
492 -- Eventuell kann auch die Länge von Buchstabenklustern um
493 -- (Nicht-)Randtrennzeichen innerhalb der Grammatik geprüft
494 -- werden. Auf diese Weise müsste hier nicht auf Wissen über die
495 -- Syntax von Trennzeichen zurückgegriffen werden bzw. die
496 -- nachträgliche Prüfung auf unzulässige, randnahe Trennzeichen
497 -- wird überflüssig.
499 -- Extrahiere Trennstellenmarkierung aus Originalwort ab
500 -- 2. Position.
501 local rch = Umatch(rawword, "^.([<>=.·-]+)")
502 -- Keine explizite Randtrennung?
503 if rch and not Ufind(rch, "·") then
504 return nil, "Trennzeichen am Wortanfang"
507 -- Am Ende.
508 local ch = Usub(word, len-1, len-1)
509 if ch == "-" then
510 -- Extrahiere Trennstellenmarkierung aus Originalwort, die bis zur
511 -- vorletzten Position reicht.
512 local rch = Umatch(rawword, "([<>=.·-]+).$")
513 -- Keine explizite Randtrennung?
514 if rch and not Ufind(rch, "·") then
515 return nil, "Trennzeichen am Wortende"
518 return props
520 M.validate_word = validate_word
524 -- Exportiere Modul-Tabelle.
525 return M