Python-Skripte: kleine Korrekturen.
[wortliste.git] / skripte / python / edit_tools / wortliste.py
blob9b16dd7ce0604e8b754841aac48f2d43d80cdac3
1 #!/usr/bin/env python3
2 # -*- coding: utf8 -*-
3 # :Copyright: © 2012 Günter Milde.
4 # :Licence: This work may be distributed and/or modified under
5 # the conditions of the `LaTeX Project Public License`,
6 # either version 1.3 of this license or (at your option)
7 # any later version.
8 # :Version: 0.1 (2012-02-07)
10 # wortliste.py
11 # ************
12 # ::
14 """Hilfsmittel für die Arbeit mit der `Wortliste`"""
16 # .. contents::
18 # Die hier versammelten Funktionen und Klassen dienen der Arbeit an und
19 # mit der freien `Wortliste der deutschsprachigen Trennmustermannschaft`_
20 # (wortliste_).
22 # Abhängigkeiten
23 # ==============
25 # Python 2.7 mit den Standardbibliotheken::
27 import difflib
28 import re
29 import unicodedata
30 import copy
31 import itertools
32 import sys, os
34 # append path to the "edit_tools" package
35 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
36 # Filter für Wahltrennungen (Re<s-pekt -> Re<spekt, …)::
37 from edit_tools.stilfilter import morphemisch
38 from edit_tools import stilfilter
41 # WordFile
42 # ========
44 # Klasse zum Lesen und Schreiben der `Wortliste`::
46 class WordFile():
48 # Initialisierung
49 # ----------------
51 # Das Argument ``format`` erwartet einen der Strings
53 # :f8: das originale, maximal 8-spaltige Wortlisten-Format (Langform_),
54 # :f5: das neue, maximal 5-spaltige Wortlisten-Format (Kurzform_), oder
55 # :auto: bestimme das Format automatisch.
57 # ::
59 def __init__(self, name, mode='r', encoding='utf8', format='f8'):
61 self.name = name
62 self.mode = mode
63 self.encoding = encoding
64 self.wordfile = open(name, mode=mode, encoding=encoding)
65 self.seek = self.wordfile.seek
66 # Dateiformat bestimmen und die Eintrags-Klasse setzen:
67 self.set_entry_class(format)
69 # Klasse zum Verarbeiten der Zeilen setzen.
70 # Mit self.format == "auto", werden die ersten Zeilen der Datei untersucht
71 # und der Pointer zurückgesetzt.
73 # ::
75 def set_entry_class(self, format, search_limit=1000):
77 assert(format in ('f8', 'f5', 'auto'))
78 self.format = format # default
80 self.entry_class = WordEntry
81 n = 0 # Zählindex
83 # Format anhand des Dateiinhalts bestimmen:
84 if format == 'auto':
85 # print("auto-determining word file format")
86 for e in self:
87 # nur ein Feld oder erstes Feld leer oder mit Trennzeichen:
88 if len(e) == 1 or e and re.search('[-=<>.·]', e[0]):
89 self.format = 'f5'
90 break
91 # mehr als 5 Felder: 8-Felder-Format (Langform)
92 elif len(e) > 5:
93 self.format = 'f8'
94 break
95 if n > search_limit:
96 break
97 n += 1
98 if self.format == 'f5':
99 self.entry_class = ShortEntry
101 self.wordfile.seek(0) # Dateizeiger zurücksetzen
104 # Iteration
105 # ---------
107 # Die spezielle Funktion `__iter__` wird aufgerufen wenn über eine
108 # Klasseninstanz iteriert wird.
109 # Sie liefert einen Iterator über die "geparsten" Zeilen (Datenfelder)
110 # ::
112 def __iter__(self):
113 entry_class = self.entry_class
114 for line in self.wordfile:
115 yield entry_class(line.rstrip())
118 # asdict()
119 # --------
121 # Gib ein `dictionary` (assoziatives Array) mit ungetrenntem Wort als
122 # Schlüssel (`key`) und den Datenfeldern als `value` zurück::
124 def asdict(self, lowkey=False):
125 words = {}
126 for entry in self:
127 key = entry.key()
128 if key == '#': # reiner Kommentar
129 try:
130 words['#'].append(entry.comment)
131 except KeyError:
132 words['#'] = entry.comment
133 elif lowkey:
134 words[key.lower()] = entry
135 else:
136 words[key] = entry
137 return words
139 # writelines()
140 # ------------
142 # Schreibe eine Liste von `unicode` Strings (Zeilen ohne Zeilenendezeichen)
143 # in die Datei `destination`::
145 def writelines(self, lines, destination, encoding=None):
146 outfile = open(destination, 'w',
147 encoding=(encoding or self.encoding))
148 outfile.writelines(lines)
150 # write_entries()
151 # ---------------
153 # Schreibe eine Liste von Einträgen (WordEntry_ oder ShortEntry_ Objekten) in
154 # die Datei `destination`::
156 def write_entries(self, wortliste, destination, encoding=None):
157 lines = (str(entry) for entry in wortliste)
158 self.writelines(lines, destination, encoding)
161 # WordEntry
162 # =========
164 # Klasse für Einträge (Zeilen) der Wortliste_ in Langform_.
165 # Jede Zeile enthält einen Eintrag mit durch Semikolon „;“
166 # getrennten Feldern.
168 # Beispiel:
170 # >>> from wortliste import WordEntry
172 # >>> aalbestand = WordEntry('Aalbestand;Aal=be<stand # Test')
173 # >>> print(aalbestand)
174 # Aalbestand;Aal=be<stand # Test
176 # .. _Langform:
177 # .. _8-Spalten-Format:
179 # Bedeutung der Felder im 8-Spalten-Format (Langform):
181 # 1. Wort ungetrennt
182 # 2. Wort mit Trennungen, falls für alle Varianten identisch,
183 # anderenfalls leer
184 # 3. falls Feld 2 leer, Trennung nach traditioneller Rechtschreibung
185 # 4. falls Feld 2 leer, Trennung nach reformierter Rechtschreibung (2006)
186 # 5. falls Feld 2 leer, Trennung für Wortform, die entweder in
187 # der Schweiz oder mit Großbuchstaben oder Kapitälchen benutzt wird
188 # und für traditionelle und reformierte Rechtschreibung identisch ist
189 # 6. falls Feld 5 leer, Trennung für Wortform, die entweder in
190 # der Schweiz oder mit Großbuchstaben oder Kapitälchen benutzt wird,
191 # traditionelle Rechtschreibung
192 # 7. falls Feld 5 leer, Trennung für Wortform, die entweder in
193 # der Schweiz oder mit Großbuchstaben oder Kapitälchen benutzt wird,
194 # reformierte Rechtschreibung (2006)
195 # 8. Falls Feld 5 leer und nicht identisch mit Feld 6, Trennung
196 # nach (deutsch)schweizerischer, traditioneller Rechtschreibung
197 # mit Trennung von „ss“, auch wenn es für „ß“ steht.
199 # ::
201 class WordEntry(list):
203 # Argumente
204 # ---------
206 # feldnamen, feldindizes
207 # ~~~~~~~~~~~~~~~~~~~~~~
209 # Benennung der Felder mit Sprachkürzeln (Tags nach [BCP47]_).
210 # (Die Zählung der Indizes beginnt in Python bei 0.)::
212 feldnamen = ('key', 'de', 'de-1901', 'de-1996', 'de-x-versal',
213 'de-1901-x-versal', 'de-1996-x-versal', 'de-CH-1901')
215 feldindizes = {
216 'key': 0, # Schlüssel
217 'de': 1, # Deutsch, allgemeingültig
218 'de-1901': 2, # "traditionell" (nach Rechtschreibreform 1901)
219 'de-1996': 3, # reformierte Reformschreibung (1996)
220 'de-x-versal': 4, # ohne ß (Schweiz oder versal) allgemein
221 'de-CH': 4, # Alias
222 'de-1901-x-versal': 5, # ohne ß (Schweiz oder versal) "traditionell"
223 'de-1996-x-versal': 6, # ohne ß (Schweiz oder versal) "reformiert"
224 'de-CH-1996': 6, # Alias
225 'de-CH-1901': 7, # ohne ß (Schweiz) "traditionell" ("süssauer")
229 # ersatzfelder
230 # ~~~~~~~~~~~~
232 # Bei der Auswahl eines Wortes in einer Rechtschreibvariante werden
233 # "generische" Felder gewählt, falls das "Originalfeld" fehlt. Die folgenden
234 # Tupel listen die Ersatzfelder für das ensprechende Originalfeld.
236 # Bsp: Ersatz-Felder für 'de-CH-1901':
238 # >>> for i in WordEntry.ersatzfelder[WordEntry.feldindizes['de-CH-1901']]:
239 # ... print(WordEntry.feldnamen[i])
240 # de-1901-x-versal
241 # de-x-versal
242 # de-1901
243 # de
245 # ::
247 # Ersatzindizes Index Nr Name
248 ersatzfelder = (tuple(), # 0 1 key
249 tuple(), # 1 2 'de'
250 (1,), # 2 3 'de-1901'
251 (1,), # 3 4 'de-1996'
252 (1,), # 4 5 'de-x-versal'
253 (4, 2, 1), # 5 6 'de-1901-x-versal'
254 (4, 3, 1), # 6 7 'de-1996-x-versal'
255 (5, 4, 2, 1), # 7 8 'de-CH-1901'
259 # comment
260 # ~~~~~~~
262 # Kommentar, Vorgabe ist ein leerer String::
264 comment = ''
267 # Initialisierung
268 # ---------------
269 # ::
272 def __init__(self, line, delimiter=';'):
274 self.delimiter = delimiter
276 # eventuell vorhandenen Kommentar abtrennen und speichern::
278 if '#' in line:
279 line, comment = line.split('#', 1)
280 self.comment = comment.strip()
281 line = line.rstrip()
282 # print(line, self.comment)
284 if not line: # kein Inhalt
285 return
287 # Zerlegen in Datenfelder, in Liste eintragen::
289 for field in line.split(delimiter):
290 if field.startswith('-'):
291 self.append('')
292 else:
293 self.append(field)
296 # Rückverwandlung in String
297 # -------------------------
299 # Erzeugen eines Eintrag-Strings (ohne Zeilenendezeichen) aus der Liste der
300 # Datenfelder und dem Kommentar
302 # __str__()
303 # ~~~~~~~~~
305 # >>> str(aalbestand)
306 # 'Aalbestand;Aal=be<stand # Test'
308 # >>> grse = WordEntry('Grüße;Grü-ße')
309 # >>> str(grse)
310 # 'Grüße;Grü-ße'
312 # >>> leerkommentar = WordEntry('# Testkommentar ')
313 # >>> leerkommentar, str(leerkommentar)
314 # (WordEntry('# Testkommentar'), '# Testkommentar')
316 # ::
318 def __str__(self):
319 # Nummerieren leerer Felder:
320 fields = [field or '-%d-' % (i+1)
321 for (i, field) in enumerate(self)]
322 line = ';'.join(fields)
323 if self.comment:
324 line += ' # ' + self.comment
325 if not self:
326 line = line.lstrip()
327 return line
329 # __repr__()
330 # ~~~~~~~~~~
331 # >>> print(type(repr(grse)), repr(grse))
332 # <class 'str'> WordEntry('Grüße;Grü-ße')
334 # ::
336 def __repr__(self):
337 s = '%s(\'%s\')' % (self.__class__.__name__, self)
338 return s
340 # lang_index()
341 # ------------
343 # Bestimme den Index des zur Sprachvariante gehörenden Datenfeldes unter
344 # Verwendung der Ersatzfelder:
346 # >>> aalbestand.lang_index('de')
348 # >>> aalbestand.lang_index('de-1901')
350 # >>> aalbestand.lang_index('de-1996')
352 # >>> aalbestand.lang_index('de-x-versal')
354 # >>> aalbestand.lang_index('de-1901-x-versal')
356 # >>> aalbestand.lang_index('de-1996-x-versal')
358 # >>> abbeissen = WordEntry(
359 # ... 'abbeissen;-2-;-3-;-4-;-5-;ab<bei-ssen;ab<beis-sen;ab<beis-sen')
360 # >>> print(abbeissen.lang_index('de'))
361 # None
362 # >>> print(abbeissen.lang_index('de-x-versal'))
363 # None
364 # >>> abbeissen.lang_index('de-1901-x-versal')
366 # >>> abbeissen.lang_index('de-1996-x-versal')
368 # >>> abbeissen.lang_index('de-CH-1901')
370 # >>> urlaubstipp = WordEntry('Urlaubstipp;-2-;-3-;Ur<laubs=tipp')
371 # >>> print(urlaubstipp.lang_index('de'))
372 # None
373 # >>> print(urlaubstipp.lang_index('de-1901'))
374 # None
375 # >>> print(urlaubstipp.lang_index('de-1996'))
377 # >>> print(urlaubstipp.lang_index('de-x-versal'))
378 # None
379 # >>> print(urlaubstipp.lang_index('de-1901-x-versal'))
380 # None
382 # ::
384 def lang_index(self, lang):
386 if lang not in self.feldindizes:
387 raise ValueError('Sprachvariante "%s" nicht in %s'
388 % (lang, self.feldindizes.keys()))
390 # Einfacher Fall: eine allgemeine Schreibweise::
392 if len(self) == 2:
393 if 'ß' in self[0] and ('CH' in lang or 'versal' in lang):
394 return None
395 else:
396 return 1
398 # Spezielle Schreibung::
400 try:
401 i = self.feldindizes[lang]
402 feld = self[i]
403 except IndexError:
404 if lang == 'de-CH-1901':
405 return self.lang_index('de-1901-x-versal')
406 # Allgemeine Schweiz/versal Schreibung:
407 if i > 4 and len(self) == 5:
408 return 4
409 # versal == normal (kein sz):
410 if len(self) == 4 and 'ß' not in self[0]:
411 if lang in ('de-CH-1901', 'de-1901-x-versal'):
412 return self.lang_index('de-1901')
413 elif lang in ('de-CH-1996', 'de-1996-x-versal'):
414 return self.lang_index('de-1996')
415 return None # Feld nicht vorhanden
417 if not feld: # leeres Feld
418 return None
420 return i
422 # key()
423 # -----
425 # Gib einen Schlüssel (ungetrenntes Wort) zurück.
427 # Bei Einträgen im Langform_ ist der Schlüssel gleich dem Inhalt des ersten
428 # Feldes. Die Funktion dient zur Kompatibilität mit ShortEntry_.
430 # >>> print(WordEntry('Fluss;Quatsch').key())
431 # Fluss
432 # >>> print(WordEntry('Dreß;-1-;Dreß;-3-').key())
433 # Dreß
435 # Das `lang` Argument wird ignoriert, da der Schlüssel (im Gegensatz zur
436 # Kurzform_) stets eindeutig ist:
438 # >>> print(WordEntry('Dreß;-1-;Dreß;-3-').key('de-1996'))
439 # Dreß
441 # Der Schlüssel eines leeren Eintrags ist ein leerer String, der eines leeren
442 # Kommentars das Kommentarzeichen:
444 # >>> WordEntry('').key(), WordEntry('# toller Kommentar').key()
445 # ('', '#')
447 # ::
449 def key(self, lang=None):
450 """Gib den Schlüssel (ungetrenntes Wort) zurück."""
451 try:
452 return self[0]
453 except IndexError: # reiner Kommentar oder leerer Eintrag
454 if self.comment:
455 return '#'
456 return ''
459 # getitem()
460 # ---------
462 # Gib Feld ``i`` oder Ersatzfeld zurück.
463 # ::
465 def getitem(self, i):
466 """Return item ``i`` or a subsititute"""
467 try:
468 return self[i]
469 except IndexError: # Feld i nicht vorhanden
470 pass
471 # Ersatzregeln anwenden
472 for _i in self.ersatzfelder[i]:
473 try:
474 word = self[_i]
475 except IndexError: # Feld i nicht vorhanden
476 continue # nächsten `tag` versuchen
477 # Spezialfall: in Versalschreibung ungültige Ersatz-Wörter
478 if i >= 4 and _i <4 and 'ß' in word:
479 return ''
480 return word
481 return ''
483 # .. _WordEntry.get():
485 # get()
486 # -----
488 # Trennmuster für eine Sprachvariante ausgeben:
490 # >>> aalbestand.get('de')
491 # 'Aal=be<stand'
492 # >>> aalbestand.get('de-1901')
493 # 'Aal=be<stand'
494 # >>> aalbestand.get('de-1996')
495 # 'Aal=be<stand'
496 # >>> aalbestand.get('de-x-versal')
497 # 'Aal=be<stand'
498 # >>> aalbestand.get('de-1901-x-versal')
499 # 'Aal=be<stand'
500 # >>> aalbestand.get('de-1996-x-versal')
501 # 'Aal=be<stand'
502 # >>> aalbestand.get('de-CH-1901')
503 # 'Aal=be<stand'
505 # >>> weste = WordEntry('Weste;-2-;We-ste;Wes-te')
506 # >>> weste.get('de')
507 # ''
508 # >>> weste.get('de-1901')
509 # 'We-ste'
510 # >>> weste.get('de-1996')
511 # 'Wes-te'
513 # >>> abbeissen.get('de')
514 # ''
515 # >>> abbeissen.get('de-x-versal')
516 # ''
517 # >>> abbeissen.get('de,de-x-versal')
518 # ''
519 # >>> abbeissen.get('de-1901-x-versal')
520 # 'ab<bei-ssen'
521 # >>> abbeissen.get('de,de-1901,de-1901-x-versal')
522 # 'ab<bei-ssen'
523 # >>> abbeissen.get('de-CH-1901')
524 # 'ab<beis-sen'
526 # ::
528 def get(self, tags):
529 for lang in tags.split(','):
530 word = self.getitem(self.feldindizes[lang])
531 if word:
532 break
533 else:
534 return ''
535 return word
538 # set()
539 # -----
541 # Trennmuster für Sprachvariante setzen:
543 # >>> abbeissen.set('test', 'de-1901-x-versal')
544 # >>> print(abbeissen)
545 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
547 # >>> abbeissen.set('test', 'de-1901')
548 # Traceback (most recent call last):
549 # ...
550 # IndexError: kann kein leeres Feld setzen
552 # >>> abbeissen.set('test', 'de-1901,de-1901-x-versal')
553 # >>> print(abbeissen)
554 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
556 # ::
558 def set(self, word, tags):
559 for lang in tags.split(','):
560 i = self.lang_index(lang)
561 if i is None:
562 continue
563 if word is None:
564 word = ''
565 self[i] = word
566 return
567 raise IndexError("kann kein leeres Feld setzen")
569 # setitem()
570 # ---------
572 # Feld ``i`` setzen:
574 # >>> entry = WordEntry('test;test')
575 # >>> entry.setitem(4, 's-x')
576 # >>> print(entry)
577 # test;test;-3-;-4-;s-x
578 # >>> entry.setitem(3, 'sz')
579 # >>> print(entry)
580 # test;test;-3-;sz;s-x
582 # ::
584 def setitem(self, i, word):
585 while len(self) < i+1:
586 self.append('')
587 self[i] = word
590 # complete()
591 # ----------
593 # Alle Felder setzen:
595 # >>> print(WordEntry('Ruhe;Ru-he').completed())
596 # Ruhe;Ru-he;Ru-he;Ru-he;Ru-he;Ru-he;Ru-he;Ru-he
598 # >>> print(aalbestand, len(aalbestand))
599 # Aalbestand;Aal=be<stand # Test 2
600 # >>> print(aalbestand.completed())
601 # Aalbestand;Aal=be<stand;Aal=be<stand;Aal=be<stand;Aal=be<stand;Aal=be<stand;Aal=be<stand;Aal=be<stand # Test
603 # >>> auffrass = WordEntry('auffrass;-2-;-3-;-4-;auf-frass')
604 # >>> print(auffrass.completed())
605 # auffrass;-2-;-3-;-4-;auf-frass;auf-frass;auf-frass;auf-frass
607 # >>> fresssack= WordEntry('Fresssack;-2-;-3-;Fress=sack;Fress=sack')
608 # >>> print(fresssack.completed())
609 # Fresssack;-2-;-3-;Fress=sack;Fress=sack;Fress=sack;Fress=sack;Fress=sack
611 # >>> line = 'Flussschiffahrt;-2-;-3-;-4-;-5-;Fluss=schi{ff/ff=f}ahrt;-7-'
612 # >>> print(WordEntry(line).completed())
613 # Flussschiffahrt;-2-;-3-;-4-;-5-;Fluss=schi{ff/ff=f}ahrt;-7-;Fluss=schi{ff/ff=f}ahrt
615 # >>> line = 'Ackerstraße;-2-;A{ck/k-k}er=stra-ße;Acker=stra-ße'
616 # >>> ackerstrasse = WordEntry(line).completed()
617 # >>> print(ackerstrasse)
618 # Ackerstraße;-2-;A{ck/k-k}er=stra-ße;Acker=stra-ße;-5-;-6-;-7-;-8-
620 # ::
622 def complete(self):
623 for i in range(len(self),8):
624 self.append(self.getitem(i))
626 # .. _WordEntry.completed():
628 # completed()
629 # -----------
631 # Gib eine vervollständigte Kopie des Eintrags zurück.
633 # ::
635 def completed(self):
636 """Return expanded copy of self."""
637 neu = copy.copy(self)
638 neu.complete()
639 return neu
641 # .. _ShortEntry.prune():
643 # prune()
644 # -------
646 # Eintrag kürzen.
648 # Felder für Sprachvarianten zusammenfassen, wenn gleich:
650 # >>> aalbestand.prune()
651 # >>> print(aalbestand)
652 # Aalbestand;Aal=be<stand # Test
653 # >>> auffrass.prune()
654 # >>> print(auffrass)
655 # auffrass;-2-;-3-;-4-;auf-frass
656 # >>> entry = WordEntry('distanziert;-2-;di-stan-ziert;di-stan-ziert')
657 # >>> entry.prune()
658 # >>> print(entry)
659 # distanziert;di-stan-ziert
660 # >>> entry = WordEntry('Gauss;-2-;Gauss;Gauss;Gauss')
661 # >>> entry.prune()
662 # >>> print(entry)
663 # Gauss;Gauss
664 # >>> fresssack.prune()
665 # >>> print(fresssack)
666 # Fresssack;-2-;-3-;Fress=sack;Fress=sack
668 # >>> masse = WordEntry('Masse;Mas-se;Mas-se;Mas-se;Mas-se;Mas-se;Mas-se;Mas-se')
669 # >>> masse.prune()
670 # >>> print(masse)
671 # Masse;Mas-se
673 # Aber nicht, wenn die Trennstellen sich unterscheiden:
675 # >>> abenddienste = WordEntry(
676 # ... 'Abenddienste;-2-;Abend=dien-ste;Abend=diens-te')
677 # >>> abenddienste.prune()
678 # >>> print(abenddienste)
679 # Abenddienste;-2-;Abend=dien-ste;Abend=diens-te
680 # >>> ackerstrasse.prune()
681 # >>> print(ackerstrasse)
682 # Ackerstraße;-2-;A{ck/k-k}er=stra-ße;Acker=stra-ße
683 # >>> entry = WordEntry(
684 # ... 'Schlammmasse;-2-;-3-;Schlamm=mas-se;-5-;-6-;Schlamm=mas-se;-8-')
685 # >>> entry.prune()
686 # >>> print(entry)
687 # Schlammmasse;-2-;-3-;Schlamm=mas-se
688 # >>> entry = WordEntry(
689 # ... 'Flussschiffahrt;-2-;-3-;-4-;-5-;Fluss=schi{ff/ff=f}ahrt;-7-')
690 # >>> entry.prune()
691 # >>> print(entry)
692 # Flussschiffahrt;-2-;-3-;-4-;-5-;Fluss=schi{ff/ff=f}ahrt;-7-
694 # ::
696 def prune(self):
697 if len(self) == 8:
698 if self[7] == self[5]: # de-CH-1901 gleich versal-1901
699 self.pop()
700 if len(self) == 7:
701 if self[6] == self[5]:
702 self[4] = self[5] # umschreiben auf versal-allgemein
703 self.pop()
704 self.pop()
705 elif (not(self[4] or self[5] or self[6])
706 or self[5] == self[2] and self[6] == self[3]):
707 # alle leer oder ohne ß-Ersetzung
708 self.pop()
709 self.pop()
710 self.pop()
711 if len(self) == 5:
712 if not self[4]:
713 self.pop()
714 elif self[1] == self[4]: # de-x-versal == de
715 self.pop()
716 self.pop()
717 self.pop()
718 elif self[2] == self[3] == self[4]:
719 self[1] = self[2] # Umschreiben auf de (allgemein)
720 self.pop()
721 self.pop()
722 self.pop()
723 if len(self) == 4:
724 if self[3] == self[2]: # de-1996 == de-1901
725 self[1] = self[2] # Umschreiben auf de (allgemein)
726 self.pop()
727 self.pop()
729 if len(self) > 6:
730 self[4] = ''
731 if len(self) > 2:
732 self[1] = ''
735 # pruned()
736 # -----------
738 # Gib eine gekürzte Kopie des Eintrags zurück.
739 # ::
741 def pruned(self):
742 """Return pruned copy of self."""
743 neu = copy.copy(self)
744 neu.prune()
745 return neu
747 # .. _WordEntry.merge():
749 # merge()
750 # -------
752 # Einträge zusammenfassen:
754 # >>> entry = WordEntry('Dienste;-2-;Dien-ste')
755 # >>> entry.merge(WordEntry('Dienste;-2-;-3-;diens-te'))
756 # >>> print(entry)
757 # Dienste;-2-;Dien-ste;Diens-te
758 # >>> entry = WordEntry('Abenddress;Abend=dress')
759 # >>> entry.merge(WordEntry('Abenddress;-2-;-3-;-4-;Abend=dress'))
760 # >>> print(entry)
761 # Abenddress;Abend=dress
762 # >>> entry = WordEntry('Gauss;Gauss')
763 # >>> entry.merge(WordEntry('Gauss;-2-;-3-;-4-;Gauss'))
764 # >>> print(entry)
765 # Gauss;Gauss
766 # >>> masse.merge(WordEntry('masse;-2-;-3-;-4-;-5-;Ma-sse;Mas-se;Mas-se'),
767 # ... allow_alternatives=True)
768 # >>> print(masse)
769 # Masse;-2-;Mas-se;Mas-se;-5-;Ma[s-/-s]se;Mas-se;Mas-se
771 # ::
773 def merge(self, other, allow_alternatives=False, prune=True,
774 merge_comments=False, start=0, stop=None):
775 self.complete()
776 other.complete()
777 islower = self.key().islower()
778 conflict = False
779 # `stop=None` oder `stop=0` bedeutet "keine Obergrenze".
780 stop = stop or len(self)
781 for i in range(start, stop):
782 o_i = other[i]
783 if not o_i:
784 continue
785 if islower != o_i.islower():
786 o_i = toggle_case(o_i)
787 s_i = self[i]
788 if not s_i:
789 self[i] = o_i
790 elif s_i != o_i:
791 if s_i.lower() == o_i.lower():
792 self[i] = toggle_case(o_i)
793 elif allow_alternatives:
794 self[i] = alternatives(s_i, o_i) # '[%s/%s]' % (s_i, o_i)
795 else:
796 conflict = True
797 if merge_comments and not conflict and self.comment != other.comment:
798 if allow_alternatives and self.comment and other.comment:
799 self.comment += ' / ' + other.comment
800 else:
801 self.comment = self.comment or other.comment
803 if conflict:
804 raise AssertionError('Merge Error:\n %s\n %s'
805 % (str(self), str(other)))
807 if prune:
808 other.prune()
809 self.prune()
812 # regelaenderungen()
813 # ------------------
815 # Teste Felder auf Konsistenz mit den Regeländerungen der Orthographiereform
816 # 1996, ändere Unstimmigkeiten `in place`.
818 # Die neuere Funktion `ableitung1901()`_, deckt mehr Ausnahmen ab aber
819 # funktioniert nur in eine Richtung.
821 # Falls ein generisches Feld von Änderung betroffen ist, schreibe die
822 # korrekten Trennungen in die spezifischen Felder.
824 # >>> entry = WordEntry('Würste;Wür-ste')
825 # >>> entry.regelaenderungen()
826 # >>> print(entry)
827 # Würste;-2-;Wür-ste;Würs-te
828 # >>> entry = WordEntry('Würste;Würs-te')
829 # >>> entry.regelaenderungen()
830 # >>> print(entry)
831 # Würste;-2-;Wür-ste;Würs-te
832 # >>> entry = WordEntry('Hecke;He-cke')
833 # >>> entry.regelaenderungen()
834 # >>> print(entry)
835 # Hecke;-2-;He{ck/k-k}e;He-cke
836 # >>> entry = WordEntry('Ligusterhecke;Ligu-ster=he{ck/k-k}e')
837 # >>> entry.regelaenderungen()
838 # >>> print(entry)
839 # Ligusterhecke;-2-;Ligu-ster=he{ck/k-k}e;Ligus-ter=he-cke
841 # Bei Änderungen der Schreibung wird das entsprechende Feld ausgekreuzt:
843 # >>> entry = WordEntry('Hass;Hass')
844 # >>> entry.regelaenderungen()
845 # >>> print(entry)
846 # Hass;-2-;-3-;Hass;Hass
847 # >>> entry = WordEntry('fasst;fasst')
848 # >>> entry.regelaenderungen()
849 # >>> print(entry)
850 # fasst;-2-;-3-;fasst;fasst
851 # >>> entry = WordEntry('Missbrauch;Miss<brauch')
852 # >>> entry.regelaenderungen()
853 # >>> print(entry)
854 # Missbrauch;-2-;-3-;Miss<brauch;Miss<brauch
855 # >>> entry = WordEntry('schlifffest;schliff=fest')
856 # >>> entry.regelaenderungen()
857 # >>> print(entry)
858 # schlifffest;-2-;-3-;schliff=fest
860 # Bei gleicher, nicht betroffener Trennung werden Felder zusammengefasst:
862 # >>> entry = WordEntry('austoben;-2-;aus<to-ben;aus<to-ben')
863 # >>> entry.regelaenderungen()
864 # >>> print(entry)
865 # austoben;aus<to-ben
867 # Achtung: nicht narrensicher -- Ausnahmen und Mehrdeutigkeiten werden nicht
868 # alle erkannt:
870 # >>> entry = WordEntry('Boss;Boss # engl.') # korrekt
871 # >>> entry.regelaenderungen()
872 # >>> print(entry)
873 # Boss;Boss # engl.
875 # >>> entry = WordEntry('Ästhesie;Äs-the-sie') # Trennung von "sth" erlaubt
876 # >>> entry.regelaenderungen()
877 # >>> print(entry)
878 # Ästhesie;Äs-the-sie
880 # >>> entry = WordEntry('Fluß;Fluß') # in de-1996 nur Fluss
881 # >>> entry.regelaenderungen()
882 # >>> print(entry)
883 # Fluß;Fluß
885 # ::
887 def regelaenderungen(self):
888 wort01 = self.get('de-1901')
889 wort96 = self.get('de-1996')
890 w_versal = None
892 if wort01 is None or wort96 is None or 'engl.' in self.comment:
893 return
895 # Trennregeländerungen:
897 # Trennung von ck:
898 wort01 = wort01.replace('-ck', '{ck/k-k}')
899 wort96 = wort96.replace('{ck/k-k}', '-ck')
901 # Trenne nie st:
902 wort01 = re.sub('(?<!s)s-t(?!h)', '-st', wort01)
903 wort96 = wort96.replace('-st', 's-t')
905 # kein Schluss-ss und sst in de-1901 (ungetrenntes "ss" nur in Ausnahmen)
906 # aber: 'ßt' und Schluß-ß auch in de-1996 möglich (langer Vokal)
907 if 'ss' in wort01:
908 w_versal = wort01
909 wort01 = None
911 # Dreikonsonantenregel:
912 if wort01 and re.search(r'(.)\1=\1', wort01):
913 wort01 = None
915 # Speichern:
916 if wort01 == wort96: # keine Regeländerung im Wort
917 if len(self) > 2:
918 self.prune()
919 return
921 if wort01 is None:
922 self.extend( ['']*(4-len(self)) )
923 self[1] = ''
924 self[2] = ''
925 self[3] = wort96
926 else:
927 self.extend( ['']*(4-len(self)) )
928 self[1] = ''
929 self[2] = wort01
930 self[3] = wort96
931 if w_versal:
932 self.append(w_versal)
935 # ShortEntry
936 # ==========
938 # Klasse für Einträge (Zeilen) der Wortlisten im `5-Felder-Format`_ (Kurzform).
940 # ::
942 class ShortEntry(WordEntry):
943 """Entry of the German hyphenation database file (5 fields)."""
945 # .. _Kurzform:
947 # 5-Felder-Format
948 # ---------------
950 # Ein vollständiger Eintrag enthält fünf, durch Semikolon getrennte, Felder
951 # für die Sprachvarianten `de`, `de-1901`, `de-CH`, `de-1901-x-versal` und
952 # `de-CH-1901` (Tags nach [BCP47]_).
954 # Jedes Feld enthält ein Wort mit Kennzeichnung der Trennstellen im Format
955 # der `Wortliste der deutschsprachigen Trennmustermannschaft`_, z.B.
956 # ``Pro<zent=zah-len``.
958 # Felder können weggelassen werden, wenn sich der Inhalt aus einem
959 # allgemeineren Feld gewinnen lässt (siehe `Ersatzregeln`_).
962 # Feldbelegung
963 # ~~~~~~~~~~~~
965 # Die Felder beschreiben die Schreibung und Trennung eines Wortes gemäß
966 # der Sprachvarianten:
968 # 1. `de`: `aktuelle Rechtschreibung`_ wie sie in Deutschland und Österreich
969 # angewendet wird.
971 # Beispiele: ``Diens-te``, ``ba-cken``, ``Grü-ße``, ``Schluss=satz``
973 # 2. `de-1901`: `traditionelle Rechtschreibung`_ wie sie in Deutschland und
974 # Österreich von 1901 bis 1996 gültig war.
976 # Beispiele: ``Dien-ste``, ``ba{ck/k-k}en``, ``Grü-ße``, ``Schluß=satz``
978 # 3. `de-CH` oder `de-x-versal`: aktuelle Rechtschreibung wie sie in der
979 # Schweiz und bei ß-Ersatzschreibung angewendet wird.
981 # Beispiele: ``ba-cken``, ``Grüs-se``, ``Schluss=satz``
983 # 4. `de-1901-x-versal`: traditionelle Rechtschreibung mit
984 # ß-Ersatzschreibung wie sie in Deutschland und Österreich
985 # angewendet wurde (keine Trennung von „ss“ als Ersatz für „ß“).
987 # Beispiele: ``ba{ck/k-k}en``, ``Grü-sse``, ``Schluss=satz``
989 # 5. `de-CH-1901`: traditionelle Rechtschreibung wie sie in der Schweiz
990 # angewendet wurde (Trennung von „ss“ auch wenn es für „ß“ steht aber
991 # keine Dreikonsonantenregel für „ss=s“).
993 # Beispiele: ``ba{ck/k-k}en``, ``Grüs-se``, ``Schluss=satz``
996 # Ersatzregeln
997 # ~~~~~~~~~~~~
999 # Feld 1 (`de`) ist ein Pflichtfeld, die anderen Felder können weggelassen
1000 # werden, wenn sich der Inhalt über regelmäßige Transformationen_ aus einem
1001 # anderen Feld gewinnen lässt:
1003 # ==================== ============= =======================
1004 # Feld Quelle Transformation
1005 # ==================== ============= =======================
1006 # 1 de
1007 # 2 de-1901 1 de „Rechtschreibreversion_“
1008 # 3 de-CH 1 de SZ-Ersatz_
1009 # 4 de-1901-x-versal 2 de-1901 SZ-Ersatz_
1010 # 5 de-CH-1901 2 de-1901 SZ-Ersatz_
1011 # ==================== ============= =======================
1013 # Die Ersetzung erfolgt rekursiv (d.h. wenn Feld 2 nicht gegeben ist, können
1014 # die Felder 4 und 5 aus Feld 1 mit „Rechtschreibreversion_“ und SZ-Ersatz_
1015 # bestimmt werden).
1017 # Wenn ein Eintrag in einer Sprachvariante leer bleiben soll, muss das
1018 # zugehörige Feld entsprechend markiert („ausgekreuzt“) werden.
1019 # (Da die ß-Ersatzschreibung für alle Wörter definiert ist, ist in einer
1020 # vollständigen Liste das Auskreuzen der Spalten 3…5 nicht erforderlich.)
1022 # Beispiele: ``-1-;de<pla-ziert``, ``auf<wän-dig;-2-;``.
1025 # Implementiert in `ShortEntry.getitem()`_.
1028 # Rechtschreibreversion
1029 # """""""""""""""""""""
1031 # Überführung eines Wortes in die Sprachvariante de-1901 durch
1032 # Anwendung der mit der Rechtschreibreform 1996 entfallenen Regeln:
1034 # st-Trennung_:
1035 # Trenne nie "st". (Aber "s-th" ist trennbar.)
1036 # ck-Trennung_:
1037 # Trenne "ck" als "k-k".
1038 # Schluss-ss_:
1039 # "ß", "ßt" und "ßl" statt "ss", "sst" und "ssl" am Silbenende.
1040 # Dreikonsonantenregel_:
1041 # Von drei gleichen Konsonanten vor einem Selbstlaut entfällt einer,
1042 # taucht aber bei Worttrennung wieder auf.
1044 # Abweichende Änderungen müssen als Ausnahmen explizit erfasst werden.
1046 # Implementiert in `ableitung1901()`_.
1048 # SZ-Ersatz
1049 # """""""""
1050 # Ersetze 'ß' mit 'ss', trenne je nach Sprachvarietät (siehe Feldbelegung_).
1052 # Implementiert in `versalschreibung()`_.
1056 # Beispiel
1057 # --------
1059 # Durch die Ersatzregeln reicht für die meisten Einträge die Angabe des ersten
1060 # Feldes, z.B.
1062 # >>> from wortliste import ShortEntry
1063 # >>> entry = ShortEntry('Pass=stra-ße')
1065 # Eintrag vervollständigen mit `ShortEntry.complete()`_:
1067 # >>> entry.complete(); print(entry)
1068 # Pass=stra-ße;Paß=stra-ße;Pass=stras-se;Pass=stra-sse;Pass=stras-se
1070 # Eintrag kürzen mit `ShortEntry.prune()`_:
1072 # >>> entry.prune(); print(entry)
1073 # Pass=stra-ße
1075 # Wort mit Unregelmäßigkeiten:
1077 # >>> entry = ShortEntry('Boss;Boss;Boss;Boss;Boss # en.')
1078 # >>> entry.prune(); print(entry)
1079 # Boss;Boss # en.
1082 # Argumente
1083 # ---------
1085 # feldnamen
1086 # ~~~~~~~~~
1088 # Tupel der Sprachbezeichner für die Felder im 5-Felder-Format_::
1090 feldnamen = ("de", "de-1901", "de-CH", "de-1901-x-versal", "de-CH-1901")
1092 # feldindizes
1093 # ~~~~~~~~~~~
1095 # Zuordnung von Sprachbezeichnern nach [BCP47]_ zu den Feldern.
1097 # In Python beginnt die Indexzählung mit Null::
1099 feldindizes = {
1100 "de": 0, # Deutsch, aktuell
1101 "de-DE": 0, # Alias
1102 "de-AT": 0, # Alias
1103 "de-1996": 0, # Alias
1104 "de-1996": 0, # Alias
1105 "de-DE-1996": 0, # Alias
1106 "de-AT-1996": 0, # Alias
1107 "de-1901": 1, # traditionell (Reform 1901)
1108 "de-CH": 2, # ohne ß (Schweiz/versal) aktuell
1109 "de-x-versal": 2, # Alias
1110 "de-1996-x-versal": 2, # Alias
1111 "de-1901-x-versal": 3, # ohne ß (versal) traditionell
1112 "de-CH-1901": 4, # ohne ß (Schweiz) traditionell
1115 # Achtung: die Sprachtags `de` und `de-CH` sind in der Kurzform_ ein Alias für
1116 # die aktuelle Rechtschreibung aber in der Langform_ Bezeichner für separate
1117 # Felder mit allgemeingültigen Trennungen:
1119 # >>> print(ShortEntry.feldindizes['de-1901'], ShortEntry.feldindizes['de-CH'])
1120 # 1 2
1121 # >>> print(ShortEntry.feldindizes['de'], ShortEntry.feldindizes['de-1996'])
1122 # 0 0
1124 # >>> print(WordEntry.feldindizes['de'], WordEntry.feldindizes['de-1996'])
1125 # 1 3
1127 # Schlüssel-Cache
1128 # ~~~~~~~~~~~~~~~
1129 # ::
1131 _key = None
1134 # Initialisierung
1135 # ---------------
1137 # Das Argument `line` ist eine Zeichenkette (`string`, `unicode`) im
1138 # `5-Felder-Format`_ oder eine Instanz der Klasse WordEntry_.
1140 # >>> print(ShortEntry('Diens-te'))
1141 # Diens-te
1142 # >>> tpp = ShortEntry('Ur<laubs=tipp;-2-')
1143 # >>> print(tpp)
1144 # Ur<laubs=tipp;-2-
1146 # >>> print(abenddienste)
1147 # Abenddienste;-2-;Abend=dien-ste;Abend=diens-te
1148 # >>> print(ShortEntry(abenddienste))
1149 # Abend=diens-te
1151 # Achtung: Ein Lang-Eintrag mit nur zwei Feldern kann unregelmäßig sein und
1152 # daher im Kurzformat mehrere Felder benötigen (siehe Tests)
1153 # ::
1155 def __init__(self, line, delimiter=';', prune=True):
1157 if isinstance(line, WordEntry):
1158 self.comment = line.comment # Kommentar
1159 self._key = line.getitem(0) # Schlüssel cachen
1160 if not line:
1161 return
1162 self.append(line.getitem(3)) # Deutsch, aktuell (Reform 1996)
1163 self.append(line.getitem(2)) # "traditionell" (Reform 1901)
1164 if len(line) > 4:
1165 self.append(line.getitem(6)) # ohne ß (Schweiz oder versal) "aktuell"
1166 self.append(line.getitem(5)) # ohne ß (Schweiz oder versal) "traditionell"
1167 self.append(line.getitem(7)) # ohne ß (Schweiz) "traditionell" ("süssauer")
1168 elif 'ß' in self._key: # auskreuzen
1169 self.append('')
1170 self.append('')
1171 self.append('')
1172 else:
1173 WordEntry.__init__(self, line, delimiter)
1175 if prune: # Felder zusammenfassen
1176 self.prune()
1178 # Tests
1179 # ~~~~~
1180 # >>> drs = str(ShortEntry('-1-;Dreß'))
1181 # >>> print(drs)
1182 # -1-;Dreß
1183 # >>> print(ShortEntry('# Testkommentar'))
1184 # # Testkommentar
1186 # In der Voreinstellung werden optionale Felder mit `prune()` gekürzt.
1187 # Mit der Option `prune=False` bleiben alle übergebenen Felder erhalten:
1189 # >>> print(ShortEntry('Fluss;Fluß'))
1190 # Fluss
1191 # >>> print(ShortEntry('Fluss;Fluß', prune=False))
1192 # Fluss;Fluß
1194 # Wird dem Konstruktor eine WordEntry_-Instanz übergeben, so wird diese in das
1195 # Kurzformat überführt:
1197 # >>> print(ShortEntry(WordEntry('heute;heu-te')))
1198 # heu-te
1200 # Bei Ausnahmen von der regelmäßigen Worttrennung werden bei Bedarf
1201 # zusätzliche Spalten belegt:
1203 # >>> print(ShortEntry(WordEntry('Amnesty;Am-nes-ty # en.')))
1204 # Am-nes-ty;Am-nes-ty # en.
1205 # >>> bss = ShortEntry(WordEntry('Boss;Boss # engl.'))
1206 # >>> print(bss)
1207 # Boss;Boss # engl.
1209 # Bei Wörtern mit unterschiedlicher Schreibung in den verschiedenen
1210 # Sprachvarianten werden Felder, die im übergebenen `WordEntry` nicht belegt
1211 # sind als leer markiert:
1213 # >>> print(urlaubstipp)
1214 # Urlaubstipp;-2-;-3-;Ur<laubs=tipp
1215 # >>> print(ShortEntry(urlaubstipp))
1216 # Ur<laubs=tipp;-2-
1217 # >>> print(ShortEntry(WordEntry('Abfalllager;-2-;-3-;Ab<fall=la-ger')))
1218 # Ab<fall=la-ger;-2-
1220 # >>> dresz = WordEntry('Dreß;-2-;Dreß;-4-')
1221 # >>> print(ShortEntry(dresz))
1222 # -1-;Dreß;-3-;-4-;-5-
1223 # >>> g_ebene = WordEntry('Gaußebene;Gauß=ebe-ne')
1224 # >>> print(ShortEntry(g_ebene))
1225 # Gauß=ebe-ne;Gauß=ebe-ne;-3-;-4-;-5-
1226 # >>> print(auffrass)
1227 # auffrass;-2-;-3-;-4-;auf-frass
1228 # >>> print(ShortEntry(auffrass))
1229 # -1-;-2-;auf-frass;auf-frass;auf-frass
1230 # >>> fraesse = WordEntry('frässe;-2-;-3-;-4-;-5-;frä-sse;fräs-se;fräs-se')
1231 # >>> frs = ShortEntry(fraesse)
1232 # >>> print(frs)
1233 # -1-;-2-;fräs-se;frä-sse;fräs-se
1234 # >>> loesz = WordEntry('Lößboden;Löß=bo-den')
1235 # >>> print(ShortEntry(loesz))
1236 # Löß=bo-den;Löß=bo-den;-3-;-4-;-5-
1237 # >>> loess = WordEntry('Lössboden;-2-;-3-;Löss=bo-den;Löss=bo-den')
1238 # >>> print(ShortEntry(loess))
1239 # Löss=bo-den;-2-;Löss=bo-den;Löss=bo-den;Löss=bo-den
1241 # >>> print(ShortEntry(WordEntry('Fussballliga;-2-;-3-;-4-;-5-;-6-;Fuss=ball==li-ga')))
1242 # -1-;-2-;Fuss=ball==li-ga
1243 # >>> print(ShortEntry(WordEntry('Fussballiga;-2-;-3-;-4-;-5-;Fuss=ba{ll/ll=l}i-.ga;-7-')))
1244 # -1-;-2-;-3-;Fuss=ba{ll/ll=l}i-.ga;Fuss=ba{ll/ll=l}i-.ga
1245 # >>> messignal = WordEntry('Messignal;-2-;-3-;-4-;-5-;-6-;-7-;Me{ss/ss=s}i-.gnal')
1246 # >>> print(ShortEntry(messignal))
1247 # -1-;-2-;-3-;-4-;Me{ss/ss=s}i-.gnal
1249 # >>> ShortEntry(WordEntry('# nur Kommentar'))
1250 # ShortEntry('# nur Kommentar')
1253 # key()
1254 # -----
1256 # Gib einen Schlüssel (ungetrenntes Wort) zurück.
1258 # Bei Einträgen im Kurzformat ist der Schlüssel nicht eindeutig: durch
1259 # Transformationen_ kann ein Wort in verschiedenen Schreibungen vorkommen.
1261 # Standardmäßig wird das erste nichtleere Feld ohne Trennzeichen verwendet:
1263 # >>> print(ShortEntry('Fluss;Fluß').key())
1264 # Fluss
1265 # >>> print(ShortEntry('-1-;Dreß').key())
1266 # Dreß
1268 # Die Auswahl kann über das `lang` Argument gesteuert werden:
1270 # >>> print(ShortEntry('-1-;Dreß').key('de-CH-1901'))
1271 # Dress
1273 # Der Schlüssel eines leeren Eintrags ist ein leerer String, der eines leeren
1274 # Kommenars das Kommentarzeichen:
1276 # >>> ShortEntry('').key(), ShortEntry('# toller Kommentar').key()
1277 # ('', '#')
1279 # ::
1281 def key(self, lang=None):
1282 """Erstelle einen Schlüssel (ungetrenntes Wort)."""
1283 if lang:
1284 return join_word(self.get(lang))
1285 if self._key:
1286 return self._key
1287 for f in self:
1288 if f:
1289 self._key = f
1290 return join_word(f)
1291 if self.comment:
1292 return '#' # reiner Kommentar
1293 return '' # leerer Eintrag
1296 # .. _`ShortEntry.getitem()`:
1298 # getitem()
1299 # ---------
1301 # Gib Feld `i` zurück. Wende bei Bedarf die Ersatzregeln_ an.
1303 # >>> entry = ShortEntry('Bu-ße')
1304 # >>> print(entry.getitem(1), entry.getitem(2), entry.getitem(4))
1305 # Bu-ße Bus-se Bus-se
1307 # >>> entry.getitem(5)
1308 # Traceback (most recent call last):
1309 # ...
1310 # IndexError: list index out of range
1312 # Mit `subsititute=True` wird der Feldinhalt ignoriert und immer nach
1313 # Ersatzregel bestimmt:
1315 # >>> entry = ShortEntry('Bu-ße;Reue;Bu-sse')
1316 # >>> print(entry.getitem(1), entry.getitem(1, substitute=True))
1317 # Reue Bu-ße
1319 # ::
1322 def getitem(self, i, substitute=False):
1323 """Return item ``i`` or a subsititute"""
1324 if not substitute:
1325 try:
1326 return self[i]
1327 except IndexError: # Feld i nicht vorhanden
1328 if not self:
1329 raise ValueError('leerer Eintrag')
1330 if i > 4:
1331 raise # maximal 5 Felder im Kurzformat
1333 # Rekursion: wähle das generischere Feld
1334 ersatzfelder = (None, 0, 0, 1, 1)
1335 word = self.getitem(ersatzfelder[i])
1337 # Bei leerem Feld ("ausgekreuzt") ist keine Transformation nötig
1338 if not word:
1339 return ''
1341 # Rechschreibreform (s-t, ck, Dreikonsonantenregel)
1342 if i == 1: # de-1901
1343 return ableitung1901(word)
1345 # Versalschreibung: ß -> ss
1346 if 'ß' in word:
1347 word = versalschreibung(word, self.feldnamen[i])
1348 return word
1351 # get()
1352 # -----
1354 # Gib Trennmuster für Sprachvariante zurück (ggf. über Transformationen_).
1356 # Beispiele:
1358 # ohne Transformation
1360 # >>> aalbst = ShortEntry('Aal=be<stand # Test')
1361 # >>> aalbst.get('de')
1362 # 'Aal=be<stand'
1363 # >>> aalbst.get('de-1996')
1364 # 'Aal=be<stand'
1365 # >>> aalbst.get('de-1901')
1366 # 'Aal=be<stand'
1367 # >>> aalbst.get('de-x-versal')
1368 # 'Aal=be<stand'
1369 # >>> aalbst.get('de-1901-x-versal')
1370 # 'Aal=be<stand'
1371 # >>> aalbst.get('de-1996-x-versal')
1372 # 'Aal=be<stand'
1373 # >>> aalbst.get('de-CH-1901')
1374 # 'Aal=be<stand'
1376 # st und ck:
1378 # >>> dst = ShortEntry('Diens-te')
1379 # >>> print(dst.get('de'), dst.get('de-1901'), dst.get('de-1996'))
1380 # Diens-te Dien-ste Diens-te
1381 # >>> print(ShortEntry('Es-te').get('de-1901'))
1382 # E·ste
1383 # >>> sck = ShortEntry('Stre-cke')
1384 # >>> print(sck.get('de'), sck.get('de-1901'))
1385 # Stre-cke Stre{ck/k-k}e
1387 # Versalschreibung:
1389 # >>> abus = ShortEntry('ab<bü-ßen')
1390 # >>> print(abus.get('de'), abus.get('de-CH'))
1391 # ab<bü-ßen ab<büs-sen
1392 # >>> print(abus.get('de-1901-x-versal'), abus.get('de-CH-1901'))
1393 # ab<bü-ssen ab<büs-sen
1395 # >>> print(ShortEntry('passt').get('de-1901'))
1396 # paßt
1397 # >>> print(ShortEntry('passt').get('de-1901-x-versal'))
1398 # passt
1399 # >>> rs = ShortEntry('-1-;-2-;Russ') # versal für Ruß
1400 # >>> print(rs.get('de-x-versal'))
1401 # Russ
1402 # >>> rs.get('de-1901-x-versal')
1403 # ''
1405 # >>> entry = ShortEntry('süß=sau-er')
1406 # >>> print(entry.get('de-CH'), entry.get('de-1901-x-versal'), entry.get('de-CH-1901'))
1407 # süss=sau-er süss=sau-er süss=sau-er
1409 # >>> ShortEntry('Pro<zess=en-.de').get('de-CH-1901')
1410 # 'Pro<zess=en-.de'
1411 # >>> pstr = ShortEntry('Pass=stra-ße')
1412 # >>> print(pstr.get('de-1996'), pstr.get('de-1901'))
1413 # Pass=stra-ße Paß=stra-ße
1414 # >>> print(pstr.get('de-x-versal'), pstr.get('de-1901-x-versal'))
1415 # Pass=stras-se Pass=stra-sse
1417 # Test: Wenn keine Ersatzschreibung vorliegt, wird auch in traditioneller
1418 # Versalschreibung s-s getrennt:
1420 # >>> print(ShortEntry('Bu-ße').get('de-1901-x-versal'))
1421 # Bu-sse
1422 # >>> print(ShortEntry('Bus-se').get('de-1901-x-versal'))
1423 # Bus-se
1425 # Geerbt von `WordEntry.get()`_.
1427 # .. _ShortEntry.complete():
1429 # complete()
1430 # ----------
1432 # Eintrag vervollständigen (alle 5 Felder ausfüllen).
1434 # >>> dst.complete()
1435 # >>> print(dst)
1436 # Diens-te;Dien-ste;Diens-te;Dien-ste;Dien-ste
1437 # >>> aalbst.complete()
1438 # >>> print(aalbst)
1439 # Aal=be<stand;Aal=be<stand;Aal=be<stand;Aal=be<stand;Aal=be<stand # Test
1440 # >>> abus.complete()
1441 # >>> print(abus)
1442 # ab<bü-ßen;ab<bü-ßen;ab<büs-sen;ab<bü-ssen;ab<büs-sen
1443 # >>> bss.complete()
1444 # >>> print(bss)
1445 # Boss;Boss;Boss;Boss;Boss # engl.
1446 # >>> frs.complete()
1447 # >>> print(frs)
1448 # -1-;-2-;fräs-se;frä-sse;fräs-se
1449 # >>> tpp.complete()
1450 # >>> print(tpp)
1451 # Ur<laubs=tipp;-2-;Ur<laubs=tipp;-4-;-5-
1453 # >>> entry = ShortEntry('# toller Hecht')
1454 # >>> entry.complete()
1455 # >>> print(entry)
1456 # # toller Hecht
1458 # ::
1460 def complete(self):
1461 for i in range(len(self), 5):
1462 try:
1463 field = self.getitem(i)
1464 except ValueError: # leerer Eintrag
1465 return
1466 self.append(field)
1469 # completed()
1470 # -----------
1472 # Gib eine vervollständigte Kopie des Eintrags zurück.
1474 # >>> entry = ShortEntry('-1-;-2-;sties-se').completed()
1475 # >>> entry
1476 # ShortEntry('-1-;-2-;sties-se;-4-;-5-')
1477 # >>> [field for field in entry]
1478 # ['', '', 'sties-se', '', '']
1479 # >>> print(pstr.completed())
1480 # Pass=stra-ße;Paß=stra-ße;Pass=stras-se;Pass=stra-sse;Pass=stras-se
1481 # >>> print(ShortEntry('Miss<er<.folg').completed())
1482 # Miss<er<.folg;Miß<er<.folg;Miss<er<.folg;Miss<er<.folg;Miss<er<.folg
1484 # Geerbt von `WordEntry.completed()`_.
1487 # prune()
1488 # -------
1490 # Eintrag kürzen.
1492 # Felder weglassen, wenn sich der Inhalt durch Ersatzregeln_ gewinnen läßt:
1494 # >>> aalbst.prune()
1495 # >>> print(aalbst)
1496 # Aal=be<stand # Test
1497 # >>> dst.prune()
1498 # >>> print(dst)
1499 # Diens-te
1500 # >>> abus.prune()
1501 # >>> print(abus)
1502 # ab<bü-ßen
1503 # >>> bss.prune()
1504 # >>> print(bss)
1505 # Boss;Boss # engl.
1506 # >>> tpp.prune()
1507 # >>> print(tpp)
1508 # Ur<laubs=tipp;-2-
1510 # Auch das „Auskreuzen“ wird weitergereicht:
1512 # Wenn ein Wort in "traditioneller" Rechtschreibung nicht existiert, reicht
1513 # das "Auskreuzen" von Feld 2:
1515 # >>> entry = ShortEntry('Tipp;-2-;Tipp;-4-;-5-')
1516 # >>> entry.prune()
1517 # >>> print(entry)
1518 # Tipp;-2-
1520 # Wenn ein Wort in aktueller Rechtschreibung nicht existiert, reicht das
1521 # "Auskreuzen" von Feld 1:
1523 # >>> entry = ShortEntry('-1-;Rauh=nacht;-3-;Rauh=nacht')
1524 # >>> entry.prune()
1525 # >>> print(entry)
1526 # -1-;Rauh=nacht
1528 # Felder werden nicht gekürzt, wenn die Rekonstruktion einen anderen Wert
1529 # ergäbe:
1531 # >>> frs.prune()
1532 # >>> print(frs)
1533 # -1-;-2-;fräs-se;frä-sse;fräs-se
1535 # >>> entry = ShortEntry('-1-;Dreß;-3-;-4-;-5-')
1536 # >>> entry.prune()
1537 # >>> print(entry)
1538 # -1-;Dreß;-3-;-4-;-5-
1540 # Mit ``drop_sz=True`` werden die drei letzten Felder (ß-Ersatzschreibung)
1541 # stets gekürzt:
1543 # * ß-Ersatzschreibung ist immer definiert, daher kann eigentlich nichts falsch
1544 # gemacht werden.
1546 # * Die "nachlässige" Wandlung garantiert nicht die exakte Reproduktion der
1547 # Ausgangsliste nach einem "Rundtrip":
1549 # Mit `ShortEntry.complete()`_ werden die letzten Spalten ausgefüllt, auch
1550 # wenn sie im Original "ausgekreuzt" waren.
1552 # Bei Wandlung ins Langformat können zusätzliche Einträge mit
1553 # ß-Ersatzschreibung entstehen.
1555 # Die Auszeichnung von Mehrdeutigkeiten (z.B. „Mas-se/Ma-sse“ in
1556 # de-1901-x-versal) geht verloren.
1558 # >>> entry.prune(drop_sz=True)
1559 # >>> print(entry)
1560 # -1-;Dreß
1561 # >>> entry.complete()
1562 # >>> print(entry)
1563 # -1-;Dreß;-3-;Dress;Dress
1565 # ::
1567 def prune(self, drop_sz=False):
1568 if len(self) == 1: # bereits kompakt
1569 return
1570 if drop_sz:
1571 while len(self) > 2:
1572 self.pop()
1573 for i in range(len(self)-1, 0, -1):
1574 wort = self.pop()
1575 rekonstruktion = self.getitem(i)
1576 if wort != rekonstruktion:
1577 # print(tag, repr(self), wort, rekonstruktion)
1578 self.append(wort)
1579 return
1580 if len(self) == 1 and not self[0]:
1581 self.pop()
1584 # merge()
1585 # -------
1587 # Einträge zusammenlegen.
1589 # Das als Argument übergebene WordEntry Objekt wird in dem eigenen Eintrag
1590 # eingegliedert.
1592 # Leere („ausgekreuzte“) Felder werden überschrieben:
1594 # >>> entry = ShortEntry('be<wusst;-2-')
1595 # >>> entry.merge(ShortEntry('-1-;be<wußt'))
1596 # >>> print(entry)
1597 # be<wusst
1599 # Identische Felder bleiben erhalten. Alle Felder die über Transformationen_
1600 # rekonstruiert werden können werden entfernt:
1602 # >>> entry = ShortEntry('Maß=nah-me # < nehmen')
1603 # >>> entry.merge(ShortEntry('-1-;-2-;Mass=nah-me'))
1604 # >>> print(entry)
1605 # Maß=nah-me # < nehmen
1606 # >>> entry = ShortEntry('-1-;-2-;Mass=nah-me')
1607 # >>> entry.merge(ShortEntry('Maß=nah-me # < nehmen'), merge_comments=True)
1608 # >>> print(entry)
1609 # Maß=nah-me # < nehmen
1611 # Es sei den die Option ``prune`` ist ``False``:
1613 # >>> entry.merge(ShortEntry('Maß=nah-me'), prune=False)
1614 # >>> print(entry)
1615 # Maß=nah-me;Maß=nah-me;Mass=nah-me;Mass=nah-me;Mass=nah-me # < nehmen
1617 # Bei Konflikten wird ein AssertionError erzeugt:
1619 # >>> entry = ShortEntry('be<wusst;-2-')
1620 # >>> entry.merge(ShortEntry('-1-;-2-;ver<bor-gen'))
1621 # Traceback (most recent call last):
1622 # ...
1623 # AssertionError: Merge Error:
1624 # be<wusst;-2-;be<wusst;-4-;-5-
1625 # -1-;-2-;ver<bor-gen;-4-;-5-
1627 # Geerbt von `WordEntry.merge()`_
1630 # wordentries()
1631 # -------------
1633 # Gib eine Liste von WordEntry_ Instanzen (8-Spalten-Format_) zurück.
1635 # >>> mse = ShortEntry('Mas-se')
1636 # >>> mse.wordentries()
1637 # [WordEntry('Masse;Mas-se')]
1639 # Im Langformat gibt es für jede Schreibung einen separaten Eintrag:
1641 # >>> mas = ShortEntry('Ma-ße')
1642 # >>> for e in mas.wordentries():
1643 # ... print(e)
1644 # Maße;Ma-ße
1645 # Masse;-2-;-3-;-4-;-5-;Ma-sse;Mas-se;Mas-se
1647 # >>> entry = ShortEntry('Löss')
1648 # >>> for e in entry.wordentries():
1649 # ... print(e)
1650 # Löss;-2-;-3-;Löss;Löss
1651 # Löß;-2-;Löß;-4-
1653 # ::
1655 def wordentries(self, prune=True):
1657 if not self:
1658 if self.comment: # leerer Kommentar
1659 return [WordEntry('# '+self.comment)]
1660 return [WordEntry('')]
1662 entries = [] # liste für WordEntry Einträge (Rückgabeobjekt)
1663 words = {} # dictionary für WordEntry Einträge
1664 # Zuordnung der Indizes: ShortEntry[s] == WordEntry[l]
1665 indices = (3, 2, 6, 5, 7)
1667 for s,l in enumerate(indices):
1668 word = self.getitem(s)
1669 if not word:
1670 continue # Leerfelder überspringen
1671 # Schlüssel:
1672 key = join_word(word)
1673 # WordEntry Instanz heraussuchen oder erzeugen:
1674 try:
1675 entry = words[key]
1676 except KeyError:
1677 entry = WordEntry(key)
1678 entry.comment = self.comment
1679 words[key] = entry
1680 entries.append(entry)
1681 # Eintrag in entry[j]:
1682 entry.setitem(l, word)
1684 # Auffüllen und Komprimieren
1685 for entry in entries:
1686 while len(entry) < 8:
1687 entry.append('')
1688 if prune:
1689 entry.prune()
1690 return entries
1693 # Hilfsfunktion für Tests:
1695 # >>> def print_langform(line, prune=True):
1696 # ... for e in ShortEntry(line).wordentries(prune):
1697 # ... print(e)
1699 # Eine Schreibung:
1701 # >>> print_langform('ba-den')
1702 # baden;ba-den
1703 # >>> print_langform('Wes-te')
1704 # Weste;-2-;We-ste;Wes-te
1705 # >>> print_langform('Toll-patsch;-2-')
1706 # Tollpatsch;-2-;-3-;Toll-patsch
1707 # >>> print_langform('-1-;rau-he')
1708 # rauhe;-2-;rau-he;-4-
1710 # Unterschiedliche Schreibungen:
1712 # >>> print_langform('Ab<fall=la-ger # Rechtschreibänderung')
1713 # Abfalllager;-2-;-3-;Ab<fall=la-ger # Rechtschreibänderung
1714 # Abfallager;-2-;Ab<fa{ll/ll=l}a-.ger;-4- # Rechtschreibänderung
1716 # >>> print_langform('Ab<guss')
1717 # Abguss;-2-;-3-;Ab<guss;Ab<guss
1718 # Abguß;-2-;Ab<guß;-4-
1720 # >>> print_langform('groß')
1721 # groß;groß
1722 # gross;-2-;-3-;-4-;gross
1724 # >>> print_langform('gro-ßen')
1725 # großen;gro-ßen
1726 # grossen;-2-;-3-;-4-;-5-;gro-ssen;gros-sen;gros-sen
1728 # >>> print_langform('Spaß')
1729 # Spaß;Spaß
1730 # Spass;-2-;-3-;-4-;Spass
1731 # >>> print_langform('Spass')
1732 # Spass;-2-;-3-;Spass;Spass
1733 # Spaß;-2-;Spaß;-4-
1734 # >>> print_langform('spa-ßen')
1735 # spaßen;spa-ßen
1736 # spassen;-2-;-3-;-4-;-5-;spa-ssen;spas-sen;spas-sen
1738 # >>> print_langform('ab<bü-ßen')
1739 # abbüßen;ab<bü-ßen
1740 # abbüssen;-2-;-3-;-4-;-5-;ab<bü-ssen;ab<büs-sen;ab<büs-sen
1742 # >>> print_langform('Biss')
1743 # Biss;-2-;-3-;Biss;Biss
1744 # Biß;-2-;Biß;-4-
1745 # >>> print_langform('Boss;Boss # engl.')
1746 # Boss;Boss # engl.
1748 # >>> print_langform('Pass=sys-tem')
1749 # Passsystem;-2-;-3-;Pass=sys-tem;-5-;Pass=sy-stem;Pass=sys-tem
1750 # Paßsystem;-2-;Paß=sy-stem;-4-
1752 # >>> print_langform('Press=saft')
1753 # Presssaft;-2-;-3-;Press=saft;Press=saft
1754 # Preßsaft;-2-;Preß=saft;-4-
1756 # >>> print_langform('Pro<gramm==maß=nah-me')
1757 # Programmmaßnahme;-2-;-3-;Pro<gramm==maß=nah-me
1758 # Programmaßnahme;-2-;Pro<gra{mm/mm==m}aß=nah-me;-4-
1759 # Programmmassnahme;-2-;-3-;-4-;-5-;-6-;Pro<gramm==mass=nah-me
1760 # Programmassnahme;-2-;-3-;-4-;-5-;Pro<gra{mm/mm==m}ass=nah-me;-7-
1762 # >>> print_langform('Pass=stra-ße')
1763 # Passstraße;-2-;-3-;Pass=stra-ße
1764 # Paßstraße;-2-;Paß=stra-ße;-4-
1765 # Passstrasse;-2-;-3-;-4-;-5-;Pass=stra-sse;Pass=stras-se;Pass=stras-se
1767 # >>> print_langform('Pro<zess=en-.de;Pro<zeß=en-de;Pro<zess=en-.de')
1768 # Prozessende;-2-;-3-;Pro<zess=en-.de;Pro<zess=en-.de
1769 # Prozeßende;-2-;Pro<zeß=en-de;-4-
1771 # Explizit nur eine Schreibung:
1773 # >>> print_langform('Abend=dress;Abend=dress')
1774 # Abenddress;Abend=dress
1775 # >>> print_langform('-1-;Abend=dreß')
1776 # Abenddreß;-2-;Abend=dreß;-4-
1777 # Abenddress;-2-;-3-;-4-;-5-;Abend=dress;-7-
1779 # Nur Versalschreibung:
1781 # >>> print_langform('-1-;-2-;Fuss;Fuss;Fuss')
1782 # Fuss;-2-;-3-;-4-;Fuss
1783 # >>> line = 'Fussballiga;-2-;-3-;-4-;-5-;Fuss=ba{ll/ll=l}i-.ga'
1784 # >>> print(ShortEntry(WordEntry(line), prune=False))
1785 # -1-;-2-;-3-;Fuss=ba{ll/ll=l}i-.ga;Fuss=ba{ll/ll=l}i-.ga
1786 # >>> print_langform('-1-;-2-;-3-;Fuss=ba{ll/ll=l}i-.ga;Fuss=ba{ll/ll=l}i-.ga')
1787 # Fussballiga;-2-;-3-;-4-;-5-;Fuss=ba{ll/ll=l}i-.ga;-7-
1788 # >>> print_langform('-1-;-2-;Fuss=ball==li-ga')
1789 # Fussballliga;-2-;-3-;-4-;-5-;-6-;Fuss=ball==li-ga
1791 # Nur Kommentar:
1793 # >>> print_langform('# holla')
1794 # # holla
1797 # Transformationen
1798 # ================
1800 # ableitung1901()
1801 # ---------------
1803 # Ableitung von de-1901 aus de-1996 (Reversion der Reform 1996).
1805 # Mit keep_key == True werden nur Änderungen vorgenommen, die die
1806 # Schreibung des (ungetrennten) Wortes nicht verändern.
1808 # >>> from wortliste import ableitung1901
1810 # ::
1812 def ableitung1901(wort, keep_key=False):
1813 """Reverse regular changes of the 1996 orthography reform."""
1816 # Trennregeländerungen
1817 # ~~~~~~~~~~~~~~~~~~~~
1819 # Diese Regeln ändern nicht das Wort, nur die Trennmöglichkeiten.
1821 # Alternativtrennungen
1822 # """"""""""""""""""""
1823 # (siehe Trennstile.txt)::
1825 # wort = fremdwortsilben(wort) # Wahltrennungen wie no-b-le
1826 wort = morphemisch(wort) # Wahltrennungen wie He-li-ko<p-ter
1828 # st-Trennung
1829 # """""""""""
1831 # K75: Trenne nie st.
1833 # Ersetze 's-t' mit '-st':
1835 # >>> ableitung1901('Diens-te')
1836 # 'Dien-ste'
1837 # >>> print(ableitung1901('wuss-te', keep_key=True))
1838 # wuss-te
1840 # Aber Trennung von s-theta erlaubt (nach K74):
1842 # >>> print(ableitung1901('Äs-thet'))
1843 # Äs-thet
1845 # ::
1847 wort = re.sub('(?<!s)s-t(?!h)', '-st', wort)
1849 # Keine Trennung nach nur einem Buchstaben am Wortanfang:
1851 # >>> print(ableitung1901('Es-te'))
1852 # E·ste
1853 # >>> print(ableitung1901('Nord=os-ten'))
1854 # Nord=o·sten
1855 # >>> print(ableitung1901('Po-ly<es-ter'))
1856 # Po-ly<e·ster
1858 # Im Wort sind Einvokalsilben erlaubt:
1860 # >>> print(ableitung1901('the>is-tisch'))
1861 # the>i-stisch
1862 # >>> print(ableitung1901('neu-es-te'))
1863 # neu-e-ste
1865 # ::
1867 wort = re.sub('((?<=[=<].)|(?<=^.))-', r'·', wort)
1870 # ck-Trennung
1871 # """""""""""
1873 # K76: Trenne 'ck' als 'k-k'.
1875 # Ersetze '-ck' mit '{ck/k-k}':
1877 # >>> ableitung1901('Ha-cke')
1878 # 'Ha{ck/k-k}e'
1879 # >>> ableitung1901('A·cker')
1880 # 'A{ck/k-k}er'
1881 # >>> ableitung1901('Got-tes=a·cker')
1882 # 'Got-tes=a{ck/k-k}er'
1883 # >>> ableitung1901('Aal=beck')
1884 # 'Aal=beck'
1885 # >>> ableitung1901('be<spick-te')
1886 # 'be<spick-te'
1888 # ::
1890 wort = re.sub('[-·]ck', '{ck/k-k}', wort)
1892 if keep_key:
1893 return wort
1896 # Rechtschreibänderungen
1897 # ~~~~~~~~~~~~~~~~~~~~~~
1899 # Diese Regeln ändern die Schreibung des ungetrennten Worts (und somit den
1900 # Schlüssel im Langformat der Wortliste).
1902 # Schluss-ss
1903 # """"""""""
1905 # K38: Kein "ss" und "sst" am Silbenende (ungetrenntes "ss" nur in Ausnahmen)
1906 # (Andererseit ist 'ßt' und Schluss-ß auch in de-1996 möglich (langer Vokal).)
1908 # Ersetze ungetrenntes 'ss' mit 'ß':
1910 # >>> print(ableitung1901('passt'))
1911 # paßt
1912 # >>> print(ableitung1901('Hass'))
1913 # Haß
1914 # >>> print(ableitung1901('Fass=brau-se'))
1915 # Faß=brau-se
1916 # >>> print(ableitung1901('wuss-te'))
1917 # wuß-te
1919 # ß steht für inlautendes ss, wenn ein 'e' ausfällt (und der Ausfall nicht
1920 # durch Apostroph angedeutet wird)
1922 # >>> print(ableitung1901('wäss-rig'))
1923 # wäß-rig
1924 # >>> print(ableitung1901('an<ge<mess-ner'))
1925 # an<ge<meß-ner
1926 # >>> print(ableitung1901('duss-lig'))
1927 # duß-lig
1928 # >>> print(ableitung1901('bissl'))
1929 # bißl
1931 # Keine Wandlung zu "ß":
1932 # getrenntes Doppel-s (s-s)
1934 # >>> print(ableitung1901('Was-ser'))
1935 # Was-ser
1937 # Vokal folgt (Fremdwörter):
1938 # >>> print(ableitung1901('Com-tesse'))
1939 # Com-tesse
1941 # Großbuchstabe folgt
1943 # >>> print(ableitung1901('WissZeitVG')) # Abkürzung
1944 # WissZeitVG
1946 # Drei oder mehr 's' = Lautmalerei → erhalten:
1948 # >>> print(ableitung1901('pssst'))
1949 # pssst
1951 # ::
1953 wort = re.sub('(?<=[^s])ss(?=[^aeiouyäöüA-Zs]|$)', 'ß', wort)
1955 # Unterdrückung der Trennstelle nach "…ß=er" und "…ß=en" nicht nötig:
1957 # >>> print(ableitung1901('hass=er<.füllt'))
1958 # haß=er<füllt
1960 # ::
1962 wort = re.sub('ß(=+)e([rn][<-]+)\.', r'ß\1e\2', wort)
1964 # Dreikonsonantenregel
1965 # """"""""""""""""""""
1967 # K78: Zusammensetzungen, bei denen von drei zusammenstoßenden gleichen
1968 # Konsonanten einer entfällt (K15), schreibt man bei Silbentrennung wieder
1969 # mit allen drei Konsonanten.
1971 # Ersetze 'xx=x' mit '{xx/xx=x}' (für alle Konsonanten vor Selbstlaut)
1973 # >>> print(ableitung1901('Kipp=pflug'))
1974 # Kipp=pflug
1975 # >>> print(ableitung1901('Kipp=punkt'))
1976 # Ki{pp/pp=p}unkt
1977 # >>> print(ableitung1901('Ab<fall=la-ger'))
1978 # Ab<fa{ll/ll=l}a-.ger
1979 # >>> print(ableitung1901('All<lie-be'))
1980 # A{ll/ll<l}ie-be
1981 # >>> print(ableitung1901('hell>licht'))
1982 # he{ll/ll>l}icht
1983 # >>> print(ableitung1901('Pro<gramm==maß=nah-me'))
1984 # Pro<gra{mm/mm==m}aß=nah-me
1986 # ::
1988 wort = re.sub(r'([bfglmnprt])\1([<=>]+)\1(?=[aeiouyäöü])',
1989 r'{\1\1/\1\1\2\1}', wort)
1991 # Unterdrücken der Trennung nach nur einem Buchstaben::
1993 wort = re.sub(r'(?<=[<=>].}[aeiouyäöü])([-<])\.?', r'\1.', wort)
1995 return wort
1997 # Tests:
1999 # Ein-Vokal-Silben auch schon 1901 erlaubt:
2001 # >>> print(ableitung1901('ver<knäu-e-le'))
2002 # ver<knäu-e-le
2004 # versalschreibung()
2005 # ------------------
2006 # Ersetze 'ß' mit 'ss', trenne je nach Sprachvarietät `lang`:
2008 # >>> from wortliste import versalschreibung
2009 # >>> print(versalschreibung('paßt'))
2010 # passt
2011 # >>> print(versalschreibung('Dar-ßer'))
2012 # Dars-ser
2013 # >>> print(versalschreibung('bü-ßen', 'de-CH'))
2014 # büs-sen
2015 # >>> print(versalschreibung('bü-ßen', 'de-1996-x-versal'))
2016 # büs-sen
2017 # >>> print(versalschreibung('bü-ßen', 'de-CH-1901'))
2018 # büs-sen
2019 # >>> print(versalschreibung('ä·ßen', 'de-CH-1901'))
2020 # äs-sen
2021 # >>> print(versalschreibung('auf<äßen', 'de-CH-1901'))
2022 # auf<äs-sen
2023 # >>> print(versalschreibung('auf<eßt', 'de-CH-1901'))
2024 # auf<esst
2025 # >>> print(versalschreibung('Groß=se-gel', 'de-1901-x-versal'))
2026 # Gross=se-gel
2027 # >>> print(versalschreibung('Groß=se-gel', 'de-CH-1901'))
2028 # Gross=se-gel
2029 # >>> print(versalschreibung('Paß=sy-ste-me', 'de-CH-1901'))
2030 # Pass=sy-ste-me
2031 # >>> print(versalschreibung('Pro<zeß=en-de', 'de-CH-1901'))
2032 # Pro<zess=en-.de
2033 # >>> print(versalschreibung('Pro<zess=en-.de', 'de-CH-1901'))
2034 # Pro<zess=en-.de
2035 # >>> print(versalschreibung('Fluß==sy-stem', 'de-CH-1901'))
2036 # Fluss==sy-stem
2037 # >>> print(versalschreibung('Meß==sen-der', 'de-CH-1901'))
2038 # Mess==sen-der
2040 # ::
2042 def versalschreibung(wort, lang='de'):
2044 if not 'ß' in wort:
2045 return wort
2047 wort = wort.replace('ß', 'ss')
2049 # Trennung von Ersatz-ss in de-CH und de-1996 nach Sprechsilbenregel::
2051 if '1901-x-versal' not in lang:
2052 wort = wort.replace('·ss', 's-s')
2053 # wort = re.sub('(?<=[aeiouyäöü])-\.?ss', 's-s', wort)
2054 wort = re.sub('-\.?ss(?=[aeiouyäöü])', 's-s', wort)
2055 wort = re.sub('(?<=^[aeiouyäöü])ss(?=[aeiouyäöü])', 's-s', wort)
2056 wort = re.sub('(?<=[=<][aeiouyäöü])ss(?=[aeiouyäöü])', 's-s', wort)
2058 # Unterdrückung irreführender Trennung::
2060 wort = re.sub('ss(=+)(en|er)([<-])\.?', r'ss\1\2\3.', wort)
2062 # Dreikonsonantenregel für Ersatz-ss in de-CH-1901::
2064 if 'CH-1901-x-dreikonsonanten' in lang:
2065 wort = re.sub('ss(=+)s(?=[aeiouyäöü])', r'{ss/ss\1s}', wort)
2066 # Unterdrücken der Trennung nach nur einem Buchstaben und irreführender Trennungen
2067 wort = re.sub(r'(?<=[=>]s}[aeiouyäöü])([-<])\.?', r'\1.', wort)
2068 # wort = re.sub(r'(?<===s}[aeiouyäöü])([-<])\.?', r'\1.', wort) # Reißverschus=sy-.stem
2069 wort = re.sub('(?<=[=>]s})(en|er)([<-])\.?', r'\1\2.', wort)
2071 return wort
2074 # Kurzformat in Langformat
2075 # ------------------------
2077 # Zusätzlich benötigte Felder werden automatisch erzeugt. Ein Kurzeintrag kann
2078 # mehrere Langeinträge ergeben:
2080 # >>> from wortliste import short2long
2081 # >>> for line in short2long(["Diens-te", "Ge<biss"]):
2082 # ... print(line)
2083 # Dienste;-2-;Dien-ste;Diens-te
2084 # Gebiss;-2-;-3-;Ge<biss;Ge<biss
2085 # Gebiß;-2-;Ge<biß;-4-
2087 # >>> for line in short2long(["Ge<schoss", "Ge<schoß # österr."]):
2088 # ... print(line)
2089 # Geschoss;-2-;-3-;Ge<schoss;Ge<schoss
2090 # Geschoß;Ge<schoß # österr.
2092 # short2long()
2093 # ~~~~~~~~~~~~
2094 # ::
2096 def short2long(lines, sort=True, prune=True):
2097 """Convert sequence of lines in ShortEntry format to WordEntry instances.
2100 # Sammeln in Liste und Dictionary:
2101 words = {} # zum Zusammenfassen
2102 entries = []
2104 for line in lines:
2105 shortentry = ShortEntry(line)
2106 shortentry.complete()
2107 for entry in shortentry.wordentries(prune=False):
2108 key = entry.key().lower()
2109 try: # Eintrag mit gleichem Schlüssel vorhanden?
2110 altentry = words[key]
2111 except KeyError: # nein -> neuer Eintrag
2112 words[key] = entry
2113 entries.append(entry)
2114 continue
2115 try:
2116 if entry[3]: # de-1996 non-empty
2117 entry.merge(altentry, prune=False, allow_alternatives=True)
2118 # Alternativen Eintrag "in-place" ersetzen:
2119 for i, word in enumerate(entry):
2120 altentry[i] = word
2121 altentry.comment = entry.comment
2122 else:
2123 altentry.merge(entry, prune=False, allow_alternatives=True)
2124 except AssertionError as e:
2125 sys.stderr.write(str(e)+'\n')
2126 entries.append(entry)
2127 except IndexError: # Leerer Eintrag (Kommentar)
2128 entries.append(entry)
2131 if prune:
2132 for entry in entries:
2133 entry.prune()
2135 if sort:
2136 entries.sort(key=sortkey_duden)
2138 return entries
2140 # Tests:
2142 # Kommentare bleiben erhalten:
2144 # >>> for line in short2long(['Aal=an-geln', '# toller Kommentar'],
2145 # ... sort=False):
2146 # ... print(line)
2147 # Aalangeln;Aal=an-geln
2148 # # toller Kommentar
2150 # Beim Sortieren werden Kommenare an den Beginn geschrieben.
2151 # >>> for line in short2long(['# erster Kommentar',
2152 # ... 'Aal=an-geln',
2153 # ... '# zweiter Kommentar']):
2154 # ... print(line)
2155 # # erster Kommentar
2156 # # zweiter Kommentar
2157 # Aalangeln;Aal=an-geln
2159 # >>> for line in short2long(['Fluss=schiff=fahrt']):
2160 # ... print(line)
2161 # Flussschiffahrt;-2-;-3-;-4-;-5-;Fluss=schi{ff/ff=f}ahrt;-7-
2162 # Flußschiffahrt;-2-;Fluß=schi{ff/ff=f}ahrt;-4-
2163 # Flussschifffahrt;-2-;-3-;Fluss=schiff=fahrt
2166 # Langformat in Kurzformat
2167 # ------------------------
2169 # Optionale Felder werden weggelassen, wenn sie mit dem automatisch erzeugten
2170 # Inhalt übereinstimmen:
2172 # >>> from wortliste import long2short
2173 # >>> for entry in long2short(["Dienste;-2-;Dien-ste;Diens-te"]):
2174 # ... print(entry)
2175 # Diens-te
2177 # Zusammengehörige Einträge werden zusammengefasst:
2179 # >>> for line in long2short(["Großanlass;-2-;-3-;Groß=an<lass",
2180 # ... "Großanlaß;-2-;Groß=an<laß;-4-",
2181 # ... "Grossanlass;-2-;-3-;-4-;Gross=an<lass"]):
2182 # ... print(line)
2183 # Groß=an<lass
2185 # Sonderfälle
2186 # ~~~~~~~~~~~
2188 # Bei einigen Wörtern ist die ß-ss-Beziehung nicht eindeutig:
2190 # ======= ======== =========
2191 # de1901 de-1996 de-CH
2192 # ======= ======== =========
2193 # Maße Maße Masse
2194 # Masse Masse Masse
2195 # Geschoß Geschoß Geschoss
2196 # Geschoß Geschoss Geschoss
2197 # ======= ======== =========
2199 # Daher kann es vorkommen, dass ein Langform-Eintrag Beiträge von
2200 # verschiedenen Kurzformen erhält. So erzeugen, z.B. sowohl
2201 # „Masse“ als auch „Maße“ einen Langeintrag mit Schlüssel „Masse“:
2203 # >>> for entry in short2long(['Mas-se']): print(entry)
2204 # Masse;Mas-se
2205 # >>> for entry in short2long(['Ma-ße']): print(entry)
2206 # Masse;-2-;-3-;-4-;-5-;Ma-sse;Mas-se;Mas-se
2207 # Maße;Ma-ße
2209 # In der Langform werden Alternativen in ein Feld geschrieben:
2211 # >>> for entry in short2long(['Mas-se', 'Ma-ße']):
2212 # ... print(entry)
2213 # Masse;-2-;Mas-se;Mas-se;-5-;Ma[s-/-s]se;Mas-se;Mas-se
2214 # Maße;Ma-ße
2216 # Sind die Alternativen bereits in der Quelle, bleiben sie in der Kurzform
2217 # erhalten:
2219 # >>> ml = ['Masse;-2-;Mas-se;Mas-se;-5-;Ma[-s/s-]se;Mas-se;Mas-se',
2220 # ... 'Maße;Ma-ße']
2221 # >>> for entry in long2short(ml):
2222 # ... print(entry)
2223 # Mas-se;Mas-se;Mas-se;Ma[-s/s-]se
2224 # Ma-ße;Ma-ße;Mas-se;Ma[-s/s-]se
2226 # Zurück in die Langform:
2228 # >>> for entry in short2long(['Mas-se;Mas-se;Mas-se;Ma[-s/s-]se',
2229 # ... 'Ma-ße;Ma-ße;-3-;Ma[-s/s-]se']):
2230 # ... print(entry)
2231 # Masse;-2-;Mas-se;Mas-se;-5-;Ma[-s/s-]se;Mas-se;Mas-se
2232 # Maße;Ma-ße
2234 # long2short()
2235 # ~~~~~~~~~~~~
2236 # ::
2238 def long2short(lines, prune=True, drop_sz=False):
2239 """Convert sequence of 8-column lines to ShortEntry instances."""
2240 words = {} # Einträge mit `de`
2241 words_x = {} # Einträge ohne `de`
2242 words_merged = set() # Einträge die vollständig in andere einsortiert wurden
2243 entries = [] # Rückgabewert: Liste der Kurzeinträge
2245 # Zeilen Einlesen, Wandeln und Sammeln::
2247 for line in lines:
2248 longentry = WordEntry(line)
2249 entry = ShortEntry(longentry, prune=False)
2250 key = entry.key().lower() # Schlüssel ohne Großschreibung
2252 if not entry: # reiner Kommentar oder leerer Eintrag
2253 if entry.comment:
2254 entries.append(entry)
2255 continue
2257 # Einträge mit leerem ersten Feld werden später einsortiert:
2258 if prune and not entry[0]:
2259 # print(key, "in words_x eintragen")
2260 words_x[key] = entry
2261 continue
2263 # Eintrag in `dictionary` und Liste:
2264 words[key] = entry
2265 entries.append(entry)
2267 # Straffen (Weglassen von Feldern/Einträgen wenn möglich),
2268 # es sei denn, der Aufruf erfolgte mit `prune=False`::
2270 if not prune:
2271 return entries
2273 for entry in entries:
2275 # Auffüllen:
2276 i = 1
2277 while i < len(entry):
2278 # for i in range(1, len(entry)):
2279 if not entry[i]: # Feld ausgekreuzt
2280 key_i = join_word(entry.getitem(i, substitute=True)).lower()
2281 # print("Auffüllen", i, key_i, str(entry))
2282 try:
2283 co_entry = words_x[key_i]
2284 entry.merge(co_entry, prune=False, start=1)
2285 words_merged.add(key_i)
2286 # del words_x[key_i]
2287 except (KeyError, AssertionError):
2288 try:
2289 co_entry = words[key_i]
2290 entry.merge(co_entry, prune=False, start=1)
2291 words_merged.add(key_i)
2292 except (KeyError, AssertionError):
2293 pass
2294 i += 1
2296 # Anhängen aller Einträge mit leerem `de`-Feld, die nicht in
2297 # einen zugehörigen Eintrag einsortiert wurden an die Liste::
2299 for key, entry in words_x.items():
2300 # print(key, str(entry))
2301 if key in words_merged:
2302 continue
2303 # ggf. ergänzen:
2304 if len(entry) > 2 and not entry[3]: # de-1901-x-versal
2305 key = join_word(entry.getitem(3, substitute=True)).lower()
2306 try:
2307 co_entry = words_x[key]
2308 entry.merge(co_entry, prune=False, start=3)
2309 words_merged.add(key)
2310 except (KeyError, AssertionError):
2311 try:
2312 co_entry = words[key]
2313 entry.merge(co_entry, prune=False, start=3)
2314 # print(key, str(co_entry))
2315 except (KeyError, AssertionError):
2316 pass
2318 entries.append(entry)
2320 for entry in entries:
2321 entry.prune(drop_sz)
2323 return entries
2325 # Tests:
2327 # Separate Einträge ß-Schreibungen zusammenfassen:
2329 # Ein von 2 ß wird zu ss in de-1996:
2331 # >>> for entry in long2short([
2332 # ... 'Passstrasse;-2-;-3-;-4-;-5-;Pass=stra-sse;Pass=stras-se;Pass=stras-se',
2333 # ... 'Passstraße;-2-;-3-;Pass=stra-ße',
2334 # ... 'Paßstraße;-2-;Paß=stra-ße;-4-']):
2335 # ... print(entry)
2336 # Pass=stra-ße
2338 # ß in de-1996:
2340 # >>> for line in long2short([
2341 # ... "Grossserie;-2-;-3-;-4-;-5-;Gross=se-rie;Gross=se-rie",
2342 # ... "Großserie;Groß=se-rie"]):
2343 # ... print(line)
2344 # Groß=se-rie
2346 # kein ß in de-1996:
2348 # >>> for line in long2short([
2349 # ... "Basssaite;-2-;-3-;Bass=sai-te;-5-;Bass=sai-te;Bass=sai-te",
2350 # ... "Baßsaite;-2-;Baß=sai-te;-4-"]):
2351 # ... print(line)
2352 # Bass=sai-te
2354 # ß und Dreikonsonantenregel:
2356 # >>> for line in long2short([
2357 # ... "Flussschiffahrt;-2-;-3-;-4-;-5-;Fluss=schi{ff/ff=f}ahrt;-7-",
2358 # ... "Flußschiffahrt;-2-;Fluß=schi{ff/ff=f}ahrt;-4-",
2359 # ... "Flussschifffahrt;-2-;-3-;Fluss=schiff=fahrt"]):
2360 # ... print(line)
2361 # Fluss=schiff=fahrt
2363 # Zusätzliche Variante (Fremdwort vs. Lehnwort) in de-1901:
2365 # >>> for entry in short2long(['Boss;Boss # en.']): print(entry)
2366 # Boss;Boss # en.
2367 # >>> for entry in long2short(['Boss;Boss # en.']): print(entry)
2368 # Boss;Boss # en.
2369 # >>> for entry in long2short(['Boss;Boss # en.', 'Boß;-2-;Boß;-3- # < en.']):
2370 # ... print(entry)
2371 # Boss;Boss # en.
2372 # -1-;Boß # < en.
2374 # Alternativschreibung in de-1996 (Geschoß, Löß):
2376 # >>> for entry in long2short(['Geschoss;-2-;-3-;Ge<schoss;Ge<schoss',
2377 # ... 'Geschoß;Ge<schoß # österr. auch de-1996']):
2378 # ... print(entry)
2379 # Ge<schoss
2380 # Ge<schoß # österr. auch de-1996
2382 # Eigennamen auf -ss:
2384 # >>> for entry in long2short(['Vossstrasse;-2-;-3-;-4-;-5-;Voss=stra-sse;Voss=stras-se;Voss=stras-se',
2385 # ... 'Vossstraße;Voss=stra-ße',
2386 # ... 'Voßstraße;Voß=stra-ße']):
2387 # ... print(entry)
2388 # Voss=stra-ße;Voss=stra-ße
2389 # Voß=stra-ße
2391 # Kommentare:
2393 # >>> long2short(['# toller Kommentar'], prune=False)
2394 # [ShortEntry('# toller Kommentar')]
2395 # >>> long2short(['# toller Kommentar'], prune=True)
2396 # [ShortEntry('# toller Kommentar')]
2399 # Hilfsfunktionen
2400 # ===============
2402 # join_word()
2403 # -----------
2405 # Trennzeichen entfernen::
2407 def join_word(wort, assert_complete=False):
2409 # Einfache Trennzeichen:
2411 # === =================================== ==========================
2412 # = Trennstelle an Wortfugen Wort=fu-ge
2413 # < Trennstelle nach Präfix Vor<sil-be
2414 # > Trennstelle vor Suffix Freund>schaf-ten
2415 # \- Nebentrennstelle ge-hen
2416 # . unerwünschte/ungünstige Trennstelle Fett=em<.bo-li.en
2417 # · Randtrennstelle für Notentext O·bo·e
2418 # === =================================== ==========================
2420 # ::
2422 wort = re.sub('[-=<>.·]', '', wort)
2424 # Spezielle Trennungen für die traditionelle Rechtschreibung
2425 # (siehe ../../dokumente/README.wortliste)::
2427 if '{' in wort or '}' in wort:
2428 wort = wort.replace('{ck/kk}', 'ck')
2429 wort = wort.replace('{ck/k', 'k')
2430 wort = wort.replace('k}', 'k')
2431 # Konsonanthäufungen an Wortfuge: '{xx/xxx}' -> 'xx':
2432 wort = re.sub(r'\{(.)\1/\1\1\1\}', r'\1\1', wort)
2433 # schon getrennt: ('{xx/xx' -> 'xx' und 'x}' -> 'x'):
2434 wort = re.sub(r'\{(.)\1/\1\1$', r'\1\1', wort)
2435 wort = re.sub(r'^(.)\}', r'\1', wort)
2437 # Trennstellen in doppeldeutigen Wörtern::
2439 if '[' in wort or ']' in wort:
2440 wort = re.sub(r'\[(.*)/\1\]', r'\1', wort)
2441 # schon getrennt:
2442 wort = re.sub(r'\[([^/\[]+)$', r'\1', wort)
2443 wort = re.sub(r'^([^/\]]+)\]', r'\1', wort)
2445 # Test auf verbliebene komplexe Trennstellen::
2447 if assert_complete:
2448 for spez in '[{/}]':
2449 if spez in wort:
2450 raise AssertionError('Spezialtrennung %s, %s' %
2451 (wort, wort))
2453 return wort
2455 # zerlege()
2456 # ---------
2458 # Zerlege ein Wort mit Trennzeichen in eine Liste von Silben und eine Liste
2459 # von Trennzeichen)
2461 # >>> from wortliste import zerlege
2463 # >>> zerlege('Haupt=stel-le')
2464 # (['Haupt', 'stel', 'le'], ['=', '-'])
2465 # >>> zerlege('Ge<samt=be<triebs=rats==chef')
2466 # (['Ge', 'samt', 'be', 'triebs', 'rats', 'chef'], ['<', '=', '<', '=', '=='])
2467 # >>> zerlege('an<stands>los')
2468 # (['an', 'stands', 'los'], ['<', '>'])
2469 # >>> zerlege('An<al.pha-bet')
2470 # (['An', 'al', 'pha', 'bet'], ['<', '.', '-'])
2472 # ::
2474 def zerlege(wort):
2475 silben = re.split('[-·._<>=]+', wort)
2476 trennzeichen = re.split('[^-·._|<>=]+', wort)
2477 return silben, [tz for tz in trennzeichen if tz]
2479 # alternatives()
2480 # --------------
2482 # Gib einen String mit Trennmarkierung für abweichende Trennungen in
2483 # mehrdeutigen Wörtern zurück:
2485 # >>> from wortliste import alternatives
2486 # >>> alternatives('Mas-se', 'Ma-sse')
2487 # 'Ma[s-/-s]se'
2488 # >>> alternatives('Ma-sse', 'Mas-se')
2489 # 'Ma[-s/s-]se'
2491 # ::
2493 def alternatives(wort1, wort2):
2494 # convert to lists
2495 wort1 = [c for c in wort1]
2496 wort2 = [c for c in wort2]
2497 pre = []
2498 post = []
2499 for c1, c2 in zip(wort1, wort2):
2500 if c1 != c2:
2501 break
2502 pre += c1
2503 for c1, c2 in zip(wort1.__reversed__(), wort2.__reversed__()):
2504 if c1 != c2:
2505 break
2506 post += c1
2507 post.reverse()
2508 return ''.join(pre + ['['] + wort1[len(pre):-len(post)] + ['/']
2509 + wort2[len(pre):-len(post)] + [']'] + post)
2512 # toggle_case()
2513 # -------------
2515 # Großschreibung in Kleinschreibung wandeln und umgekehrt
2517 # Diese Version funktioniert auch für Wörter mit Trennzeichen (während
2518 # str.title() nach jedem Trennzeichen wieder groß anfängt)
2520 # >>> from wortliste import toggle_case
2521 # >>> toggle_case('Ha-se')
2522 # 'ha-se'
2523 # >>> toggle_case('arm')
2524 # 'Arm'
2525 # >>> toggle_case('frei=bier')
2526 # 'Frei=bier'
2527 # >>> toggle_case('L}a-ger')
2528 # 'l}a-ger'
2530 # Keine Änderung bei Wörtern mit Großbuchstaben im Inneren:
2532 # >>> toggle_case('USA')
2533 # 'USA'
2534 # >>> toggle_case('iRFD')
2535 # 'iRFD'
2537 # >>> toggle_case('gri[f-f/{ff/ff')
2538 # 'Gri[f-f/{ff/ff'
2539 # >>> toggle_case('Gri[f-f/{ff/ff')
2540 # 'gri[f-f/{ff/ff'
2542 # ::
2544 def toggle_case(wort):
2545 try:
2546 key = join_word(wort, assert_complete=True)
2547 except AssertionError:
2548 key = wort[0]
2549 if key.istitle():
2550 return wort.lower()
2551 elif key.islower():
2552 return wort[0].upper() + wort[1:]
2553 else:
2554 return wort
2556 # sortkey_duden()
2557 # ---------------
2559 # Schlüssel für die alphabetische Sortierung gemäß Duden-Regeln.
2561 # >>> from wortliste import sortkey_duden
2562 # >>> sortkey_duden(["Abflußröhren"])
2563 # 'abflussrohren a*bflu*szroehren'
2564 # >>> sortkey_duden(["Abflußrohren"])
2565 # 'abflussrohren a*bflu*szro*hren'
2566 # >>> sortkey_duden(["Abflussrohren"])
2567 # 'abflussrohren'
2569 # >>> s = sorted([["Abflußröhren"], ["Abflußrohren"], ["Abflussrohren"]],
2570 # ... key=sortkey_duden)
2571 # >>> print(', '.join(e[0] for e in s))
2572 # Abflussrohren, Abflußrohren, Abflußröhren
2574 # Umschreibung
2576 # Ligaturen auflösen und andere "normalisierende" Ersetzungen für den
2577 # (Haupt-)Sortierschlüssel (Akzente werden über ``unicodedata.normalize``
2578 # entfernt)::
2580 umschrift_skey = {
2581 ord('æ'): 'ae',
2582 ord('œ'): 'oe',
2583 ord('ſ'): 's',
2586 # "Zweitschlüssel" zur Unterscheidung von Umlauten/SZ und Basisbuchstaben::
2588 umschrift_subkey = {
2589 ord('a'): 'a*',
2590 ord('å'): 'aa',
2591 ord('ä'): 'ae',
2592 ord('o'): 'o*',
2593 ord('ö'): 'oe',
2594 ord('ø'): 'oe',
2595 ord('u'): 'u*',
2596 ord('ü'): 'ue',
2597 ord('ß'): 'sz',
2600 # Argument ist ein Eintrag im WordEntry oder ShortEntry Format oder
2601 # ein (Unicode) String:
2603 # >>> print(sortkey_duden(weste)) # WordEntry
2604 # weste
2605 # >>> print(sortkey_duden(dst)) # ShortEntry
2606 # dienste
2608 # >>> print(sortkey_duden('Stra-ße'))
2609 # strasse stra*sze
2610 # >>> print(sortkey_duden(['Büh-ne']))
2611 # buhne buehne
2612 # >>> print(sortkey_duden('Weiß=flog;Weiß=flog;-3-;-4-;-5-'))
2613 # weissflog weiszflo*g
2614 # >>> print(sortkey_duden('-1-;Meß=sen-der;-3-;-4-;-5-'))
2615 # messsender meszsender
2616 # >>> print(sortkey_duden('As-sen # (geogr. und Eigen-) Name\n'))
2617 # assen
2618 # >>> print(sortkey_duden('aßen;aßen;-3-;-4-;-5-\n'))
2619 # assen a*szen
2620 # >>> print(sortkey_duden('äßen\n'))
2621 # assen aeszen
2623 # ::
2625 def sortkey_duden(entry):
2627 # ggf. ungetrenntes Wort extrahieren oder generieren::
2629 if isinstance(entry, list):
2630 try:
2631 key = entry.key()
2632 except AttributeError:
2633 key = entry[0]
2634 if len(entry) == 1: # ein Muster pro Zeile, siehe z.B. pre-1901
2635 key = join_word(key)
2636 else:
2637 match = re.search('^[-0-9;]*([^;\s]+)', entry) # erstes volles Feld
2638 if match:
2639 key = match.group(1)
2640 else:
2641 key = ''
2642 key = join_word(key)
2643 key = re.sub('[0-9;]', '', key)
2645 # Großschreibung ignorieren:
2647 # Der Duden sortiert Wörter, die sich nur in der Großschreibung unterscheiden
2648 # "klein vor groß" (ASCII sortiert "groß vor klein"). In der
2649 # `Trennmuster-Wortliste` kommen Wörter nur mit der häufiger anzutreffenden
2650 # Großschreibung vor, denn der TeX-Trennalgorithmus ignoriert Großschreibung.
2651 # ::
2653 key = key.lower()
2655 # Ersetzungen:
2657 # ß -> ss ::
2659 skey = key.replace('ß', 'ss')
2661 # Restliche Akzente weglassen: Wandeln in Darstellung von Buchstaben mit
2662 # Akzent als "Grundzeichen + kombinierender Akzent". Anschließend alle
2663 # nicht-ASCII-Zeichen ignorieren::
2665 skey = skey.translate(umschrift_skey)
2666 skey = unicodedata.normalize('NFKD', skey)
2667 skey = skey.encode('ascii', 'ignore').decode()
2669 # "Zweitschlüssel" für das eindeutige Einsortieren von Wörtern mit
2670 # gleichem Schlüssel (Masse/Maße, waren/wären, ...):
2672 # * "*" nach aou für die Unterscheidung Grund-/Umlaut
2673 # * ß->sz
2675 # ::
2677 if key != skey:
2678 subkey = key.translate(umschrift_subkey)
2679 skey = '%s %s' % (skey,subkey)
2681 # Gib den Sortierschlüssel zurück::
2683 return skey
2687 # udiff()
2688 # -------
2690 # Vergleiche zwei Sequenzen von `WordEntries` (genauer: alle Objekte, die
2691 # sich sinnvoll zu Unicode wandeln lassen).
2693 # Beispiel:
2695 # >>> from wortliste import udiff
2696 # >>> print(udiff([abbeissen, aalbestand], [abbeissen,dresz], 'alt', 'neu'))
2697 # --- alt
2698 # +++ neu
2699 # @@ -1,2 +1,2 @@
2700 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
2701 # -Aalbestand;Aal=be<stand # Test
2702 # +Dreß;-2-;Dreß;-4-
2703 # <BLANKLINE>
2704 # >>> udiff([abbeissen, aalbestand], [abbeissen, aalbestand], 'alt', 'neu')
2705 # ''
2707 # ::
2709 def udiff(a, b, fromfile='', tofile='',
2710 fromfiledate='', tofiledate='', n=1):
2712 a = [str(entry).rstrip() for entry in a]
2713 b = [str(entry).rstrip() for entry in b]
2715 diff = '\n'.join(difflib.unified_diff(a, b, fromfile, tofile,
2716 fromfiledate, tofiledate, n, lineterm=''))
2717 if diff:
2718 diff += '\n'
2719 return diff
2722 # run_filters()
2723 # -------------
2725 # Anwenden der Trennstile auf ein Wort:
2727 # >>> from wortliste import run_filters
2729 # >>> run_filters(['keine_flattervokale'], 'Psy-ch<i-a-trie')
2730 # 'Psy-ch<ia-trie'
2731 # >>> run_filters(['keine_flattervokale'], 'An<woh-ner=in<.i-ti-a-ti-ve')
2732 # 'An<woh-ner=in<.i-tia-ti-ve'
2734 # Test: Trennungen wie bisher im 'dehyphen-exptl' TeX-Paket:
2736 # >>> run_filters(['morphemisch', 'standard'], 'Psy-ch<i-a-trie')
2737 # 'Psych<ia-trie'
2738 # >>> run_filters(['morphemisch', 'standard'], 'An<woh-ner=in<.i-ti-a-ti-ve')
2739 # 'An<woh-ner=initia-ti-ve'
2741 # >>> run_filters(['standard'], 'Text=il<..lu-stra-ti-.on')
2742 # 'Text=illu-stra-tion'
2744 # >>> run_filters(['morphemisch'], 'Psy-ch<i-a-trie')
2745 # 'Psych<i·a-trie'
2746 # >>> run_filters(['morphemisch'], 'An<woh-ner=in<.i-ti-a-ti-ve')
2747 # 'An<woh-ner=in<.i·ti-a-ti-ve'
2749 # >>> run_filters(['morphemisch'], 'ge-r<i-a-tri-sche')
2750 # 'ger<i·a-tri-sche'
2751 # >>> run_filters(['syllabisch','regelsilben'], 'ge-r<i-a-tri-sche')
2752 # 'ge-ri-at-ri-sche'
2754 # >>> run_filters(['regelsilben'], 'abs-trakt')
2755 # 'abst-rakt'
2757 # >>> run_filters(['morphemisch'], 'Fern=ab<.i-tur')
2758 # 'Fern=ab<.i·tur'
2759 # >>> run_filters(['syllabisch','regelsilben'], 'Fern=ab<.i-tur')
2760 # 'Fern=abi-tur'
2761 # >>> run_filters(['morphemisch', 'standard'], 'Fern=ab<.i-tur')
2762 # 'Fern=abitur'
2764 # ::
2766 def run_filters(styles, word):
2767 """Apply a sequence of hyphenation `styles` to `word`."""
2769 for style in styles:
2770 try:
2771 word = getattr(stilfilter, style)(word)
2772 except AttributeError:
2773 if style:
2774 raise ValueError('Trennstil %s nicht definiert. Siehe "--stilliste"' % style)
2775 return word
2778 # normalize_language_tag(tag)
2779 # ---------------------------
2781 # Normalisierung und Expansion von Sprachtags nach [BCP47]_
2783 # >>> from wortliste import normalize_language_tag
2784 # >>> normalize_language_tag('de_AT-1901')
2785 # ['de-AT-1901', 'de-AT', 'de-1901', 'de']
2787 # >>> normalize_language_tag('de') # Deutsch, allgemeingültig
2788 # ['de']
2789 # >>> normalize_language_tag('de_1901') # traditionell (Reform 1901)
2790 # ['de-1901', 'de']
2791 # >>> normalize_language_tag('de_1996') # reformiert (Reform 1996)
2792 # ['de-1996', 'de']
2793 # >>> normalize_language_tag('de_CH') # ohne ß (Schweiz oder versal)
2794 # ['de-CH', 'de']
2795 # >>> normalize_language_tag('de-x-versal') # versal
2796 # ['de-x-versal', 'de']
2797 # >>> normalize_language_tag('de-1901-x-versal') # versal
2798 # ['de-1901-x-versal', 'de-1901', 'de-x-versal', 'de']
2799 # >>> normalize_language_tag('de_CH-1996') # Schweiz traditionell (süssauer)
2800 # ['de-CH-1996', 'de-CH', 'de-1996', 'de']
2802 # 'de': 1, # Deutsch, allgemeingültig
2803 # 'de-1901': 2, # "traditionell" (nach Rechtschreibreform 1901)
2804 # 'de-1996': 3, # reformierte Reformschreibung (1996)
2805 # 'de-x-versal': 4, # ohne ß (Schweiz oder versal) allgemein
2806 # # 'de-CH': 4, # Alias
2807 # 'de-1901-x-versal': 5, # ohne ß (Schweiz oder versal) "traditionell"
2808 # 'de-1996-x-versal': 6, # ohne ß (Schweiz oder versal) "reformiert"
2809 # # 'de-CH-1996': 6, # Alias
2810 # 'de-CH-1901': 7, # ohne ß (Schweiz) "traditionell" ("süssauer")
2813 # ::
2815 def normalize_language_tag(tag):
2816 """Return a list of normalized combinations for a `BCP 47` language tag.
2818 # normalize:
2819 tag = tag.replace('_','-')
2820 # split (except singletons, which mark the following tag as non-standard):
2821 tag = re.sub(r'-([a-zA-Z0-9])-', r'-\1_', tag)
2822 taglist = []
2823 subtags = [subtag.replace('_', '-') for subtag in tag.split('-')]
2824 base_tag = [subtags.pop(0)]
2825 # find all combinations of subtags
2826 for n in range(len(subtags), 0, -1):
2827 # for tags in unique_combinations(subtags, n):
2828 for tags in itertools.combinations(subtags, n):
2829 taglist.append('-'.join(base_tag+list(tags)))
2830 taglist += base_tag
2831 return taglist
2834 # Iteratoren
2835 # ==========
2837 # filelines
2838 # ---------
2840 # Iterator über mehrere Eingabedateien.
2841 # Ergibt Sequenz von Zeilen als Unicode-Strings::
2843 def filelines(names):
2844 for name in names:
2845 if name=='-':
2846 infile = sys.stdin
2847 else:
2848 infile = open(name, encoding='utf-8')
2849 for line in infile:
2850 yield line.rstrip()
2853 # sprachauszug
2854 # ------------
2856 # Iterator über Worliste-Datei(en)
2858 # Ausgabe: Nach Trennregeln der Sprachvariante getrenntes Wort.
2861 # ::
2863 def sprachauszug(infiles, language="de-1996", entry_class=WordEntry, verbose=False):
2865 for line in filelines(infiles):
2867 # Zeile lesen und in WordEntry oder ShortEntry Objekt wandeln::
2869 if not line or line.startswith('#'):
2870 if verbose:
2871 yield line
2872 continue
2873 entry = entry_class(line)
2875 # Wort in der gewünschten Sprachvarietät aussuchen::
2877 word = entry.get(language)
2878 if not word:
2879 if verbose:
2880 yield ('# ' + line)
2881 continue
2883 yield word
2886 # Tests
2887 # =====
2889 # Teste Übereinstimmung des ungetrennten Wortes in Feld 1 mit den
2890 # Trennmustern nach Entfernen der Trennmarker. Schreibe Inkonsistenzen auf die
2891 # Standardausgabe.
2893 # Das Argument ist ein Iterator über die Einträge (Klasse `WordEntry`). ::
2895 def test_keys(wortliste):
2896 print("Teste Schlüssel-Trennmuster-Übereinstimmung:")
2897 is_OK = True
2898 for entry in wortliste:
2899 if isinstance(entry, ShortEntry):
2900 print("Wortliste im Kurzformat: überspringe Schlüssel-Test.")
2901 return
2902 # Test der Übereinstimmung ungetrenntes/getrenntes Wort
2903 # für alle Felder:
2904 key = entry.key()
2905 for wort in entry[1:]:
2906 if not wort: # leere Felder
2907 continue
2908 if key != join_word(wort):
2909 is_OK = False
2910 print(" key '%s' != join_word('%s')" % (key, wort),)
2911 if key.lower() == join_word(wort).lower():
2912 print(" Abgleich der Großschreibung mit"
2913 "`prepare-patch.py grossabgleich`.", end=' ')
2914 if is_OK:
2915 print("OK")
2918 # Finde Doppeleinträge (teste, ob jeder Schlüssel nur einmal vorkommt).
2919 # Schreibe Inkonsistenzen auf die Standardausgabe.
2921 # Das Argument ist ein Iterator über die Einträge (Klasse `WordEntry`). ::
2923 def test_uniqueness(wortliste):
2925 doppelte = 0
2926 words = {}
2927 for entry in wortliste:
2928 key = entry.key()
2929 if key in words:
2930 doppelte += 1
2931 print("da ", str(words[key]))
2932 print("neu", str(entry))
2933 words[key] = entry
2934 print("%d Doppeleinträge." % doppelte)
2936 # Teste die Wandlung einer Zeile im "wortliste"-Format in eine
2937 # ``WordEntry``-Instanz und zurück::
2939 def test_str_entry_str_conversion(wordfile):
2940 OK = 0
2941 for line in filelines([wordfile.name]):
2942 entry = WordEntry(line)
2943 if line == str(entry):
2944 OK +=1
2945 else:
2946 print('-', line,)
2947 print('+', str(entry))
2949 print(OK, "Einträge rekonstruiert")
2952 # Teste Vervollständigung und Zusammenfassung von Einträgen::
2954 def test_completion_pruning(entries):
2955 reko = []
2956 for entry in entries:
2957 new = copy.copy(entry)
2958 new.complete()
2959 new.prune()
2960 reko.append(new)
2961 patch = udiff(entries, reko, 'wortliste', 'neu')
2962 if patch:
2963 print(patch)
2964 else:
2965 print("alle Einträge rekonstruiert")
2967 def test_doppelte(entries):
2968 doppelte = 0
2969 langs = ("de", "de-1901", "de-CH", "de-1901-x-versal", "de-CH-1901")
2970 for lang in langs:
2971 words[lang] = {}
2973 for entry in entries:
2974 entry.complete()
2975 for lang in langs:
2976 word = entry.get(lang)
2977 key = join_word(word)
2978 if word and (key in words[lang]):
2979 oldword = words[lang][key].get(lang)
2980 if oldword and (word != oldword):
2981 doppelte += 1
2982 print("%-16s" % lang, oldword, '!=', word)
2983 print(" "*18, str(words[lang][key].pruned()))
2984 print(" "*18, str(entry.pruned()))
2985 words[lang][key] = entry
2986 print("%d Doppeleinträge." % doppelte)
2990 # Aufruf von der Kommandozeile
2991 # ============================
2993 # ::
2995 if __name__ == '__main__':
2997 print("Test der Werkzeuge und inneren Konsistenz der Wortliste")
2999 # Ein WordFile Dateiobjekt::
3001 try:
3002 wordfile = WordFile(sys.argv[1], format='auto')
3003 except IndexError:
3004 wordfile = WordFile('../../../wortliste')
3005 # wordfile = WordFile('../../../wlst', format='auto') # wortliste im Kurzformat
3006 # wordfile = WordFile('neu.todo')
3007 # wordfile = WordFile('neu-kurz.todo', format='f5')
3008 # wordfile = WordFile('korrektur.todo', format='auto')
3010 # Format bestimmen::
3012 print("Datei: '%s'" % wordfile.name, "Format:", wordfile.format)
3014 # Liste der Datenfelder (die Klasseninstanz als Argument für `list` liefert
3015 # den Iterator über die Felder, `list` macht daraus eine Liste)::
3017 wordlist = list(wordfile)
3018 print(len(wordlist), "Zeilen")
3019 print(wordlist[0], type(wordlist[0]))
3021 # Ein Wörterbuch (dict Instanz)::
3023 wordfile.seek(0) # Pointer zurücksetzen
3024 words = wordfile.asdict()
3025 print(len(words), "Wörterbucheinträge")
3027 # Test auf Doppeleinträge::
3029 if len(words) != len(wordlist) and wordfile.format != "f5":
3030 test_uniqueness(wordlist)
3032 # Teste Schlüssel-Trennmuster-Übereinstimmung::
3034 if isinstance(wordlist[0], WordEntry):
3035 test_keys(wordlist)
3037 # Teste Eintrags-Konsistenz im Kurzformat::
3039 # if isinstance(wordlist[0], ShortEntry):
3040 # test_doppelte(wordlist)
3042 # Teste Komplettieren/Zusammenfassen der Einträge::
3044 # test_completion_pruning(wordlist)
3046 # Sprachauswahl::
3048 # Sprachtags:
3050 # sprache = 'de-1901' # traditionell
3051 sprache = 'de-1996' # Reformschreibung
3052 # sprache = 'de-x-versal' # ohne ß (Schweiz oder versal) allgemein
3053 # sprache = 'de-1901-x-versal' # ohne ß (Schweiz oder versal) "traditionell"
3054 # sprache = 'de-1996-x-versal' # ohne ß (Schweiz oder versal) "reformiert"
3055 # sprache = 'de-CH-1901' # ohne ß (Schweiz) "traditionell" ("süssauer")
3057 # worte = [entry.get(sprache) for entry in wordlist]
3058 # worte = [wort for wort in worte if wort]
3059 # print(len(worte), "Einträge für Sprachvariante", sprache)
3061 # Zeilenrekonstruktion::
3063 # test_str_entry_str_conversion(wordfile)
3066 # Teste Konsistenz der Auszeichnung::
3068 # schwankungsfaelle = {}
3069 # words = [entry.get(sprache) for entry in wordlist]
3070 # for word in words:
3071 # if '.' in word:
3072 # match = re.search('[<=]([^<=]*[aeiouyäöü]\.[aeiouyäöü])[^<=]', word)
3073 # if not match:
3074 # match = re.search('^(.*[aeiouyäöü]\.[aeiouyäöü])[^<=]', word)
3075 # if match:
3076 # key = match.group(1).lower()
3077 # key = key[:-2] + '-' + key[-1]
3078 # schwankungsfaelle[key] = word
3079 # print(len(schwankungsfaelle), "Schwankungsfälle (von </= bis eins nach .)")
3080 # # for key, value in schwankungsfaelle.items():
3081 # # print("%15s" % key, ":", value)
3082 # print("Inkonsistenzien:")
3083 # for word in words:
3084 # for key in schwankungsfaelle:
3085 # if (word.startswith(key.lower())
3086 # or '=' + key in word
3087 # or '<' + key in word):
3088 # print(key[:-2] + '*' + key[-1], schwankungsfaelle[key], word)
3091 # Quellen
3092 # =======
3094 # .. [BCP47] A. Phillips und M. Davis, (Editoren.),
3095 # `Tags for Identifying Languages`, http://www.rfc-editor.org/rfc/bcp/bcp47.txt
3097 # .. _aktuelle Rechtschreibung:
3099 # .. [Rechtschreibregeln] Rat für deutsche Rechtschreibung,
3100 # `Deutsche Rechtschreibung – Regeln und Wörterverzeichnis`,
3101 # http://www.rechtschreibrat.com/regeln-und-woerterverzeichnis/
3103 # .. _traditionelle Rechtschreibung:
3105 # .. [Duden1991] Wissenschaftlicher Rat der Dudenredaktion (Editoren),
3106 # `Duden: Rechtschreibung der deutschen Sprache`,
3107 # Dudenverlag Mannheim, 1991.
3109 # .. _Wortliste der deutschsprachigen Trennmustermannschaft:
3110 # ../../../dokumente/README.wortliste
3112 # .. _wortliste: ../../../wortliste