Laden...

Einfacher word wrap Algorithmus als extension method für System.String

Erstellt von jaensen vor 11 Jahren Letzter Beitrag vor 11 Jahren 5.132 Views
jaensen Themenstarter:in
2.760 Beiträge seit 2006
vor 11 Jahren
Einfacher word wrap Algorithmus als extension method für System.String

Beschreibung:
Ein einfacher word wrap algorithmus der bei Whitespace trennt. Wenn ein Wort zu lang ist für die angegebene Zeilenlänge dann wird es rücksichtslos an der Stelle getrennt ab der es nicht mehr in die Zeile passt.

Das ist die 2. Version des Snippets in dem einige der von herbivore angesprochenen Schwächen behoben sind.


 
        public static string[] WordWrap(this string str, int width, string[] seperators) {

            if (str == null)
                throw new NullReferenceException();
            if (seperators == null)
                throw new ArgumentNullException("seperators", "The seperators array can not be null");
            if (seperators.Length == 0)
                throw new ArgumentException("The seperators array can not be empty", "seperators");
            if (width <= 0)
                throw new ArgumentException("The width must be greater than zero.", "width");

            StringBuilder lineBuilder = new StringBuilder(width);
            List<string> wrappedLines = new List<string>();

            string[] words = str.Split(seperators, StringSplitOptions.RemoveEmptyEntries);

            foreach (string word in words) {

                bool wordWritten = false;

                while (!wordWritten) {

                    if (lineBuilder.Length > 0) {

                        if (lineBuilder.Length + word.Length + 1 <= width) {

                            lineBuilder.Append(" " + word);
                            wordWritten = true;
                        } else {

                            wrappedLines.Add(lineBuilder.ToString());
                            lineBuilder.Length = 0;
                        }
                    } else {

                        if (word.Length <= width) {

                            lineBuilder.Append(word);
                            wordWritten = true;
                        } else {

                            string tooLongWordRemains = word;
                            while (tooLongWordRemains.Length > width) {

                                lineBuilder.Append(tooLongWordRemains.Substring(0, tooLongWordRemains.Length < width ? tooLongWordRemains.Length : width));
                                wrappedLines.Add(lineBuilder.ToString());
                                lineBuilder.Length = 0;

                                int startIdx = tooLongWordRemains.Length < width ? tooLongWordRemains.Length : width;
                                tooLongWordRemains = tooLongWordRemains.Substring(startIdx, tooLongWordRemains.Length - startIdx);
                            }
                            lineBuilder.Append(tooLongWordRemains);
                            wordWritten = true;
                        }
                    }
                }
            }

            if (lineBuilder.Length > 0)
                wrappedLines.Add(lineBuilder.ToString());

            return wrappedLines.ToArray();
        }

        public static string[] WordWrap(this string str, int width) {
            return str.WordWrap(width, new string[] { " ", "\t", "\r", "\n" });
        }

Schlagwörter: word wrap

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo jaensen,

das Snippet ist praktisch, auch wenn es sich nur eignet, wenn der Text später monospaced dargestellt wird, aber das ist ja üblicherweise der Fall, wenn man den Ergebnistext z.B. in eine Textdatei schreibt.

Ich habe [EDIT]hatte in der ersten Version des Snippets[/EDIT] noch kein paar Verbesserungen bzw. kleine Fehler gefunden (ohne besondere Reihenfolge):
*Wenn ein Parameter null ist, sollte eine ArgumentNullException statt einer NullReferenceException geworfen werden. [EDIT]Ich hab ganz übersehen, dass das eine Erweiterungsmethode ist, und daher der erste Parameter kein Argument, sondern das this-Objekt ist. Die NullReferenceException geht also in Ordnung.[/EDIT]

*Wenn witdh 0 oder zumindest kleiner 0 ist, könnte eine ArgumentException geworfen werden.

*Die Bedingung if (lineBuilder.Length + word.Length < width) passt nicht, wenn es sich um das erste Wort einer Zeile handelt, denn das passt ja auch dann noch, wenn dessen Länge gleich width ist.

*Wenn das allererste Wort länger ist als width (momentan: mindestens solang ist wie width) wird davor eine unnötige Leerzeile eingefügt.

*Vielleicht ist es einfacher, die Behandlung des ersten Worts vor die Schleife zu ziehen und die Schleife dann über words.Skip (1) laufen zu lassen. Dann kann man sich auch die firstWord-Konstruktion sparen.

*Wenn die Länge des gesamten Texts kleiner kleiner gleich width ist, wird der Text unverändert in die erste Zeile übernommen, ohne dass das Spacing zwischen den Worten auf immer genau ein Space angepasst wird.

*(Folgen von) Zeilenvorschübe(n) und Tabs werden wie Worte behandelt, was zu komischen Ergebnissen führt. String.Split sollte besser alle Whitespaces als Trennzeichen übergeben bekommen. Außerdem ist es vermutlich keine Absicht, dass StringSplitOptions.RemoveEmptyEntries nicht angegeben ist. Das kann ebenfalls zu komischen Ergebnissen führen, z.B. zu Leerzeichen am Anfang einer Zeile.

*Bei einem leeren Text bzw. einem Text, der nur aus Whitespaces besteht, könnte man darüber streiten, ob das Ergebnis-Array nicht besser leer sein müsste. Momentan enthält es immer mindestens eine Zeile.

*Es könnte von Vorteil sein, statt für jede Zeile einen neuen StringBuilder zu erzeugen, StringBuilder.Clear zu verwenden bzw. vielleicht noch besser StringBuilder.Length = 0 zu setzen. Außerdem könnte bei der Erzeugung des Stringbuilders eine Kapazität von width festgelegt werden. Jedenfalls ist das Ziel, dass beim Füllen der Zeile keine Kapazitäterhöhungen erforderlich sind. Ob StringBuilder.Clear die Kapazität wieder senkt, habe ich der Doku nicht zweifelsfrei entnehmen können, aber das Setzen von Length auf 0 tut das wohl nicht.

*Cool wäre, bei langen Worten optional(!) zu prüfen, ob sie einen Bindestrich (und vielleicht auch einen Punkt, z.B. System.Windows.Forms.Button) enthalten und diese Worte dann beim letzten Vorkommen, das noch auf die Zeile passt, aufzuteilen (Achtung: So ein Wort kann sich dann prinzipiell auch über mehrere Zeilen erstrecken). Man könnte z.B. ein optionales char[] mit In-Wort-Trennzeichen übergeben.

*Praktisch wäre auch eine Variante der Methode, die einen langen String liefert, bei der die einzelnen Zeilen durch Environment.Newline getrennt sind. Darüber, ob dann ganz am Ende ein Zeilenvorschub stehen sollte/muss oder nicht sollte/darf, kann man sich natürlich streiten.

*Außerdem wäre eine Blocksatz-Option cool.

*Nützlich wäre auch eine Option, die Absätze (zwei oder mehr aufeinanderfolgende bzw. nur durch andere Whitespaces getrennte Zeilenvorschübe) im Original-Text auch in der Ausgabe erhält.

Ok, je mehr du von den Verbesserungen umsetzt, desto weniger ist es ein "Einfacher word wrap Algorithmus". 😃

herbivore

jaensen Themenstarter:in
2.760 Beiträge seit 2006
vor 11 Jahren

Danke für die Anregungen. Werde heut abend mal schauen ob ich dazu komme einige der Vorschläge umzusetzen da ich dieses Snippet selbst immer mal wieder brauche.