Neustrukturierung der Python Skripte als Paket.
[wortliste.git] / skripte / python / edit_tools / wortliste.py
blob44e5f8c5b72c778af4c8dac1b2cdf5ba579c0de2
1 #!/usr/bin/env python
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 # ***********
13 # ::
15 """Hilfsmittel für die Arbeit mit der `Wortliste`"""
17 # .. contents::
19 # Die hier versammelten Funktionen und Klassen dienen der Arbeit an und
20 # mit der freien `Wortliste der deutschsprachigen Trennmustermannschaft`_
21 # ("Lembergsche Liste")
23 # Vorspann
25 # ::
27 import difflib
28 import re
29 import codecs
30 import unicodedata
32 # WordFile
33 # ========
35 # Klasse zum Lesen und Schreiben der `Wortliste`::
37 class WordFile(file):
39 # encoding
40 # --------
42 # ::
44 encoding = 'utf8'
46 # Iteration
47 # ---------
49 # Die spezielle Funktion `__iter__` wird aufgerufen wenn über eine
50 # Klasseninstanz iteriert wird.
52 # Liefer einen Iterator über die "geparsten" Zeilen (Datenfelder)::
54 def __iter__(self):
55 line = self.readline().rstrip().decode(self.encoding)
56 while line:
57 yield WordEntry(line)
58 line = self.readline().rstrip().decode(self.encoding)
60 # asdict
61 # ------
63 # Lies Datei und trage die Zeilen mit ungetrenntem Wort
64 # als `key` und den Datenfeldern als `value` in ein `dictionary`
65 # (assoziatives Array) ein::
67 def asdict(self):
68 words = dict()
69 for entry in self:
70 words[entry[0]] = entry
71 return words
73 # writelines
74 # -----------
76 # Schreibe eine Liste von `unicode` Strings (Zeilen ohne Zeilenendezeichen)
77 # in die Datei `destination`::
79 def writelines(self, lines, destination, encoding=None):
80 outfile = codecs.open(destination, 'w',
81 encoding=(encoding or self.encoding))
82 outfile.write(u'\n'.join(lines))
83 outfile.write(u'\n')
85 # write_entry
86 # ------------
88 # Schreibe eine Liste von Datenfeldern (geparste Zeilen) in die Datei
89 # `destination`::
91 def write_entry(self, wortliste, destination, encoding=None):
92 lines = [unicode(entry) for entry in wortliste]
93 self.writelines(lines, destination, encoding)
96 # WordEntry
97 # =========
99 # Klasse für Einträge (Zeilen) der Wortliste
101 # Beispiel:
103 # >>> from wortliste import WordEntry
105 # >>> aalbestand = WordEntry(u'Aalbestand;Aal=be<stand # Test')
106 # >>> print aalbestand
107 # Aalbestand;Aal=be<stand # Test
109 # ::
111 class WordEntry(list):
113 # Argumente
114 # ---------
116 # Kommentare (aktualisiert, wenn Kommentar vorhanden)::
118 comment = u''
120 # Feldbelegung:
122 # 1. Wort ungetrennt
123 # 2. Wort mit Trennungen, falls für alle Varianten identisch,
124 # anderenfalls leer
125 # 3. falls Feld 2 leer, Trennung nach traditioneller Rechtschreibung
126 # 4. falls Feld 2 leer, Trennung nach reformierter Rechtschreibung (2006)
127 # 5. falls Feld 2 leer, Trennung für Wortform, die entweder in
128 # der Schweiz oder mit Großbuchstaben oder Kapitälchen benutzt wird
129 # und für traditionelle und reformierte Rechtschreibung identisch ist
130 # 6. falls Feld 5 leer, Trennung für Wortform, die entweder in
131 # der Schweiz oder mit Großbuchstaben oder Kapitälchen benutzt wird,
132 # traditionelle Rechtschreibung
133 # 7. falls Feld 5 leer, Trennung für Wortform, die entweder in
134 # der Schweiz oder mit Großbuchstaben oder Kapitälchen benutzt wird,
135 # reformierte Rechtschreibung (2006)
136 # 8. falls Feld 5 leer, Trennung nach (deutsch)schweizerischer
137 # Rechtschreibung; insbesondere Wörter mit "sss" gefolgt von
138 # einem Vokal, die wie andere Dreifachkonsonanten gehandhabt wurden
139 # (also anders, als der Duden früher vorgeschrieben hat), z.B.
140 # "süssauer"
142 # Sprachvarianten (Tags nach [BCP47]_) (Die Zählung der Indizes beginn in
143 # Python bei 0)::
145 sprachvarianten = {
146 'de': 1, # Deutsch, allgemeingültig
147 'de-1901': 2, # "traditionell" (nach Rechtschreibreform 1901)
148 'de-1996': 3, # reformierte Reformschreibung (1996)
149 'de-x-GROSS': 4, # ohne ß (Schweiz oder GROSS) allgemein
150 'de-1901-x-GROSS': 5, # ohne ß (Schweiz oder GROSS) "traditionell"
151 'de-1996-x-GROSS': 6, # ohne ß (Schweiz oder GROSS) "reformiert"
152 # 'de-CH-1996': 6, # Alias für 'de-1996-x-GROSS'
153 'de-CH-1901': 7, # ohne ß (Schweiz) "traditionell" ("süssauer")
157 # Initialisierung::
159 def __init__(self, line, delimiter=';'):
160 self.delimiter = delimiter
162 # eventuell vorhandenen Kommentar abtrennen und speichern::
164 if '#' in line:
165 line = line.split(u'#')
166 self.comment = u'#'.join(line[1:])
167 line = line[0].rstrip()
169 # Zerlegen in Datenfelder, in Liste eintragen::
171 list.__init__(self, line.split(delimiter))
174 # Rückverwandlung in String
175 # -----------------------------------
177 # Erzeugen eines Eintrag-Strings (Zeile) aus der Liste der Datenfelder und
178 # dem Kommentar
180 # >>> unicode(aalbestand)
181 # u'Aalbestand;Aal=be<stand # Test'
183 # ::
185 def __unicode__(self):
186 line = ';'.join(self)
187 if self.comment:
188 line += ' #' + self.comment
189 return line
192 def __str__(self):
193 return unicode(self).encode('utf8')
195 # lang_index
196 # ---------------
198 # Index des zur Sprachvariante gehörenden Datenfeldes:
200 # >>> aalbestand.lang_index('de')
202 # >>> aalbestand.lang_index('de-1901')
204 # >>> aalbestand.lang_index('de-1996')
206 # >>> aalbestand.lang_index('de-x-GROSS')
208 # >>> aalbestand.lang_index('de-1901-x-GROSS')
210 # >>> aalbestand.lang_index('de-1996-x-GROSS')
212 # >>> abbeissen = WordEntry(
213 # ... u'abbeissen;-2-;-3-;-4-;-5-;ab<bei-ssen;ab<beis-sen;ab<beis-sen')
214 # >>> print abbeissen.lang_index('de')
215 # None
216 # >>> print abbeissen.lang_index('de-x-GROSS')
217 # None
218 # >>> abbeissen.lang_index('de-CH-1901')
221 # ::
223 def lang_index(self, lang):
225 assert lang in self.sprachvarianten, \
226 'Sprachvariante "%s" nicht in %s' % (lang,
227 self.sprachvarianten.keys())
229 # Einfacher Fall: eine allgemeine Schreibweise::
231 if len(self) == 2:
232 return 1
234 # Spezielle Schreibung::
236 try:
237 i = self.sprachvarianten[lang]
238 feld = self[i]
239 except IndexError:
240 if i > 4 and len(self) == 5:
241 return 4 # Allgemeine Schweiz/GROSS Schreibung:
242 return None # Feld nicht vorhanden
244 if feld.startswith('-'): # '-1-', '-2-', ...
245 return None # leeres Feld
247 return i
249 # Trennmuster für Sprachvariante ausgeben
251 # >>> aalbestand.get('de')
252 # u'Aal=be<stand'
253 # >>> aalbestand.get('de-1901')
254 # u'Aal=be<stand'
255 # >>> aalbestand.get('de-1996')
256 # u'Aal=be<stand'
257 # >>> aalbestand.get('de-x-GROSS')
258 # u'Aal=be<stand'
259 # >>> aalbestand.get('de-1901-x-GROSS')
260 # u'Aal=be<stand'
261 # >>> aalbestand.get('de-1996-x-GROSS')
262 # u'Aal=be<stand'
263 # >>> aalbestand.get('de-CH-1901')
264 # u'Aal=be<stand'
266 # >>> print abbeissen.get('de')
267 # None
268 # >>> print abbeissen.get('de-x-GROSS')
269 # None
270 # >>> print abbeissen.get('de,de-x-GROSS')
271 # None
272 # >>> abbeissen.get('de-1901-x-GROSS')
273 # u'ab<bei-ssen'
274 # >>> abbeissen.get('de,de-1901,de-1901-x-GROSS')
275 # u'ab<bei-ssen'
276 # >>> abbeissen.get('de-CH-1901')
277 # u'ab<beis-sen'
279 # ::
281 def get(self, sprachvarianten):
282 for lang in sprachvarianten.split(','):
283 i = self.lang_index(lang) # integer>0 or None
284 if i:
285 return self[i]
286 return None
288 # Trennmuster für Sprachvariante setzen
290 # >>> abbeissen.set('test', 'de-1901-x-GROSS')
291 # >>> print abbeissen
292 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
294 # >>> abbeissen.set('test', 'de-1901')
295 # Traceback (most recent call last):
296 # ...
297 # IndexError: kann kein leeres Feld setzen
299 # >>> abbeissen.set('test', 'de-1901,de-1901-x-GROSS')
300 # >>> print abbeissen
301 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
303 # ::
305 def set(self, wort, sprachvarianten):
306 for lang in sprachvarianten.split(','):
307 i = self.lang_index(lang)
308 if i is None:
309 continue
310 if wort is None:
311 wort = u'-%d-' % i+1
312 self[i] = wort
313 return
314 raise IndexError, "kann kein leeres Feld setzen"
316 # Felder für alle Sprachvarianten ausfüllen
318 # >>> print str(aalbestand), len(aalbestand)
319 # Aalbestand;Aal=be<stand # Test 2
320 # >>> aalbestand.expand_fields()
321 # >>> print len(aalbestand)
323 # >>> auffrass = WordEntry('auffrass;-2-;-3-;-4-;auf-frass')
324 # >>> auffrass.expand_fields()
325 # >>> print auffrass
326 # auffrass;-2-;-3-;-4-;auf-frass;auf-frass;auf-frass;auf-frass
328 # ::
330 def expand_fields(self):
331 fields = [self.get(sv) or '-%d-' % (self.sprachvarianten[sv] + 1)
332 for sv in sorted(self.sprachvarianten.keys(),
333 key=self.sprachvarianten.get)]
334 # return fields
335 for i, field in enumerate(fields):
336 try:
337 self[i+1] = field # Feld 1 ist "key" (ungetrennt)
338 except IndexError:
339 self.append(field)
342 # Felder für Sprachvarianten zusammenfassen
344 # >>> aalbestand.conflate_fields()
345 # >>> print aalbestand
346 # Aalbestand;Aal=be<stand # Test
347 # >>> auffrass.conflate_fields()
348 # >>> print auffrass
349 # auffrass;-2-;-3-;-4-;auf-frass
350 # >>> entry = WordEntry(u'distanziert;-2-;di-stan-ziert;di-stan-ziert')
351 # >>> entry.conflate_fields()
352 # >>> print entry
353 # distanziert;di-stan-ziert
355 # Aber nicht, wenn die Trennstellen sich unterscheiden:
357 # >>> abenddienste = WordEntry(
358 # ... u'Abenddienste;-2-;Abend=dien-ste;Abend=diens-te')
359 # >>> abenddienste.conflate_fields()
360 # >>> print abenddienste
361 # Abenddienste;-2-;Abend=dien-ste;Abend=diens-te
363 # ::
365 def conflate_fields(self):
366 if len(self) == 8:
367 if self[7] == self[6] == self[5]:
368 self[4] = self[5] # umschreiben auf GROSS-allgemein
369 self.pop()
370 self.pop()
371 self.pop()
372 if len(self) == 5:
373 if self[4] == self[2]: # de-x-GROSS == de-1901
374 self.pop()
375 else:
376 return
377 if len(self) >= 4:
378 if self[3] == self[2]: # de-1996 == de-1901
379 self[1] = self[2] # Umschreiben auf de (allgemein)
380 self.pop()
381 self.pop()
384 # Prüfe auf Vorkommen von Regeländerungen der Orthographiereform 1996.
386 # >>> entry = WordEntry(u'Würste;Wür-ste')
387 # >>> entry.regelaenderungen()
388 # >>> print unicode(entry)
389 # Würste;-2-;Wür-ste;Würs-te
390 # >>> entry = WordEntry(u'Würste;Würs-te')
391 # >>> entry.regelaenderungen()
392 # >>> print unicode(entry)
393 # Würste;-2-;Wür-ste;Würs-te
394 # >>> entry = WordEntry(u'Hecke;He-cke')
395 # >>> entry.regelaenderungen()
396 # >>> print unicode(entry)
397 # Hecke;-2-;He{ck/k-k}e;He-cke
398 # >>> entry = WordEntry(u'Ligusterhecke;Ligu-ster=he{ck/k-k}e')
399 # >>> entry.regelaenderungen()
400 # >>> print unicode(entry)
401 # Ligusterhecke;-2-;Ligu-ster=he{ck/k-k}e;Ligus-ter=he-cke
402 # >>> entry = WordEntry(u'Hass;Hass')
403 # >>> entry.regelaenderungen()
404 # >>> print unicode(entry)
405 # Hass;-2-;-3-;Hass;Hass
406 # >>> entry = WordEntry(u'fasst;fasst')
407 # >>> entry.regelaenderungen()
408 # >>> print unicode(entry)
409 # fasst;-2-;-3-;fasst;fasst
411 # ::
413 def regelaenderungen(self):
414 # Regeländerungen:
415 r1901 = (u'-st', u'{ck/k-k}')
416 r1996 = (u's-t', u'-ck')
417 # kein Schluss-ss und sst in de-1901
418 # aber: 'ßt' und Schluß-ß auch in de-1996 möglich (langer Vokal)
420 w1901 = self.get('de-1901')
421 w1996 = self.get('de-1996')
423 if w1901 is None or w1996 is None:
424 return
426 for r1, r2 in zip(r1901, r1996):
427 w1901 = w1901.replace(r2,r1)
428 w1996 = w1996.replace(r1,r2)
429 if u'sst' in w1901 or w1901.endswith(u'ss'):
430 w1901 = None
432 if w1901 == w1996: # keine Regeländerung im Wort
433 if len(self) > 2:
434 self.conflate_fields()
435 return
436 elif w1901 is None:
437 self.extend( ['']*(5-len(self)) )
438 self[1] = u'-2-'
439 self[2] = u'-3-'
440 self[3] = w1996
441 self[4] = w1996
442 else:
443 self.extend( ['']*(4-len(self)) )
444 self[1] = u'-2-'
445 self[2] = w1901
446 self[3] = w1996
450 # Funktionen
451 # ==========
453 # join_word
454 # ---------
456 # Trennzeichen entfernen::
458 def join_word(word, assert_complete=False):
460 # Einfache Trennzeichen:
462 # == ================================================================
463 # \· ungewichtete Trennstelle (solche, wo sich noch niemand um die
464 # Gewichtung gekümmert hat)
465 # \. unerwünschte Trennstelle (sinnentstellend), z.B. Ur·in.stinkt
466 # oder ungünstige Trennstelle (verwirrend), z.B. Atom·en.er·gie
467 # in ungewichteten Wörtern
468 # \= Trennstelle an Wortfugen (Wort=fu-ge)
469 # \< Trennstelle nach Präfix (Vor<sil-be)
470 # \> Trennstelle vor Suffix (Freund>schaf-ten)
471 # \- Nebentrennstelle (ge-hen)
472 # == ================================================================
474 # ::
476 table = {}
477 for char in u'·.=|-_<>':
478 table[ord(char)] = None
479 key = word.translate(table)
481 # Spezielle Trennungen für die traditionelle Rechtschreibung
482 # (siehe ../../dokumente/README.wortliste)::
484 if '{' in key or '}' in key:
485 key = key.replace(u'{ck/kk}', u'ck')
486 key = key.replace(u'{ck/k', u'k')
487 key = key.replace(u'k}', u'k')
488 # Konsonanthäufungen an Wortfuge: '{xx/xxx}' -> 'xx':
489 key = re.sub(ur'\{(.)\1/\1\1\1\}', ur'\1\1', key)
490 # schon getrennt: ('{xx/xx' -> 'xx' und 'x}' -> 'x'):
491 key = re.sub(ur'\{(.)\1/\1\1$', ur'\1\1', key)
492 key = re.sub(ur'^(.)\}', ur'\1', key)
494 # Trennstellen in doppeldeutigen Wörtern::
496 if '[' in key or ']' in key:
497 key = re.sub(ur'\[(.*)/\1\]', ur'\1', key)
498 # schon getrennt:
499 key = re.sub(ur'\[([^/\[]+)$', ur'\1', key)
500 key = re.sub(ur'^([^/\]]+)\]', ur'\1', key)
502 # Test auf verbliebene komplexe Trennstellen::
504 if assert_complete:
505 for spez in u'[{/}]':
506 if spez in key:
507 raise AssertionError('Spezialtrennung %s, %s' %
508 (word.encode('utf8'), key.encode('utf8')))
510 return key
512 # zerlege
513 # -------
515 # Zerlege ein Wort mit Trennzeichen in eine Liste von Silben und eine Liste
516 # von Trennzeichen)
518 # >>> from wortliste import zerlege
520 # >>> zerlege(u'Haupt=stel-le')
521 # ([u'Haupt', u'stel', u'le'], [u'=', u'-'])
522 # >>> zerlege(u'Ge<samt=be<triebs=rats==chef')
523 # ([u'Ge', u'samt', u'be', u'triebs', u'rats', u'chef'], [u'<', u'=', u'<', u'=', u'=='])
524 # >>> zerlege(u'an<stands>los')
525 # ([u'an', u'stands', u'los'], [u'<', u'>'])
526 # >>> zerlege(u'An<al.pha-bet')
527 # ([u'An', u'al', u'pha', u'bet'], [u'<', u'.', u'-'])
529 # ::
531 def zerlege(wort):
532 silben = re.split(u'[-·._<>=]+', wort)
533 trennzeichen = re.split(u'[^-·._|<>=]+', wort)
534 return silben, [tz for tz in trennzeichen if tz]
536 # TransferError
537 # -------------
539 # Fehler beim Übertragen von Trennstellen mit uebertrage_::
541 class TransferError(ValueError):
542 def __init__(self, wort1, wort2):
543 msg = u'Inkompatibel: %s %s' % (wort1, wort2)
544 ValueError.__init__(self, msg.encode('utf8'))
546 def __unicode__(self):
547 return str(self).decode('utf8')
550 # uebertrage
551 # ----------
553 # Übertrage die Trennzeichen von `wort1` auf `wort2`:
555 # >>> from wortliste import uebertrage, TransferError
557 # >>> uebertrage(u'Haupt=stel-le', u'Haupt·stel·le')
558 # u'Haupt=stel-le'
560 # Auch teilweise Übertragung, von "kategorisiert" nach "unkategorisiert":
562 # >>> print uebertrage(u'Haupt=stel-le', u'Haupt=stel·le')
563 # Haupt=stel-le
565 # >>> print uebertrage(u'Haupt·stel-le', u'Haupt=stel·le')
566 # Haupt=stel-le
568 # >>> print uebertrage(u'Aus<stel-ler', u'Aus-stel-ler')
569 # Aus<stel-ler
571 # >>> print uebertrage(u'Freund>schaf·ten', u'Freund-schaf-ten')
572 # Freund>schaf-ten
574 # Übertragung doppelter Marker:
576 # >>> print uebertrage(u'ver<<aus<ga-be', u'ver<aus<ga-be')
577 # ver<<aus<ga-be
579 # >>> print uebertrage(u'freund>lich>>keit', u'freund>lich>keit')
580 # freund>lich>>keit
582 # >>> print uebertrage(u'Amts==haupt=stel-le', u'Amts=haupt=stel-le')
583 # Amts==haupt=stel-le
585 # Kein Überschreiben doppelter Marker:
586 # >>> print uebertrage(u'ver<aus<ga-be', u'ver<<aus<ga-be')
587 # ver<<aus<ga-be
589 # >>> print uebertrage(u'Amts=haupt=stel-le', u'Amts==haupt=stel·le')
590 # Amts==haupt=stel-le
592 # Erhalt des Markers für ungünstige Stellen:
593 # >>> print uebertrage(u'An·al.pha·bet', u'An<al.pha-bet')
594 # An<al.pha-bet
596 # Keine Übertragung, wenn die Zahl oder Position der Trennstellen
597 # unterschiedlich ist oder bei unterschiedlichen Wörtern:
599 # >>> try:
600 # ... uebertrage(u'Ha-upt=stel-le', u'Haupt=stel·le')
601 # ... uebertrage(u'Haupt=ste-lle', u'Haupt=stel·le')
602 # ... uebertrage(u'Waupt=stel-le', u'Haupt=stel·le')
603 # ... except TransferError:
604 # ... pass
606 # Übertragung auch bei unterschiedlicher Schreibung oder Position der
607 # Trennstellen mit `strict=False` (für Abgleich zwischen Sprachvarianten):
609 # >>> uebertrage(u'er-ster', u'ers·ter', strict=False)
610 # u'ers-ter'
611 # >>> uebertrage(u'Fluß=bett', u'Fluss·bett', strict=False)
612 # u'Fluss=bett'
613 # >>> uebertrage(u'ab>bei-ßen', u'ab>beis·sen', strict=False)
614 # u'ab>beis-sen'
615 # >>> print uebertrage(u'Aus<tausch=dien-stes', u'Aus-tausch=diens-tes', False)
616 # Aus<tausch=diens-tes
618 # Auch mit `strict=False` muß die Zahl der Trennstellen übereinstimmen
619 # (Ausnahmen siehe unten):
621 # >>> try:
622 # ... uebertrage(u'Ha-upt=ste-lle', u'Haupt=stel·le', strict=False)
623 # ... except TransferError:
624 # ... pass
626 # Akzeptiere unterschiedliche Anzahl von Trennungen bei st und ck nach
627 # Selbstlaut:
629 # >>> uebertrage(u'acht=ecki-ge', u'acht·e{ck/k·k}i·ge', strict=False)
630 # u'acht=e{ck/k-k}i-ge'
631 # >>> uebertrage(u'As-to-ria', u'Asto·ria', strict=False)
632 # u'Asto-ria'
633 # >>> uebertrage(u'Asto-ria', u'As·to·ria', strict=False)
634 # u'As-to-ria'
635 # >>> uebertrage(u'So-fa=ecke', u'So·fa=e{ck/k-k}e', strict=False)
636 # u'So-fa=e{ck/k-k}e'
638 # Mit ``upgrade=False`` werden nur unspezifische Trennstellen überschrieben:
640 # >>> print uebertrage(u'an=stel-le', u'an<stel·le', upgrade=False)
641 # an<stel-le
643 # >>> print uebertrage(u'Aus<stel-ler', u'Aus-stel-ler', upgrade=False)
644 # Aus-stel-ler
646 # >>> print uebertrage(u'Aus-stel-ler', u'Aus<stel-ler', upgrade=False)
647 # Aus<stel-ler
649 # >>> print uebertrage(u'vor<an<<stel-le', u'vor-an<stel·le', upgrade=False)
650 # vor-an<stel-le
652 # ::
654 selbstlaute = u'aeiouäöüAEIOUÄÖÜ'
656 def uebertrage(wort1, wort2, strict=True, upgrade=True):
658 silben1, trennzeichen1 = zerlege(wort1)
659 silben2, trennzeichen2 = zerlege(wort2)
660 # Prüfe strikte Übereinstimmung:
661 if silben1 != silben2 and strict:
662 if u'<' in trennzeichen1 or u'·' in trennzeichen2:
663 raise TransferError(wort1, wort2)
664 else:
665 return wort2
666 # Prüfe ungefähre Übereinstimmung:
667 if len(trennzeichen1) != len(trennzeichen2):
668 # Selbstlaut + st oder ck?
669 for s in selbstlaute:
670 if (wort2.find(s+u'{ck/k·k}') != -1 or
671 wort2.find(s+u'{ck/k-k}') != -1):
672 wort1 = wort1.replace(s+u'ck', s+u'-ck')
673 silben1, trennzeichen1 = zerlege(wort1)
674 if wort2.find(s+u's·t') != -1:
675 wort1 = wort1.replace(s+u'st', s+u's-t')
676 silben1, trennzeichen1 = zerlege(wort1)
677 elif wort1.find(s+u's-t') != -1:
678 wort1 = wort1.replace(s+u's-t', s+u'st')
679 silben1, trennzeichen1 = zerlege(wort1)
680 # print u'retry:', silben1, trennzeichen1
681 # immer noch ungleiche Zahl an Trennstellen?
682 if len(trennzeichen1) != len(trennzeichen2):
683 raise TransferError(wort1, wort2)
685 # Baue wort3 aus silben2 und spezifischeren Trennzeichen:
686 wort3 = silben2.pop(0)
687 for t1,t2 in zip(trennzeichen1, trennzeichen2):
688 if ((t2 == u'·' and t1 != u'.') # unspezifisch
689 or upgrade and
690 ((t2 in (u'-', u'<') and t1 in (u'<', u'<<', u'<=')) # Praefixe
691 or (t2 in (u'-', u'>') and t1 in (u'>', u'>>', u'=>')) # Suffixe
692 or (t2 in (u'-', u'=') and t1 in (u'=', u'==', u'===')) # W-fugen
695 wort3 += t1
696 elif t2 == u'.' and t1 not in u'·.':
697 wort3 += t1 + t2
698 else:
699 wort3 += t2
700 wort3 += silben2.pop(0)
701 return wort3
704 # Übertrag kategorisierter Trennstellen zwischen den Feldern aller Einträge
705 # in `wortliste`::
707 def sprachabgleich(entry, vorbildentry=None):
709 if len(entry) <= 2:
710 return # allgemeine Schreibung
712 mit_affix = None # < oder >
713 kategorisiert = None # kein ·
714 unkategorisiert = None # mindestens ein ·
715 gewichtet = None # == oder <= oder =>
716 for field in entry[1:]:
717 if field.startswith('-'): # -2-, -3-, ...
718 continue
719 if u'·' in field:
720 unkategorisiert = field
721 elif u'<' in field or u'>' in field:
722 mit_affix = field
723 else:
724 kategorisiert = field
725 if u'==' in field or u'<=' in field or u'=>' in field:
726 gewichtet = field
727 if vorbildentry:
728 for field in vorbildentry[1:]:
729 if field.startswith('-'): # -2-, -3-, ...
730 continue
731 if not mit_affix and u'<' in field or u'>' in field :
732 mit_affix = field
733 elif not kategorisiert and unkategorisiert and u'·' not in field:
734 kategorisiert = field
735 if not gewichtet and u'==' in field or u'<=' in field or u'=>' in field:
736 gewichtet = field
737 # print 've:', mit_affix, kategorisiert, unkategorisiert
738 if mit_affix and (kategorisiert or unkategorisiert or gewichtet):
739 for i in range(1,len(entry)):
740 if entry[i].startswith('-'): # -2-, -3-, ...
741 continue
742 if u'<' not in entry[i] or u'·' in entry[i]:
743 try:
744 entry[i] = uebertrage(mit_affix, entry[i], strict=False)
745 except TransferError, e:
746 print u'Sprachabgleich:', unicode(e)
747 # print mit_affix+u':', unicode(entry)
748 elif kategorisiert and unkategorisiert:
749 for i in range(1,len(entry)):
750 if u'·' in entry[i]:
751 try:
752 entry[i] = uebertrage(kategorisiert, entry[i], strict=False)
753 except TransferError, e:
754 print u'Sprachabgleich:', unicode(e)
755 # print kategorisiert, unicode(entry)
756 elif gewichtet:
757 for i in range(1,len(entry)):
758 if u'=' in entry[i]:
759 try:
760 entry[i] = uebertrage(gewichtet, entry[i], strict=False)
761 except TransferError, e:
762 print u'Sprachabgleich:', unicode(e)
766 # Großschreibung in Kleinschreibung wandeln und umgekehrt
768 # Diese Version funktioniert auch für Wörter mit Trennzeichen (während
769 # str.title() nach jedem Trennzeichen wieder groß anfängt)
771 # >>> from wortliste import toggle_case
772 # >>> toggle_case(u'Ha-se')
773 # u'ha-se'
774 # >>> toggle_case(u'arm')
775 # u'Arm'
776 # >>> toggle_case(u'frei=bier')
777 # u'Frei=bier'
778 # >>> toggle_case(u'L}a-ger')
779 # u'l}a-ger'
781 # Keine Änderung bei Wörtern mit Großbuchstaben im Inneren:
783 # >>> toggle_case(u'USA')
784 # u'USA'
785 # >>> toggle_case(u'iRFD')
786 # u'iRFD'
788 # >>> toggle_case(u'gri[f-f/{ff/ff')
789 # u'Gri[f-f/{ff/ff'
790 # >>> toggle_case(u'Gri[f-f/{ff/ff')
791 # u'gri[f-f/{ff/ff'
793 # ::
795 def toggle_case(wort):
796 try:
797 key = join_word(wort, assert_complete=True)
798 except AssertionError:
799 key = wort[0]
800 if key.istitle():
801 return wort.lower()
802 elif key.islower():
803 return wort[0].upper() + wort[1:]
804 else:
805 return wort
807 # Sortierschlüssel
808 # ================
810 # Duden-Sortierung für die Wortliste
812 # >>> from wortliste import sortkey_duden
813 # >>> sortkey_duden([u"Abflußröhren"])
814 # u'abflussrohren a*bflu*szroehren'
815 # >>> sortkey_duden([u"Abflußrohren"])
816 # u'abflussrohren a*bflu*szro*hren'
817 # >>> sortkey_duden([u"Abflussrohren"])
818 # u'abflussrohren'
820 # >>> s = sorted([[u"Abflußröhren"], [u"Abflußrohren"], [u"Abflussrohren"]],
821 # ... key=sortkey_duden)
822 # >>> print ', '.join(e[0] for e in s)
823 # Abflussrohren, Abflußrohren, Abflußröhren
825 # Umschreibung
827 # Ligaturen auflösen und andere "normalisierunde" Ersetzungen für den
828 # (Haupt-)Sortierschlüssel (Akzente werden über ``unicodedata.normalize``
829 # entfernt)::
831 umschrift_skey = {
832 ord(u'æ'): u'ae',
833 ord(u'œ'): u'oe',
834 ord(u'ſ'): u's',
837 # "Zweitschlüssel" zur Unterscheidung von Umlauten/SZ und Basisbuchstaben::
839 umschrift_subkey = {
840 ord(u'a'): u'a*',
841 ord(u'å'): u'aa',
842 ord(u'ä'): u'ae',
843 ord(u'o'): u'o*',
844 ord(u'ö'): u'oe',
845 ord(u'ø'): u'oe',
846 ord(u'u'): u'u*',
847 ord(u'ü'): u'ue',
848 ord(u'ß'): u'sz',
852 # sortkey_duden
853 # -------------
855 # Sortiere nach erstem Feld, alphabetisch gemäß Duden-Regeln::
857 def sortkey_duden(entry):
859 # Sortieren nach erstem Feld (ungetrenntes Wort)::
861 key = entry[0]
863 if len(entry) == 1: # ein Muster pro Zeile, siehe z.B. pre-1901
864 key = join_word(key)
866 # Großschreibung ignorieren:
868 # Der Duden sortiert Wörter, die sich nur in der Großschreibung unterscheiden
869 # "klein vor groß" (ASCII sortiert "groß vor klein"). In der
870 # `Trennmuster-Wortliste` kommen Wörter nur mit der häufiger anzutreffenden
871 # Großschreibung vor, denn der TeX-Trennalgorithmus ignoriert Großschreibung.
872 # ::
874 key = key.lower()
876 # Ersetzungen:
878 # ß -> ss ::
880 skey = key.replace(u'ß', u'ss')
882 # Restliche Akzente weglassen: Wandeln in Darstellung von Buchstaben mit
883 # Akzent als "Grundzeichen + kombinierender Akzent". Anschließend alle
884 # nicht-ASCII-Zeichen ignorieren::
886 skey = skey.translate(umschrift_skey)
887 skey = unicodedata.normalize('NFKD', skey)
888 skey = unicode(skey.encode('ascii', 'ignore'))
890 # "Zweitschlüssel" für das eindeutige Einsortieren von Wörtern mit
891 # gleichem Schlüssel (Masse/Maße, waren/wären, ...):
893 # * "*" nach aou für die Unterscheidung Grund-/Umlaut
894 # * ß->sz
896 # ::
898 if key != skey:
899 subkey = key.translate(umschrift_subkey)
900 skey = u'%s %s' % (skey,subkey)
902 # Gib den Sortierschlüssel zurück::
904 return skey
908 # udiff
909 # ------------
911 # Vergleiche zwei Sequenzen von `WordEntries`, gib einen "unified diff" als
912 # Byte-String zurück (weil difflib nicht mit Unicode-Strings arbeiten kann).
914 # Beispiel:
916 # >>> from wortliste import udiff
917 # >>> print udiff([abbeissen, aalbestand], [abbeissen], 'alt', 'neu')
918 # --- alt
919 # +++ neu
920 # @@ -1,2 +1 @@
921 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
922 # -Aalbestand;Aal=be<stand # Test
924 # ::
926 def udiff(a, b, fromfile='', tofile='',
927 fromfiledate='', tofiledate='', n=1, encoding='utf8'):
929 a = [unicode(entry).encode(encoding) for entry in a]
930 b = [unicode(entry).encode(encoding) for entry in b]
932 diff = difflib.unified_diff(a, b, fromfile, tofile,
933 fromfiledate, tofiledate, n, lineterm='')
935 if diff:
936 return '\n'.join(diff)
937 else:
938 return None
941 def test_keys(wortliste):
942 """Teste Übereinstimmung des ungetrennten Wortes in Feld 1
943 mit den Trennmustern nach Entfernen der Trennmarker.
944 Schreibe Inkonsistenzen auf die Standardausgabe.
946 `wortliste` ist ein Iterator über die Einträge (Klasse `WordEntry`)
948 is_OK = True
949 for entry in wortliste:
950 # Test der Übereinstimmung ungetrenntes/getrenntes Wort
951 # für alle Felder:
952 key = entry[0]
953 for wort in entry[1:]:
954 if wort.startswith(u'-'): # leere Felder
955 continue
956 if key != join_word(wort):
957 is_OK = False
958 print u"\nkey '%s' != '%s'" % (key, wort),
959 if key.lower() == join_word(wort).lower():
960 print(u" Abgleich der Großschreibung mit"
961 u"`prepare-patch.py grossabgleich`."),
962 return is_OK
965 # Test
966 # ====
968 # ::
970 if __name__ == '__main__':
971 import sys
973 # sys.stdout mit UTF8 encoding (wie in Python 3)
974 sys.stdout = codecs.getwriter('UTF-8')(sys.stdout)
976 print u"Test der Werkzeuge und inneren Konsistenz der Wortliste\n"
978 wordfile = WordFile('../../../wortliste')
979 # print 'Dateiobjekt:', wordfile
981 # Liste der Datenfelder (die Klasseninstanz als Argument für `list` liefert
982 # den Iterator über die Felder, `list` macht daraus eine Liste)::
984 wortliste = list(wordfile)
985 print len(wortliste), u"Einträge\n"
987 # Sprachauswahl::
989 # Sprachtags:
991 # sprache = 'de-1901' # traditionell
992 # sprache = 'de-1996' # Reformschreibung
993 # sprache = 'de-x-GROSS' # ohne ß (Schweiz oder GROSS) allgemein
994 # sprache = 'de-1901-x-GROSS' # ohne ß (Schweiz oder GROSS) "traditionell"
995 # sprache = 'de-1996-x-GROSS' # ohne ß (Schweiz oder GROSS) "reformiert"
996 # sprache = 'de-CH-1901' # ohne ß (Schweiz) "traditionell" ("süssauer")
998 # worte = [entry.get(sprache) for entry in wortliste if wort is not None]
999 # print len(worte), u"Einträge für Sprachvariante", sprache
1002 # Test keys::
1004 print u"Teste Schlüssel-Trennmuster-Übereinstimmung:",
1005 if test_keys(wortliste):
1006 print u"OK",
1007 print
1009 # Doppeleinträge::
1011 doppelte = 0
1012 words = set()
1013 for entry in wortliste:
1014 key = entry[0].lower()
1015 if key in words:
1016 doppelte += 1
1017 print unicode(entry)
1018 words.add(key)
1019 print doppelte,
1020 print u"Doppeleinträge (ohne Berücksichtigung der Großschreibung)."
1021 if doppelte:
1022 print u" Entfernen mit `prepare-patch.py doppelte`."
1023 print u" Patch vor Anwendung durchsehen!"
1026 # Ein Wörterbuch (dict Instanz)::
1028 # wordfile.seek(0) # Pointer zurücksetzen
1029 # words = wordfile.asdict()
1031 # print len(words), u"Wörterbucheinträge"
1033 # Zeilenrekonstruktion::
1035 # am Beispiel der Scheiterbeige:
1036 # original = u'beige;beige # vgl. Scheiter-bei-ge'
1037 # entry = words[u"beige"]
1038 # line = unicode(entry)
1039 # assert original == line, "Rejoined %s != %s" % (line, original)
1041 # komplett:
1042 wordfile.seek(0) # Pointer zurücksetzen
1043 OK = 0
1044 line = wordfile.readline().rstrip().decode(wordfile.encoding)
1045 while line:
1046 entry = WordEntry(line)
1047 if line == unicode(entry):
1048 OK +=1
1049 else:
1050 print u'-', line,
1051 print u'+', unicode(entry)
1052 line = wordfile.readline().rstrip().decode(wordfile.encoding)
1054 print OK, u"Einträge rekonstruiert"
1058 # Quellen
1059 # =======
1061 # .. [BCP47] A. Phillips und M. Davis, (Editoren.),
1062 # `Tags for Identifying Languages`, http://www.rfc-editor.org/rfc/bcp/bcp47.txt
1064 # .. _Wortliste der deutschsprachigen Trennmustermannschaft:
1065 # http://mirrors.ctan.org/language/hyphenation/dehyph-exptl/projektbeschreibung.pdf