von WoNáDo
Es ist etwas wunderbares geschehen. Kurz nachdem ich K. Kosako (Oniguruma) das Problem mit dem nicht erlaubten /\G/ in “look-behind”-Konstrukten gemailt habe (siehe Beitrag), stellte er eine neue Oniguruma Version (4.1.2) bereit. Diese habe ich dann in die Ruby 1.9-Quellen kopiert und Ruby 1.9 damit auf Windows erstellt.
Hier ist nun die Aufbereitung der formalen Begrüssung in der von mir gewollten Form.
Um was es ging? - Es soll ein Bankett mit Büffet für alle Rubyistinnen und Rubyisten veranstaltet werden. Der Text steht in der Variablen “inpuff”. Nun soll der aber nicht auf diese Art ausgedruckt werden, sondern im Blocksatz in einer Breite von 40 Zeichen je Zeile.
Das vereinfachte Programm:
inpuff = <<INTEXT Liebe Rubyistinnen und Rubyisten! Anlaesslich der immer wieder, ja taeglich anzutreffenden ausserordentlichen Freude, die Ihr, liebe Rubyistinnen und Rubyisten, bei der Benutzung unserer ausserordentlichen Lieblingssprache Ruby empfinden duerft, haben wir Euch alle hier zum Bankett der Rubyistinnen und Rubyisten geladen. Benutzt die Angebote des Bueffets genau so, wie Ihr Ruby nutzt: Einfach nach Wunsch zusammenstellen und geniessen. INTEXT class String def fillstring(len, lr = true) return ‘‘ unless self.match(/[^ ]/) temp = self.strip.split(‘ ‘) return temp[0] if temp.length == 1 dm = (len - temp.join(‘‘).length).divmod(temp.length - 1) if lr (temp[0…(temp.length - dm[1])].join(‘ ‘ * dm[0]) + ‘ ‘ * (dm[0] + 1) + temp[(temp.length - dm[1])…(temp.length)].join(‘ ‘ * (dm[0] + 1))).strip else (temp[0…dm[1]].join(‘ ‘ * (dm[0] + 1)) + ‘ ‘ * (dm[0] + 1) + temp[dm[1]…temp.length].join(‘ ‘ * dm[0])).strip end end end l_to_r = false outpuff = ‘‘ breite = 40 pat = /(w+[:.,;?!]? )/ normal = /((#{pat}+)(?=.*(?<=G.{#{breite+1}})))/ wideword = /(G(?=w{#{breite}}(w|[:.,;?!]))#{pat})/ final = /(#{pat}+$)/ inpuff.gsub(/s+(?=[:.,;?!])/, ‘‘).gsub(/s+/, ‘ ‘).scan(/#{normal}|#{wideword}|#{final}/) do outpuff << $~[0].fillstring(breite, (l_to_r = !l_to_r)) << “n“ end puts outpuff
Die Formatierung hat sich gegenüber der ersten Version nicht verändert - das wäre ja auch höchst unerwünscht.
Liebe Rubyistinnen und Rubyisten! Anlaesslich der immer wieder, ja taeglich anzutreffenden ausserordentlichen Freude, die Ihr, liebe Rubyistinnen und Rubyisten, bei der Benutzung unserer ausserordentlichen Lieblingssprache Ruby empfinden duerft, haben wir Euch alle hier zum Bankett der Rubyistinnen und Rubyisten geladen. Benutzt die Angebote des Bueffets genau so, wie Ihr Ruby nutzt: Einfach nach Wunsch zusammenstellen und geniessen.
Wie auch schon bei der /\G/ vermeidenden Version bespreche ich hier nur die Zeilen, die mit den regulären Ausdrücken zu tun haben. Der Rest dient dazu die notwendige Anzahl Leerzeichen zwischen die Worte zu setzen. Da bei der Aufbereitung der Zeilen einzelne Leerzeichen durch mehrere ersetzt werden, bis das letzte druckbare Zeichen der Zeile an der gewünschten Position erscheint, werden je Zeile an verschiedenen Positionen eventuell verschieden breite Zwischenräume erzeugt. Damit das Druckbild nicht zu einseitig ist, wird der Leerzeichenausgleich abwechselnd je Zeile von links, dann von rechts aus vorgenommen.
Nun zum Scan-Teil.
breite = 40 pat = /(w+[:.,;?!]? )/ normal = /((#{pat}+)(?=.*(?<=G.{#{breite+1}})))/ wideword = /(G(?=w{#{breite}}(w|[:.,;?!]))#{pat})/ final = /(#{pat}+$)/ inpuff.gsub(/s+(?=[:.,;?!])/, ‘‘).gsub(/s+/, ‘ ‘).scan(/#{normal}|#{wideword}|#{final}/) do outpuff << $~[0].fillstring(breite, (l_to_r = !l_to_r)) << “n“ end
Das Muster in pat erkennt genau wie vorher ein “Wort mit Anhang und Leerzeichen”. Es wird dann im eigentlichen Hauptmuster benutzt.
Die nächsten Zeilen definieren die verschiedenen Musterteile für die Alternativen, die dann durch /#{…}/ im Hauptmuster aufgerufen werden . Ehe ich auf normal, wideword und final komme, will ich auf den Teil vor dem eigentlichen gsub-Hauptteil eingehen.
Zuerst werden im inpuff-String alle Leerzeichen vor Satzzeichen enfernt, darauf anschliessend werden alle Sequenzen von Leerzeichen und Zeilenvorschüben durch jeweils ein Leerzeichen ersetzt.
Da inpuff wegen der Here-Doc-Konstruktion mit einem Zeilenvorschub endet, wird nach dem letzten Wort noch ein Leerzeichen eingefügt. Das ist wichtig, sonst funktioniert die Erkennung mittels pat nicht. Das Ergebnis ist also ein einzeiliger String, dessen Worte (inklusive Satzzeichen) durch ein Leerzeichen getrennt sind und der mit einem Leerzeichen nach dem letzten Wort endet.
Nun zu den drei Alternativmustern in scan.
Das Muster normal, welches das erste unter den Alternativen ist, kümmert sich um die Normalfälle. Schauen wir uns einmal das Muster an.
/((#{pat}+)(?=.*(?<=G.{#{breite+1}})))/
/(#{pat}+)/ versucht als gieriges Muster soviel wie möglich “Worte mit Anhang und Leerzeichen” vom Anfang zu nehmen. - Vom Anfang? - Besser “vom Beginn des noch nicht durch scan bearbeiteten Teils des Strings”.
scan wendet das Muster auf den String an und wiederholt das mit dem noch nicht durch das Muster verbrauchten Teil, solange es noch einen Match gibt.
Klar? - Nun also zum Rest dieses Teilmusters - /(?=.*(?<=\G.{#{breite+1}}))/.
Da kann ich erst einmal die Textersetzung für /#{breite+1}/ durchführen und es etwas kompakter schreiben.
/(?=.*(?<=G.{41}))7
Das ist fast das gleiche Muster wie in der ersten Version, bis auf das /\G/. Dieses Element bedeutet “Beginn des aktuellen Match” (ACHTUNG! - “Ende des letzten Match” ist bei Ruby nicht korrekt, dazu gibts was im sechsten Beitrag). Es hat also die Bedeutung “nach dem vorhergehenden Element muss es nach eventuellem Überlesen von Zeichen möglich sein, 41 beliebige Zeichen ab der Position des Matchbeginns zu finden”.
Etwas klarer ausgedrückt heisst das, dass die durch /(#{pat}+)/ angesammelten Worte mit Leerzeichen nicht länger als 41 Zeichen sein dürfen. Da nach jedem Wort ein Leerzeichen steht, habe ich also die gewünschte Maximallänge von 40 Zeichen. Die genaue Aufbereitung macht dann fillstring, wobei $~[0] der durch scan jeweils erkannte Teil des Strings ist ($~ ist das zugehörige “MatchData”-Objekt).
Wenn ein einziges Wort mit einer Länge von über 40 Zeichen vorkommt, dann soll es als Ganzes komplett genommen werden. Es ist dann zwar zu lang, aber Worttrennungen will ich hier nicht versuchen. Dieser Sonderfall wird durch wideword behandelt.
/(G(?=w{#{breite+1}}[:.,;?!]?)#{pat})/
Das ist recht harmlos, denn wideword schaut einfach vorausschauend nach, ob ab dem aktuellen Matchbeginn 40 Wortzeichen, gefolgt von einem Wort- oder Satzzeichen, kommen. Falls ja, nimmt es das ganze Wort.
Die letzte Alternative final zieht nur, wenn die vorhergehenden erfolglos waren. Das kann nach lage der Dinge nur am Ende des Textes geschehen, wenn nicht mehr genug Worte für 41 Zeichen vorhanden sind. Dann wird durch /(#{pat}+$)/ einfach der Rest genommen (hier ist es wichtig, dass der Text mit genau einem Leerzeichen endet).
Tja, das wars!
Abschliessend möchte ich noch einmal auf eine Besonderheit hinweisen. Dieser Artikel konnte nur deshalb geschrieben werden, weil K. Kosako, der “Macher” von Oniguruma, auf meine Mail bezüglich /\G/ sofort reagierte und dieses Leistungsmerkmal einbaute. Nachdem es mir noch gelang Ruby 1.9 mit der neuen Oniguruma-Version 4.1.2 auf Windows zu erstellen, konnte ich sofort das Beispiel auf die gewünschte Art schreiben.
Hoch lebe die ganze Ruby-Gemeinde (das beinhaltet K. Kosako von Oniguruma)!
Kommentar schreiben
Kommentare
[...] 1: Gruppen, Quantoren und Kino-süchtige Programmierer 2: Atomare Zeitersparnis 3: Zukunftsaussichten und die Jagd nach Feten 4: Ungebetene Gäste, Theatertexte und formale Begrüssungen 4a: Vereinfachte formale Begrüssungen. 5: Benannte Gruppen, Palindrome, … [...]