Python-Skript Update und Fix.
[wortliste.git] / skripte / python / werkzeug.py
blobc4d627f12cb743ea5790046643c050faf39aa6f8
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 # werkzeug.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 werkzeug 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 # >>> abbeissen = WordEntry(
207 # ... u'abbeissen;-2-;-3-;-4-;-5-;ab<bei-ssen;ab<beis-sen;ab<beis-sen')
208 # >>> print abbeissen.lang_index('de')
209 # None
210 # >>> print abbeissen.lang_index('de-x-GROSS')
211 # None
212 # >>> abbeissen.lang_index('de-CH-1901')
215 # ::
217 def lang_index(self, sprachvariante):
219 assert sprachvariante in self.sprachvarianten, \
220 'Sprachvariante nicht in ' + str(self.sprachvarianten.keys())
222 # Einfacher Fall: eine allgemeine Schreibweise::
224 if len(self) == 2:
225 return 1
227 # Spezielle Schreibung::
229 try:
230 i = self.sprachvarianten[sprachvariante]
231 feld = self[i]
232 except IndexError:
233 if i > 4 and len(self) == 5:
234 return 4 # Allgemeine Schweiz/GROSS Schreibung:
235 return None # Feld nicht vorhanden
237 if feld.startswith('-'): # '-1-', '-2-', ...
238 return None # leeres Feld
240 return i
242 # Trennmuster für Sprachvariante ausgeben
244 # >>> aalbestand.get('de')
245 # u'Aal=be<stand'
246 # >>> aalbestand.get('de-1901')
247 # u'Aal=be<stand'
248 # >>> aalbestand.get('de-1996')
249 # u'Aal=be<stand'
250 # >>> aalbestand.get('de-x-GROSS')
251 # u'Aal=be<stand'
252 # >>> aalbestand.get('de-1901-x-GROSS')
253 # u'Aal=be<stand'
254 # >>> aalbestand.get('de-1996-x-GROSS')
255 # u'Aal=be<stand'
256 # >>> aalbestand.get('de-CH-1901')
257 # u'Aal=be<stand'
259 # >>> print abbeissen.get('de')
260 # None
261 # >>> print abbeissen.get('de-x-GROSS')
262 # None
263 # >>> abbeissen.get('de-1901-x-GROSS')
264 # u'ab<bei-ssen'
265 # >>> abbeissen.get('de-CH-1901')
266 # u'ab<beis-sen'
268 # ::
270 def get(self, sprachvariante):
271 try:
272 return self[self.lang_index(sprachvariante)]
273 except TypeError: # Muster in `sprachvariante` nicht vorhanden
274 return None
276 # Trennmuster für Sprachvariante setzen
278 # >>> abbeissen.set('test', 'de-1901-x-GROSS')
279 # >>> print abbeissen
280 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
282 # >>> abbeissen.set('test', 'de-1901')
283 # Traceback (most recent call last):
284 # ...
285 # IndexError: kann kein leeres Feld setzen
287 # ::
289 def set(self, wort, sprachvariante):
290 i = self.lang_index(sprachvariante)
291 if i is None:
292 raise IndexError, "kann kein leeres Feld setzen"
293 self[i] = wort
295 # Felder für alle Sprachvarianten ausfüllen
297 # >>> print str(aalbestand), len(aalbestand)
298 # Aalbestand;Aal=be<stand # Test 2
299 # >>> aalbestand.expand_fields()
300 # >>> print len(aalbestand)
302 # >>> auffrass = WordEntry('auffrass;-2-;-3-;-4-;auf-frass')
303 # >>> auffrass.expand_fields()
304 # >>> print auffrass
305 # auffrass;-2-;-3-;-4-;auf-frass;auf-frass;auf-frass;auf-frass
307 # ::
309 def expand_fields(self):
310 fields = [self.get(sv) or '-%d-' % (self.sprachvarianten[sv] + 1)
311 for sv in sorted(self.sprachvarianten.keys(),
312 key=self.sprachvarianten.get)]
313 # return fields
314 for i, field in enumerate(fields):
315 try:
316 self[i+1] = field # Feld 1 ist "key" (ungetrennt)
317 except IndexError:
318 self.append(field)
321 # Felder für Sprachvarianten zusammenfassen
323 # >>> aalbestand.conflate_fields()
324 # >>> print aalbestand
325 # Aalbestand;Aal=be<stand # Test
326 # >>> auffrass.conflate_fields()
327 # >>> print auffrass
328 # auffrass;-2-;-3-;-4-;auf-frass
329 # >>> entry = WordEntry(u'distanziert;-2-;di-stan-ziert;di-stan-ziert')
330 # >>> entry.conflate_fields()
331 # >>> print entry
332 # distanziert;di-stan-ziert
333 # Aber nicht, wenn die Trennstellen sich unterscheiden:
335 # >>> abenddienste = WordEntry(
336 # ... u'Abenddienste;-2-;Abend=dien-ste;Abend=diens-te')
337 # >>> abenddienste.conflate_fields()
338 # >>> print abenddienste
339 # Abenddienste;-2-;Abend=dien-ste;Abend=diens-te
341 # ::
343 def conflate_fields(self):
344 if len(self) == 8:
345 if self[7] == self[6] == self[5]:
346 self[4] = self[5] # umschreiben auf GROSS-allgemein
347 self.pop()
348 self.pop()
349 self.pop()
350 if len(self) == 5:
351 if self[4] == self[2]: # de-x-GROSS == de-1901
352 self.pop()
353 else:
354 return
355 if len(self) >= 4:
356 if self[3] == self[2]: # de-1996 == de-1901
357 self[1] = self[2] # Umschreiben auf de (allgemein)
358 self.pop()
359 self.pop()
362 # Prüfe auf Vorkommen von Regeländerungen der Orthographiereform 1996.
364 # >>> entry = WordEntry(u'Würste;Wür-ste')
365 # >>> entry.regelaenderungen()
366 # >>> print unicode(entry)
367 # Würste;-2-;Wür-ste;Würs-te
368 # >>> entry = WordEntry(u'Würste;Würs-te')
369 # >>> entry.regelaenderungen()
370 # >>> print unicode(entry)
371 # Würste;-2-;Wür-ste;Würs-te
372 # >>> entry = WordEntry(u'Hecke;He-cke')
373 # >>> entry.regelaenderungen()
374 # >>> print unicode(entry)
375 # Hecke;-2-;He{ck/k-k}e;He-cke
376 # >>> entry = WordEntry(u'Ligusterhecke;Ligu-ster=he{ck/k-k}e')
377 # >>> entry.regelaenderungen()
378 # >>> print unicode(entry)
379 # Ligusterhecke;-2-;Ligu-ster=he{ck/k-k}e;Ligus-ter=he-cke
380 # >>> entry = WordEntry(u'Hass;Hass')
381 # >>> entry.regelaenderungen()
382 # >>> print unicode(entry)
383 # Hass;-2-;-3-;Hass;Hass
384 # >>> entry = WordEntry(u'fasst;fasst')
385 # >>> entry.regelaenderungen()
386 # >>> print unicode(entry)
387 # fasst;-2-;-3-;fasst;fasst
389 # ::
391 def regelaenderungen(self):
392 # Regeländerungen:
393 r1901 = (u'-st', u'{ck/k-k}')
394 r1996 = (u's-t', u'-ck')
395 # kein Schluss-ss und sst in de-1901
396 # aber: 'ßt' und Schluß-ß auch in de-1996 möglich (langer Vokal)
398 w1901 = self.get('de-1901')
399 w1996 = self.get('de-1996')
401 if w1901 is None or w1996 is None:
402 return
404 for r1, r2 in zip(r1901, r1996):
405 w1901 = w1901.replace(r2,r1)
406 w1996 = w1996.replace(r1,r2)
407 if u'sst' in w1901 or w1901.endswith(u'ss'):
408 w1901 = None
410 if w1901 == w1996: # keine Regeländerung im Wort
411 if len(self) > 2:
412 self.conflate_fields()
413 return
414 elif w1901 is None:
415 self.extend( ['']*(5-len(self)) )
416 self[1] = u'-2-'
417 self[2] = u'-3-'
418 self[3] = w1996
419 self[4] = w1996
420 else:
421 self.extend( ['']*(4-len(self)) )
422 self[1] = u'-2-'
423 self[2] = w1901
424 self[3] = w1996
428 # Funktionen
429 # ==========
431 # join_word
432 # ---------
434 # Trennzeichen entfernen::
436 def join_word(word, assert_complete=False):
438 # Einfache Trennzeichen:
440 # == ================================================================
441 # \· ungewichtete Trennstelle (solche, wo sich noch niemand um die
442 # Gewichtung gekümmert hat)
443 # \. unerwünschte Trennstelle (sinnentstellend), z.B. Ur·in.stinkt
444 # oder ungünstige Trennstelle (verwirrend), z.B. Atom·en.er·gie
445 # in ungewichteten Wörtern
446 # \= Trennstelle an Wortfugen (Wort=fu-ge)
447 # \< Trennstelle nach Präfix (Vor<sil-be)
448 # \> Trennstelle vor Suffix (Freund>schaf-ten)
449 # \- Nebentrennstelle (ge-hen)
450 # == ================================================================
452 # ::
454 table = {}
455 for char in u'·.=|-_<>':
456 table[ord(char)] = None
457 key = word.translate(table)
459 # Spezielle Trennungen für die traditionelle Rechtschreibung
460 # (siehe ../../dokumente/README.wortliste)::
462 if '{' in key or '}' in key:
463 key = key.replace(u'{ck/kk}', u'ck')
464 key = key.replace(u'{ck/k', u'k')
465 key = key.replace(u'k}', u'k')
466 # Konsonanthäufungen an Wortfuge: '{xx/xxx}' -> 'xx':
467 key = re.sub(ur'\{(.)\1/\1\1\1\}', ur'\1\1', key)
468 # schon getrennt: ('{xx/xx' -> 'xx' und 'x}' -> 'x'):
469 key = re.sub(ur'\{(.)\1/\1\1$', ur'\1\1', key)
470 key = re.sub(ur'^(.)\}', ur'\1', key)
472 # Trennstellen in doppeldeutigen Wörtern::
474 if '[' in key or ']' in key:
475 key = re.sub(ur'\[(.*)/\1\]', ur'\1', key)
476 # schon getrennt:
477 key = re.sub(ur'\[([^/\[]+)$', ur'\1', key)
478 key = re.sub(ur'^([^/\]]+)\]', ur'\1', key)
480 # Test auf verbliebene komplexe Trennstellen::
482 if assert_complete:
483 for spez in u'[{/}]':
484 if spez in key:
485 raise AssertionError('Spezialtrennung %s, %s' %
486 (word.encode('utf8'), key.encode('utf8')))
488 return key
490 # zerlege
491 # -------
493 # Zerlege ein Wort mit Trennzeichen in eine Liste von Silben und eine Liste
494 # von Trennzeichen)
496 # >>> from werkzeug import zerlege
498 # >>> zerlege(u'Haupt=stel-le')
499 # ([u'Haupt', u'stel', u'le'], [u'=', u'-'])
500 # >>> zerlege(u'Ge<samt=be<triebs=rats==chef')
501 # ([u'Ge', u'samt', u'be', u'triebs', u'rats', u'chef'], [u'<', u'=', u'<', u'=', u'=='])
502 # >>> zerlege(u'an<stands>los')
503 # ([u'an', u'stands', u'los'], [u'<', u'>'])
504 # >>> zerlege(u'An<al.pha-bet')
505 # ([u'An', u'al', u'pha', u'bet'], [u'<', u'.', u'-'])
507 # ::
509 def zerlege(wort):
510 silben = re.split(u'[-·._<>=]+', wort)
511 trennzeichen = re.split(u'[^-·._|<>=]+', wort)
512 return silben, [tz for tz in trennzeichen if tz]
514 # TransferError
515 # -------------
517 # Fehler beim Übertragen von Trennstellen mit uebertrage_::
519 class TransferError(ValueError):
520 def __init__(self, wort1, wort2):
521 msg = u'Inkompatibel: %s %s' % (wort1, wort2)
522 ValueError.__init__(self, msg.encode('utf8'))
524 def __unicode__(self):
525 return str(self).decode('utf8')
528 # uebertrage
529 # ----------
531 # Übertrage die Trennzeichen von `wort1` auf `wort2`:
533 # >>> from werkzeug import uebertrage, TransferError
535 # >>> uebertrage(u'Haupt=stel-le', u'Haupt·stel·le')
536 # u'Haupt=stel-le'
538 # Auch teilweise Übertragung, von "kategorisiert" nach "unkategorisiert":
540 # >>> print uebertrage(u'Haupt=stel-le', u'Haupt=stel·le')
541 # Haupt=stel-le
543 # >>> print uebertrage(u'Haupt·stel-le', u'Haupt=stel·le')
544 # Haupt=stel-le
546 # >>> print uebertrage(u'Aus<stel-ler', u'Aus-stel-ler')
547 # Aus<stel-ler
549 # >>> print uebertrage(u'Freund>schaf·ten', u'Freund-schaf-ten')
550 # Freund>schaf-ten
552 # Übertragung doppelter Marker:
554 # >>> print uebertrage(u'ver<<aus<ga-be', u'ver<aus<ga-be')
555 # ver<<aus<ga-be
557 # >>> print uebertrage(u'freund>lich>>keit', u'freund>lich>keit')
558 # freund>lich>>keit
560 # >>> print uebertrage(u'Amts==haupt=stel-le', u'Amts=haupt=stel-le')
561 # Amts==haupt=stel-le
563 # Kein Überschreiben doppelter Marker:
564 # >>> print uebertrage(u'ver<aus<ga-be', u'ver<<aus<ga-be')
565 # ver<<aus<ga-be
567 # >>> print uebertrage(u'Amts=haupt=stel-le', u'Amts==haupt=stel·le')
568 # Amts==haupt=stel-le
570 # Erhalt des Markers für ungünstige Stellen:
571 # >>> print uebertrage(u'An·al.pha·bet', u'An<al.pha-bet')
572 # An<al.pha-bet
574 # Keine Übertragung, wenn die Zahl oder Position der Trennstellen
575 # unterschiedlich ist oder bei unterschiedlichen Wörtern:
577 # >>> try:
578 # ... uebertrage(u'Ha-upt=stel-le', u'Haupt=stel·le')
579 # ... uebertrage(u'Haupt=ste-lle', u'Haupt=stel·le')
580 # ... uebertrage(u'Waupt=stel-le', u'Haupt=stel·le')
581 # ... except TransferError:
582 # ... pass
584 # Übertragung auch bei unterschiedlicher Schreibung oder Position der
585 # Trennstellen mit `strict=False` (für Abgleich zwischen Sprachvarianten):
587 # >>> uebertrage(u'er-ster', u'ers·ter', strict=False)
588 # u'ers-ter'
589 # >>> uebertrage(u'Fluß=bett', u'Fluss·bett', strict=False)
590 # u'Fluss=bett'
591 # >>> uebertrage(u'ab>bei-ßen', u'ab>beis·sen', strict=False)
592 # u'ab>beis-sen'
593 # >>> print uebertrage(u'Aus<tausch=dien-stes', u'Aus-tausch=diens-tes', False)
594 # Aus<tausch=diens-tes
596 # Auch mit `strict=False` muß die Zahl der Trennstellen übereinstimmen
597 # (Ausnahmen siehe unten):
599 # >>> try:
600 # ... uebertrage(u'Ha-upt=ste-lle', u'Haupt=stel·le', strict=False)
601 # ... except TransferError:
602 # ... pass
604 # Akzeptiere unterschiedliche Anzahl von Trennungen bei st und ck nach
605 # Selbstlaut:
607 # >>> uebertrage(u'acht=ecki-ge', u'acht·e{ck/k·k}i·ge', strict=False)
608 # u'acht=e{ck/k-k}i-ge'
609 # >>> uebertrage(u'As-to-ria', u'Asto·ria', strict=False)
610 # u'Asto-ria'
611 # >>> uebertrage(u'Asto-ria', u'As·to·ria', strict=False)
612 # u'As-to-ria'
613 # >>> uebertrage(u'So-fa=ecke', u'So·fa=e{ck/k-k}e', strict=False)
614 # u'So-fa=e{ck/k-k}e'
616 # Mit ``upgrade=False`` werden nur unspezifische Trennstellen überschrieben:
618 # >>> print uebertrage(u'an=stel-le', u'an<stel·le', upgrade=False)
619 # an<stel-le
621 # >>> print uebertrage(u'Aus<stel-ler', u'Aus-stel-ler', upgrade=False)
622 # Aus-stel-ler
624 # >>> print uebertrage(u'Aus-stel-ler', u'Aus<stel-ler', upgrade=False)
625 # Aus<stel-ler
627 # >>> print uebertrage(u'vor<an<<stel-le', u'vor-an<stel·le', upgrade=False)
628 # vor-an<stel-le
630 # ::
632 selbstlaute = u'aeiouäöüAEIOUÄÖÜ'
634 def uebertrage(wort1, wort2, strict=True, upgrade=True):
636 silben1, trennzeichen1 = zerlege(wort1)
637 silben2, trennzeichen2 = zerlege(wort2)
638 # Prüfe strikte Übereinstimmung:
639 if silben1 != silben2 and strict:
640 if u'<' in trennzeichen1 or u'·' in trennzeichen2:
641 raise TransferError(wort1, wort2)
642 else:
643 return wort2
644 # Prüfe ungefähre Übereinstimmung:
645 if len(trennzeichen1) != len(trennzeichen2):
646 # Selbstlaut + st oder ck?
647 for s in selbstlaute:
648 if (wort2.find(s+u'{ck/k·k}') != -1 or
649 wort2.find(s+u'{ck/k-k}') != -1):
650 wort1 = wort1.replace(s+u'ck', s+u'-ck')
651 silben1, trennzeichen1 = zerlege(wort1)
652 if wort2.find(s+u's·t') != -1:
653 wort1 = wort1.replace(s+u'st', s+u's-t')
654 silben1, trennzeichen1 = zerlege(wort1)
655 elif wort1.find(s+u's-t') != -1:
656 wort1 = wort1.replace(s+u's-t', s+u'st')
657 silben1, trennzeichen1 = zerlege(wort1)
658 # print u'retry:', silben1, trennzeichen1
659 # immer noch ungleiche Zahl an Trennstellen?
660 if len(trennzeichen1) != len(trennzeichen2):
661 raise TransferError(wort1, wort2)
663 # Baue wort3 aus silben2 und spezifischeren Trennzeichen:
664 wort3 = silben2.pop(0)
665 for t1,t2 in zip(trennzeichen1, trennzeichen2):
666 if ((t2 == u'·' and t1 != u'.') # unspezifisch
667 or upgrade and
668 ((t2 in (u'-', u'<') and t1 in (u'<', u'<<', u'<=')) # Praefixe
669 or (t2 in (u'-', u'>') and t1 in (u'>', u'>>', u'=>')) # Suffixe
670 or (t2 in (u'-', u'=') and t1 in (u'=', u'==', u'===')) # W-fugen
673 wort3 += t1
674 elif t2 == u'.' and t1 not in u'·.':
675 wort3 += t1 + t2
676 else:
677 wort3 += t2
678 wort3 += silben2.pop(0)
679 return wort3
682 # Übertrag kategorisierter Trennstellen zwischen den Feldern aller Einträge
683 # in `wortliste`::
685 def sprachabgleich(entry, vorbildentry=None):
687 if len(entry) <= 2:
688 return # allgemeine Schreibung
690 mit_affix = None # < oder >
691 kategorisiert = None # kein ·
692 unkategorisiert = None # mindestens ein ·
693 gewichtet = None # == oder <= oder =>
694 for field in entry[1:]:
695 if field.startswith('-'): # -2-, -3-, ...
696 continue
697 if u'·' in field:
698 unkategorisiert = field
699 elif u'<' in field or u'>' in field:
700 mit_affix = field
701 else:
702 kategorisiert = field
703 if u'==' in field or u'<=' in field or u'=>' in field:
704 gewichtet = field
705 if vorbildentry:
706 for field in vorbildentry[1:]:
707 if field.startswith('-'): # -2-, -3-, ...
708 continue
709 if not mit_affix and u'<' in field or u'>' in field :
710 mit_affix = field
711 elif not kategorisiert and unkategorisiert and u'·' not in field:
712 kategorisiert = field
713 if not gewichtet and u'==' in field or u'<=' in field or u'=>' in field:
714 gewichtet = field
715 # print 've:', mit_affix, kategorisiert, unkategorisiert
716 if mit_affix and (kategorisiert or unkategorisiert or gewichtet):
717 for i in range(1,len(entry)):
718 if entry[i].startswith('-'): # -2-, -3-, ...
719 continue
720 if u'<' not in entry[i] or u'·' in entry[i]:
721 try:
722 entry[i] = uebertrage(mit_affix, entry[i], strict=False)
723 except TransferError, e:
724 print u'Sprachabgleich:', unicode(e)
725 print mit_affix+u':', unicode(entry)
726 elif kategorisiert and unkategorisiert:
727 for i in range(1,len(entry)):
728 if u'·' in entry[i]:
729 try:
730 entry[i] = uebertrage(kategorisiert, entry[i], strict=False)
731 except TransferError, e:
732 print u'Sprachabgleich:', unicode(e)
733 # print kategorisiert, unicode(entry)
734 elif gewichtet:
735 for i in range(1,len(entry)):
736 if u'=' in entry[i]:
737 try:
738 entry[i] = uebertrage(gewichtet, entry[i], strict=False)
739 except TransferError, e:
740 print u'Sprachabgleich:', unicode(e)
744 # Großschreibung in Kleinschreibung wandeln und umgekehrt
746 # Diese Version funktioniert auch für Wörter mit Trennzeichen (während
747 # str.title() nach jedem Trennzeichen wieder groß anfängt)
749 # >>> from werkzeug import toggle_case
750 # >>> toggle_case(u'Ha-se')
751 # u'ha-se'
752 # >>> toggle_case(u'arm')
753 # u'Arm'
754 # >>> toggle_case(u'frei=bier')
755 # u'Frei=bier'
756 # >>> toggle_case(u'L}a-ger')
757 # u'l}a-ger'
759 # Keine Änderung bei Wörtern mit Großbuchstaben im Inneren:
761 # >>> toggle_case(u'USA')
762 # u'USA'
763 # >>> toggle_case(u'iRFD')
764 # u'iRFD'
766 # >>> toggle_case(u'gri[f-f/{ff/ff')
767 # u'Gri[f-f/{ff/ff'
768 # >>> toggle_case(u'Gri[f-f/{ff/ff')
769 # u'gri[f-f/{ff/ff'
771 # ::
773 def toggle_case(wort):
774 try:
775 key = join_word(wort, assert_complete=True)
776 except AssertionError:
777 key = wort[0]
778 if key.istitle():
779 return wort.lower()
780 elif key.islower():
781 return wort[0].upper() + wort[1:]
782 else:
783 return wort
785 # Sortierschlüssel
786 # ================
788 # Duden-Sortierung für die Wortliste
790 # >>> from werkzeug import sortkey_duden
791 # >>> sortkey_duden([u"Abflußröhren"])
792 # u'abflussrohren a*bflu*szroehren'
793 # >>> sortkey_duden([u"Abflußrohren"])
794 # u'abflussrohren a*bflu*szro*hren'
795 # >>> sortkey_duden([u"Abflussrohren"])
796 # u'abflussrohren'
798 # >>> s = sorted([[u"Abflußröhren"], [u"Abflußrohren"], [u"Abflussrohren"]],
799 # ... key=sortkey_duden)
800 # >>> print ', '.join(e[0] for e in s)
801 # Abflussrohren, Abflußrohren, Abflußröhren
803 # Umschreibung
805 # Ligaturen auflösen und andere "normalisierunde" Ersetzungen für den
806 # (Haupt-)Sortierschlüssel (Akzente werden über ``unicodedata.normalize``
807 # entfernt)::
809 umschrift_skey = {
810 ord(u'æ'): u'ae',
811 ord(u'œ'): u'oe',
812 ord(u'ſ'): u's',
815 # "Zweitschlüssel" zur Unterscheidung von Umlauten/SZ und Basisbuchstaben::
817 umschrift_subkey = {
818 ord(u'a'): u'a*',
819 ord(u'å'): u'aa',
820 ord(u'ä'): u'ae',
821 ord(u'o'): u'o*',
822 ord(u'ö'): u'oe',
823 ord(u'ø'): u'oe',
824 ord(u'u'): u'u*',
825 ord(u'ü'): u'ue',
826 ord(u'ß'): u'sz',
830 # sortkey_duden
831 # -------------
833 # Sortiere nach erstem Feld, alphabetisch gemäß Duden-Regeln::
835 def sortkey_duden(entry):
837 # Sortieren nach erstem Feld (ungetrenntes Wort)::
839 key = entry[0]
841 if len(entry) == 1: # ein Muster pro Zeile, siehe z.B. pre-1901
842 key = join_word(key)
844 # Großschreibung ignorieren:
846 # Der Duden sortiert Wörter, die sich nur in der Großschreibung unterscheiden
847 # "klein vor groß" (ASCII sortiert "groß vor klein"). In der
848 # `Trennmuster-Wortliste` kommen Wörter nur mit der häufiger anzutreffenden
849 # Großschreibung vor, denn der TeX-Trennalgorithmus ignoriert Großschreibung.
850 # ::
852 key = key.lower()
854 # Ersetzungen:
856 # ß -> ss ::
858 skey = key.replace(u'ß', u'ss')
860 # Restliche Akzente weglassen: Wandeln in Darstellung von Buchstaben mit
861 # Akzent als "Grundzeichen + kombinierender Akzent". Anschließend alle
862 # nicht-ASCII-Zeichen ignorieren::
864 skey = skey.translate(umschrift_skey)
865 skey = unicodedata.normalize('NFKD', skey)
866 skey = unicode(skey.encode('ascii', 'ignore'))
868 # "Zweitschlüssel" für das eindeutige Einsortieren von Wörtern mit
869 # gleichem Schlüssel (Masse/Maße, waren/wären, ...):
871 # * "*" nach aou für die Unterscheidung Grund-/Umlaut
872 # * ß->sz
874 # ::
876 if key != skey:
877 subkey = key.translate(umschrift_subkey)
878 skey = u'%s %s' % (skey,subkey)
880 # Gib den Sortierschlüssel zurück::
882 return skey
886 # udiff
887 # ------------
889 # Vergleiche zwei Sequenzen von `WordEntries`, gib einen "unified diff" als
890 # Byte-String zurück (weil difflib nicht mit Unicode-Strings arbeiten kann).
892 # Beispiel:
894 # >>> from werkzeug import udiff
895 # >>> print udiff([abbeissen, aalbestand], [abbeissen], 'alt', 'neu')
896 # --- alt
897 # +++ neu
898 # @@ -1,2 +1 @@
899 # abbeissen;-2-;-3-;-4-;-5-;test;ab<beis-sen;ab<beis-sen
900 # -Aalbestand;Aal=be<stand # Test
902 # ::
904 def udiff(a, b, fromfile='', tofile='',
905 fromfiledate='', tofiledate='', n=1, encoding='utf8'):
907 a = [unicode(entry).encode(encoding) for entry in a]
908 b = [unicode(entry).encode(encoding) for entry in b]
910 diff = difflib.unified_diff(a, b, fromfile, tofile,
911 fromfiledate, tofiledate, n, lineterm='')
913 if diff:
914 return '\n'.join(diff)
915 else:
916 return None
919 def test_keys(wortliste):
920 """Teste Übereinstimmung des ungetrennten Wortes in Feld 1
921 mit den Trennmustern nach Entfernen der Trennmarker.
922 Schreibe Inkonsistenzen auf die Standardausgabe.
924 `wortliste` ist ein Iterator über die Einträge (Klasse `WordEntry`)
926 is_OK = True
927 for entry in wortliste:
928 # Test der Übereinstimmung ungetrenntes/getrenntes Wort
929 # für alle Felder:
930 key = entry[0]
931 for wort in entry[1:]:
932 if wort.startswith(u'-'): # leere Felder
933 continue
934 if key != join_word(wort):
935 is_OK = False
936 print u"\nkey '%s' != '%s'" % (key, wort),
937 if key.lower() == join_word(wort).lower():
938 print(u" Abgleich der Großschreibung mit"
939 u"`prepare-patch.py grossabgleich`."),
940 return is_OK
943 # Test
944 # ====
946 # ::
948 if __name__ == '__main__':
949 import sys
951 # sys.stdout mit UTF8 encoding (wie in Python 3)
952 sys.stdout = codecs.getwriter('UTF-8')(sys.stdout)
954 print u"Test der Werkzeuge und inneren Konsistenz der Wortliste\n"
956 # wordfile = WordFile('../../wortliste-binnen-s')
957 wordfile = WordFile('../../wortliste')
958 # print 'Dateiobjekt:', wordfile
960 # Liste der Datenfelder (die Klasseninstanz als Argument für `list` liefert
961 # den Iterator über die Felder, `list` macht daraus eine Liste)::
963 wortliste = list(wordfile)
964 print len(wortliste), u"Einträge\n"
966 # Sprachauswahl::
968 # Sprachtags:
970 # sprache = 'de-1901' # traditionell
971 # sprache = 'de-1996' # Reformschreibung
972 # sprache = 'de-x-GROSS' # ohne ß (Schweiz oder GROSS) allgemein
973 # sprache = 'de-1901-x-GROSS' # ohne ß (Schweiz oder GROSS) "traditionell"
974 # sprache = 'de-1996-x-GROSS' # ohne ß (Schweiz oder GROSS) "reformiert"
975 # sprache = 'de-CH-1901' # ohne ß (Schweiz) "traditionell" ("süssauer")
977 # worte = [entry.get(sprache) for entry in wortliste if wort is not None]
978 # print len(worte), u"Einträge für Sprachvariante", sprache
981 # Test keys::
983 print u"Teste Schlüssel-Trennmuster-Übereinstimmung:",
984 if test_keys(wortliste):
985 print u"OK",
986 print
988 # Doppeleinträge::
990 doppelte = 0
991 words = set()
992 for entry in wortliste:
993 key = entry[0].lower()
994 if key in words:
995 doppelte += 1
996 print unicode(entry)
997 words.add(key)
998 print doppelte,
999 print u"Doppeleinträge (ohne Berücksichtigung der Großschreibung)."
1000 if doppelte:
1001 print u" Entfernen mit `prepare-patch.py doppelte`."
1002 print u" Patch vor Anwendung durchsehen!"
1005 # Ein Wörterbuch (dict Instanz)::
1007 # wordfile.seek(0) # Pointer zurücksetzen
1008 # words = wordfile.asdict()
1010 # print len(words), u"Wörterbucheinträge"
1012 # Zeilenrekonstruktion::
1014 # am Beispiel der Scheiterbeige:
1015 # original = u'beige;beige # vgl. Scheiter-bei-ge'
1016 # entry = words[u"beige"]
1017 # line = unicode(entry)
1018 # assert original == line, "Rejoined %s != %s" % (line, original)
1020 # komplett:
1021 wordfile.seek(0) # Pointer zurücksetzen
1022 OK = 0
1023 line = wordfile.readline().rstrip().decode(wordfile.encoding)
1024 while line:
1025 entry = WordEntry(line)
1026 if line == unicode(entry):
1027 OK +=1
1028 else:
1029 print u'-', line,
1030 print u'+', unicode(entry)
1031 line = wordfile.readline().rstrip().decode(wordfile.encoding)
1033 print OK, u"Einträge rekonstruiert"
1037 # Quellen
1038 # =======
1040 # .. [BCP47] A. Phillips und M. Davis, (Editoren.),
1041 # `Tags for Identifying Languages`, http://www.rfc-editor.org/rfc/bcp/bcp47.txt
1043 # .. _Wortliste der deutschsprachigen Trennmustermannschaft:
1044 # http://mirrors.ctan.org/language/hyphenation/dehyph-exptl/projektbeschreibung.pdf