package de.duehl.basics.text;

/*
 * Copyright 2026 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.debug.Assure;
import de.duehl.basics.io.Charset;
import de.duehl.basics.logic.Pair;
import de.duehl.basics.text.data.FoundSearch;

/**
 * Diese Klasse bietet Methoden rund um die Behandlung von Texten.
 *
 * @version 1.01     2026-06-09
 * @author Christian Dühl
 */

public class Text {

    private static final int MAXIMAL_NUMBER_OF_PARTS_FOR_SPACE_VARIATIONS = 11;
    public static final String UMLAUT_BIG_CHARS = "ÄÖÜÁÉÍÓÚÀÈÌÒÙÂÊÎÔÛÇŞČÆØÅǺĀĆC̦ĐĞİКŁŌŒŠŚṢŜȘŽŻ";
    public static final String BIG_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + UMLAUT_BIG_CHARS;
    public static final String UMLAUT_SMALL_CHARS = "äöüßáéíóúàèìòùâêîôûøçñčńãăæćşłšïýõëśąạặǎåāаấ"
            + "ầảc̦c̣đęėệěễĕēğǧģħıīịļňṅņơόōợọœö̦őрřșțţūưůųũűžźz̆ż";
    public static final String SMALL_CHARS = "abcdefghijklmnopqrstuvwxyz" + UMLAUT_SMALL_CHARS;
    public static final String MIXED_CHARS = BIG_CHARS + SMALL_CHARS;
    public static final List<String> MIXED_CHARS_LIST =
            CollectionsHelper.splitStringIntoLetters(MIXED_CHARS);
    public static final String BIG_CHARS_REGEX = "A-Z" + UMLAUT_BIG_CHARS;
    public static final String SMALL_CHARS_REGEX = "a-z" + UMLAUT_SMALL_CHARS;
    public static final String MIXED_CHARS_REGEX = BIG_CHARS + SMALL_CHARS;
    public static final String BIG = "[" + BIG_CHARS_REGEX + "]";
    public static final String SMALL = "[" + SMALL_CHARS_REGEX + "]";
    public static final String MIXED = "[" + MIXED_CHARS_REGEX + "]";
    public static final String NO_SMALL = "[^" + SMALL_CHARS_REGEX + "]";
    public static final String DOT = "\\.";
    public static final String BULLET = "·";
    public static final String SMALL_WORD_REGEX = SMALL + "+";
    public static final String SUBSTANTIVE_REGEX = BIG + SMALL_WORD_REGEX;

    private static final String BIG_CHARS_WITH_QUESTION_MARK_REGEX = BIG_CHARS_REGEX + "\\?";
    private static final String SMALL_CHARS_WITH_QUESTION_MARKS_REGEX = SMALL_CHARS_REGEX + "\\?";
    private static final String BIG_WITH_QUESTION_MARK =
            "[" + BIG_CHARS_WITH_QUESTION_MARK_REGEX + "]";
    public static final String SMALL_WITH_QUESTION_MARKS =
            "[" + SMALL_CHARS_WITH_QUESTION_MARKS_REGEX + "]";
    public static final String SMALL_WORD_REGEX_WITH_QUESTION_MARKS =
            SMALL_WITH_QUESTION_MARKS + "+";
    public static final String SMALL_WORD_REGEX_WITH_QUESTION_MARKS_AND_UMBRUCH = ""
            + SMALL_WITH_QUESTION_MARKS + "+"
            + "(?:- " + SMALL_WITH_QUESTION_MARKS + "+)*";
    public static final String SUBSTANTIVE_REGEX_WITH_QUESTION_MARKS =
            BIG_WITH_QUESTION_MARK + SMALL_WORD_REGEX_WITH_QUESTION_MARKS;
    public static final String SUBSTANTIVE_REGEX_WITH_QUESTION_MARKS_AND_UMBRUCH =
            BIG_WITH_QUESTION_MARK + SMALL_WORD_REGEX_WITH_QUESTION_MARKS_AND_UMBRUCH;

    public static final List<String> DIGITS = CollectionsHelper.buildListFrom("0", "1", "2", "3",
            "4", "5", "6", "7", "8", "9");

    /** Systemunabhängiger Zeilenumbruch. */
    public static final String LINE_BREAK = System.getProperty("line.separator");

    public static final int NOT_FOUND_INDEX = -1;

    public static final String LINE_BREAK_PLACE_HOLDER =  "###LINE_BREAK###";
    //static final String LINE_BREAK_PLACE_HOLDER =  "\\n";

    private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile(
            "(?:" + BIG + SMALL + "+){2,}");
    private static final Pattern CAMEL_CASE_PATTERN_WITH_DOTS = Pattern.compile(
            "(?:" + BIG + SMALL + "+\\.)+(?:" + BIG + SMALL + "+\\.?)");

    /** Trennzeichen in Sätzen für Wortgrenzen. */
    private static final String DELIMITER_SYMBOLS_REGEX =  "[ |,|\\.|;|:|\\?|!/]";

    public static final List<String> PUNCTUATION_MARKS = CollectionsHelper.buildListFrom(
            ".", ",", ":", ";", "-", "_", "\\", "/");
    public static final List<String> PUNCTUATION = CollectionsHelper.buildListFrom(
            ".", "!", "?", ",", ":", ";",
            "-", "_",
            "(", ")", "[", "]", "{", "}",
            "\"", "'", "´", "`",
            "^", "°", "~", "=",
            "\\", "/");

    /** Länge einer Java-Zeile. */
    private static final int MAXIMAL_JAVA_LINE_LENGTH = 100;

    // nicht instanziieren...
    private Text() {}

    /**
     * Versieht einen langen Text mit Zeilenumbrüchen, so dass die Zeilen - so überhaupt
     * Leerzeichen vorhanden sind, höchstens lineLength Zeichen lang sind. Hierbei wird dieser
     * zuvor an schon vorhandenen Umbrüchen umgebrochen und nur die Teile dazwischen behandelt.
     *
     * @param text
     *            Gegebener Text
     * @param lineLength
     *            Zeilenlänge
     * @return umgebrochener Text
     */
    public static String addLineBreaks(String text, int lineLength) {
        TextBreaker breaker = new TextBreaker(lineLength);
        return breaker.addLineBreaks(text);
    }

    /**
     * Versieht einen langen Text mit Zeilenumbrüchen, so dass die Zeilen - so überhaupt
     * Leerzeichen vorhanden sind, höchstens lineLength Zeichen lang sind. Hierbei wird dieser
     * zuvor an schon vorhandenen Umbrüchen umgebrochen und nur die Teile dazwischen behandelt.
     *
     * @param text
     *            Gegebener Text
     * @param lineLength
     *            Zeilenlänge
     * @param numberOfFrontSpaces
     *            Anzahl einzufügender Leerzeichen am Anfang der neuen Zeilen.
     * @return umgebrochener Text
     */
    public static String addLineBreaks(String text, int lineLength, int numberOfFrontSpaces) {
        TextBreaker breaker = new TextBreaker(lineLength, numberOfFrontSpaces);
        return breaker.addLineBreaks(text);
    }

    /**
     * Versieht einen langen Text mit Zeilenumbrüchen, so dass die Zeilen - so überhaupt
     * Leerzeichen vorhanden sind, höchstens lineLength Zeichen lang sind. Hierbei wird dieser
     * zuvor an schon vorhandenen Umbrüchen umgebrochen und nur die Teile dazwischen behandelt.
     *
     * Hier werden die Leerzeichen, bei denen umgebrochen wird, am Ende der Zeilen erhalten.
     *
     * @param text
     *            Gegebener Text
     * @param lineLength
     *            Zeilenlänge
     * @return umgebrochener Text
     */
    public static String addLineBreaksKeepSpacesAtBreaks(String text, int lineLength) {
        TextBreaker breaker = new TextBreaker(lineLength);
        breaker.keepSpacesAtBreaks();
        return breaker.addLineBreaks(text);
    }

    /**
     * Versieht einen langen Text mit Zeilenumbrüchen, so dass die Zeilen - so überhaupt
     * Leerzeichen vorhanden sind, höchstens lineLength Zeichen lang sind. Hierbei wird dieser
     * zuvor an schon vorhandenen Umbrüchen umgebrochen und nur die Teile dazwischen behandelt.
     *
     * Hier werden die Leerzeichen, bei denen umgebrochen wird, am Ende der Zeilen erhalten.
     *
     * @param text
     *            Gegebener Text
     * @param lineLength
     *            Zeilenlänge
     * @param numberOfFrontSpaces
     *            Anzahl einzufügender Leerzeichen am Anfang der neuen Zeilen.
     * @return umgebrochener Text
     */
    public static String addLineBreaksKeepSpacesAtBreaks(String text, int lineLength,
            int numberOfFrontSpaces) {
        TextBreaker breaker = new TextBreaker(lineLength, numberOfFrontSpaces);
        breaker.keepSpacesAtBreaks();
        return breaker.addLineBreaks(text);
    }

    /**
     * Entfernt Zeilenumbrüche aus einem String und ersetzt sie durch Leerzeichen.
     *
     * @param text
     *            Zu bearbeitender Text.
     * @return Text ohne Zeilenumbrüche.
     */
    public static String removeLineBreaks(String text) {
        String editedText = text;

        editedText = editedText.replace(LINE_BREAK, " ");
        editedText = editedText.replace("\r\n", " ");
        editedText = editedText.replace("\n", " ");
        editedText = editedText.replace("\r", " ");

        return editedText;
    }

    /**
     * Überführt alle Zeilenumbrüche in den zum System passenden Zeilenumbruch.
     *
     * @param text
     *            Zu bearbeitender Text.
     * @return Text mit den korrekten Zeilenumbrüche.
     */
    public static String repairLineBreaks(String text) {
        String editedText = text;

        editedText = editedText.replace(LINE_BREAK, LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace("\r\n", LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace("\n", LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace("\r", LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace(LINE_BREAK_PLACE_HOLDER, LINE_BREAK);

        return editedText;
    }

    /**
     * Zählt das Vorkommen eines Zeilenumbruchs in einem Text.
     *
     * @param text
     *            Zu bearbeitender Text.
     * @return Anzahl des Vorkommens von Zeilenumbrüchen.
     */
    public static int countLineBreaks(String text) {
        return countPartInString(text, LINE_BREAK);
    }

    /**
     * Ersetzt im String alle Backslashes (\) durch Slashes (/).
     *
     * @param file
     *            Eingabe
     * @return String mit umgesetzten Slashes
     */
    public static String allBackslashesToSlashes(String file) {
        return file.replace("\\", "/");
    }

    /**
     * Ersetzt im String alle Slashes (/) durch Backslashes (\).
     *
     * @param file
     *            Eingabe
     * @return String mit umgesetzten Slashes
     */
    public static String allSlashesToBackslashes(String file) {
        return file.replace("/", "\\");
    }

    /**
     * Entfernt am Ende ein oder mehrere Slashes oder Backslashes, falls vorhanden.
     *
     * @param dir
     *            Übergebenes Verzeichnis (etwa "c:/temp/").
     * @return Bereinigtes Verzeichnis (etwa "c:/temp").
     */
    public static String removeTrailingSlash(String dir) {
        String editedDir = dir;

        while (editedDir.endsWith("/") || editedDir.endsWith("\\")) {
            editedDir = editedDir.substring(0, editedDir.length() - 1);
        }

        return editedDir;
    }

    /** Baut Perls " "x8 nach. */
    public static String multipleString(String text, int times) {
        if (times <= 0) {
            return "";
        }

        StringBuilder multi = new StringBuilder(text.length() * times);
        for (int i = 0; i < times; ++i) {
            multi.append(text);
        }
        return multi.toString();
    }

    /** Baut Perls " "x8 für einzelne Zeichen nach. */
    public static String multipleString(char character, int length) {
        String text = Character.toString(character);
        return multipleString(text, length);
    }

    /**
     * Zählt das Vorkommen eines Teilstrings in einem String.
     *
     * @param source
     *            Quelle, die durchsucht wird,
     * @param searchPart
     *            Teil, der gesucht wird.
     * @return Anzahl des Vorkommens.
     */
    public static int countPartInString(String source, String searchPart) {
        int count = 0;
        int startIndex = 0;
        boolean found = true;
        while (found) {
            int index = source.indexOf(searchPart, startIndex);
            if (index == NOT_FOUND_INDEX) {
                found = false;
            }
            else {
                ++count;
                startIndex = index + searchPart.length();
            }
        }
        return count;
    }

    /**
     * Gibt die übergebene Nachricht nach System.out aus.
     *
     * @param message
     *            Auszugebende Nachricht.
     */
    public static void say(String message) {
        System.out.println(message);
    }

    /**
     * Gibt die übergebene Nachricht nach System.out aus.
     *
     * @param message
     *            Auszugebende Nachricht.
     */
    public static void say(Object message) {
        say(message.toString());
    }

    /**
     * Gibt die übergebene Nachricht ohne anschließendem Zeilenumbruch nach System.out aus.
     *
     * @param message
     *            Auszugebende Nachricht.
     */
    public static void sayNoBreak(String message) {
        System.out.print(message);
    }

    /**
     * Gibt die übergebene Nachricht mit anschließendem Zeilenumbruch nach System.out aus.
     *
     * @param message
     *            Auszugebende Nachricht.
     */
    public static void say(int message) {
        System.out.println(Integer.toString(message));
    }

    /** Gibt eine leere Nachricht mit anschließendem Zeilenumbruch nach System.out aus. */
    public static void say() {
        say("");
    }

    /** Gibt einen Trennstrich mit anschließendem Zeilenumbruch auf System.out aus. */
    public static void bar() {
        sayNoBreak(barLine());
    }

    /** Gibt einen Trennstrich mit Zeilenumbruch zurück. */
    public static String barLine() {
        return barLineWithoutLinebreak() + "\n";
    }

    /** Gibt einen Trennstrich ohne Zeilenumbruch zurück. */
    public static String barLineWithoutLinebreak() {
        return multipleString("-", 80);
    }

    /**
     * Gibt einen Trennstrich aus Gleichheitszeichen mit anschließendem Zeilenumbruch auf
     * System.out aus.
     */
    public static void barEqual() {
        sayNoBreak(barEqualLine());
    }

    /** Gibt einen Trennstrich aus Gleichheitszeichen mit Zeilenumbruch zurück. */
    public static String barEqualLine() {
        return multipleString("=", 80) + "\n";
    }

    /**
     * Ersetzt Whitespace aller Art durch Leerzeichen, trimmt und ersetzt mehrfache Leerzeichen
     * durch eines. Entfernt auch alle führenden oder am Ende folgenden Leerzeichen.
     *
     * @param input
     *            Zu bearbeitender Text.
     * @return Bearbeiteter Text.
     */
    public static String stripWhitespace(String input) {
        String output = input;

        output = output.replaceAll("\\s+", " ");
        output = output.replaceAll("\\r", " "); // sollte in \s sein, aber wer weiß
        output = output.replaceAll("\\n", " "); // sollte in \s sein, aber wer weiß
        output = output.replaceAll("  +", " ");
        output = output.strip();

        return output;
    }

    /**
     * Entfernt sämtlichen Whitespace.
     *
     * @param input
     *            Zu bearbeitender Text.
     * @return Bearbeiteter Text.
     */
    public static String removeWhitespace(String input) {
        String output = input;
        output = stripWhitespace(output);
        output = output.replace(" ", "");
        return output;
    }

    /**
     * Fügt Leerzeichen zwischen die Zeichen (und hinter das letzte Zeichen)
     * des übergebenen Textes ein.                                               <br><br>
     *
     * Aus "abc" wird "a b c ".
     *
     * @param text
     *            Zu bearbeitender Text.
     * @return Auseinandergezogener Text.
     */
    public static String stretch(String text) {
        StringBuilder builder = new StringBuilder();
        for (char letter : text.toCharArray()) {
            builder.append(letter)
                .append(" ");
        }
        return builder.toString();
    }

    /**
     * Vergleicht zwei Texte in den ersten Stellen.
     *
     * @param text1
     *            Erster Text.
     * @param text2
     *            Zweiter Text.
     * @param numberOfChars
     *            Anzahl der Stellen, bis zu der verglichen wird. Endet einer
     *            der Texte vorher, müssen beide an der gleichen Stelle enden
     *            und gleich sein, damit wahr zurückgegeben wird.
     * @return Wahrheitswert.
     */
    public static boolean firstCharsAreEqual(String text1, String text2, int numberOfChars) {
        if (numberOfChars < 0) {
            throw new IllegalArgumentException("Die Anzahl der zu ersten "
                    + "vergleichenden Stellen darf nicht negativ sein.");
        }

        int n = text1.length();
        int m = text2.length();

        if ((n < numberOfChars || m < numberOfChars) && n != m) {
            return false;
        }

        char[] chars1 = text1.toCharArray();
        char[] chars2 = text2.toCharArray();

        int diffLength = Math.min(numberOfChars, n);
        for (int i=0; i<diffLength; ++i) {
            if (chars1[i] != chars2[i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Sucht alle Vorkommen eines Suchbegriffs in einem Text.                           <br><br><i>
     *
     * Achtung, es wird nicht verhindert, dass sich die Fundstellen überlappen.
     * Sucht man in 'foofoofoo' nach 'foofoo', wird man zwei Treffer erhalten,
     * an Position 0 und Position 3.                                                          </i>
     *
     * @param searchText
     *            Text, nach dem gesucht wird.
     * @param text
     *            Text, der durchsucht wird.
     * @return Liste mit allen Null-basierten Fundstellen des Suchtextes im
     *         Text, ggf. leer.
     */
    public static List<Integer> findAllPositions(String searchText, String text) {
        List<Integer> list = new ArrayList<>();

        int index = text.indexOf(searchText);
        while (index > NOT_FOUND_INDEX) {
            list.add(index);
            /*
             * Um einen Bug in Java zu beheben, der dazu führt, dass
             *      text.indexOf("", 10);
             * bei einem neun Zeichen langen String 9 zurückliefert:
             */
            int oldIndex = index;
            index = text.indexOf(searchText, index + 1);
            if (index == oldIndex) {
                index = NOT_FOUND_INDEX;
            }
        }

        return list;
    }

    /**
     * Sucht alle Vorkommen mehrere Suchbegriffe in einem Text.                          <br><br><i>
     *
     * Achtung, es wird nicht verhindert, dass sich die Fundstellen überlappen.
     * Sucht man in 'foofoofoo' nach 'foofoo', wird man zwei Treffer erhalten,
     * an Position 0 und Position 3.                                                          </i>
     *
     * @param searchTexts
     *            Die Texte, nach denen gesucht wird.
     * @param text
     *            Text, der durchsucht wird.
     * @return Liste mit allen Null-basierten Fundstellen des Suchtextes im
     *         Text, ggf. leer.
     */
    public static List<Integer> findAllPositions(List<String> searchTexts, String text) {
        List<Integer> allPositions = new ArrayList<>();
        for (String searchText : searchTexts) {
            List<Integer> positions = findAllPositions(searchText, text);
            allPositions.addAll(positions);
        }
        Collections.sort(allPositions);
        return allPositions;
    }

    /**
     * Sucht alle Vorkommen eines Suchbegriffs in einem Text.                           <br><br><i>
     *
     * Hierbei wird verhindert, dass sich die Fundstellen überlappen.
     * Sucht man in 'foofoofoo' nach 'foofoo', wird man nur einen Treffer erhalten,
     * an Position 0.                                                                          </i>
     *
     * @param searchText
     *            Text, nach dem gesucht wird.
     * @param text
     *            Text, der durchsucht wird.
     * @return Liste mit allen Null-basierten Fundstellen des Suchtextes im
     *         Text, ggf. leer.
     */
    public static List<Integer> findAllPositionsWithoutOverlapping(String searchText, String text) {
        List<Integer> indices = Text.findAllPositions(searchText, text);

        boolean removeToCloseIndices = true;
        while (removeToCloseIndices) {
            removeToCloseIndices = false;
            int lastPostion = -searchText.length();
            for (int indexOfIndices = 0; indexOfIndices < indices.size(); ++indexOfIndices) {
                int index = indices.get(indexOfIndices);
                if (index - lastPostion < searchText.length()) {
                    removeToCloseIndices = true;
                    indices.remove(indexOfIndices);
                    break;
                }
                else {
                    lastPostion = index;
                }
            }
        }

        return indices;
    }

    /**
     * Sucht alle Vorkommen eines Suchbegriffs als ganzes Wort in einem Text.
     *
     * @param searchText
     *            Text, nachdem gesucht wird.
     * @param text
     *            Text, der durchsucht wird.
     * @return Liste mit allen Null-basierten Fundstellen des Suchtextes im Text, ggf. leer.
     */
    public static List<Integer> findAllPositionsOfCompleteWords(String searchText, String text) {
        List<Integer> pos = findAllPositions(searchText, text);
        int length = searchText.length();

        List<Integer> realWordPositions = new ArrayList<>();
        for (int index = 0; index < pos.size(); ++index) {
            int indexInPos = pos.get(index);
            if (isStartAndEndOfWord(indexInPos, indexInPos + length, text)) {
                realWordPositions.add(indexInPos);
            }
        }

        return realWordPositions;
    }

    /**
     * join() wie in Perl.
     *
     * @param fill
     *            Einzusetzendes Zwischenstück.
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch das einzusetzende Füllstück verbunden
     *         zusammengesetzt.
     */
    public static String join(String fill, List<String> list) {
        if (list.isEmpty()) {
            return "";
        }
        else if (list.size() == 1) {
            return list.get(0);
        }
        else {
            StringBuilder build = new StringBuilder();
            build.append(list.get(0));
            for (int i = 1; i < list.size(); ++i) {
                build.append(fill);
                build.append(list.get(i));
            }
            return build.toString();
        }
    }

    /**
     * join() wie in Perl.
     *
     * @param fill
     *            Einzusetzendes Zwischenstück.
     * @param array
     *            Array mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch das einzusetzende Füllstück verbunden
     *         zusammengesetzt.
     */
    public static String join(String fill, String ... array) {
        List<String> list = Arrays.asList(array);
        return join(fill, list);
    }

    /**
     * join() wie in Perl mit Tabulator.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Tabulatoren verbunden zusammengesetzt.
     */
    public static String joinWithTabulator(List<String> list) {
        return join("\t", list);
    }

    /**
     * join() wie in Perl mit Tabulator.
     *
     * @param array
     *            Array mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Tabulatoren verbunden zusammengesetzt.
     */
    public static String joinWithTabulator(String ... array) {
        List<String> list = Arrays.asList(array);
        return joinWithTabulator(list);
    }

    /**
     * join() wie in Perl mit Zeilenumbrüchen.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Zeilenumbrüche verbunden zusammengesetzt.
     */
    public static String joinWithLineBreak(List<String> list) {
        return join(LINE_BREAK, list);
    }

    /**
     * join() wie in Perl mit Pipezeichen.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Pipezeichen verbunden zusammengesetzt.
     */
    public static String joinWithPipe(List<String> list) {
        return join("|", list);
    }

    /**
     * join() wie in Perl mit Leerzeichen.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Leerzeichen verbunden zusammengesetzt.
     */
    public static String joinWithBlank(List<String> list) {
        return join(" ", list);
    }

    /**
     * join() wie in Perl mit Semikolon.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Semikolons verbunden zusammengesetzt.
     */
    public static String joinWithSemicolon(List<String> list) {
        return join(";", list);
    }

    /**
     * join() wie in Perl mit Doppelpunkt.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Doppelpunkte verbunden zusammengesetzt.
     */
    public static String joinWithColon(List<String> list) {
        return join(":", list);
    }

    /**
     * join() wie in Perl mit Komma.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Kommata verbunden zusammengesetzt.
     */
    public static String joinWithComma(List<String> list) {
        return join(",", list);
    }

    /**
     * join() wie in Perl mit Komma und einem Leerzeichen.
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch Kommata und je und einem Leerzeichen verbunden
     *         zusammengesetzt.
     */
    public static String joinWithCommaAndBlank(List<String> list) {
        return join(", ", list);
    }

    /**
     * join() wie in Perl mit "#;#".
     *
     * @param list
     *            Liste mit den zusammen zu setzenden Elementen.
     * @return Alle Elemente der Liste werden durch "#;#" verbunden zusammengesetzt.
     */
    public static String joinWithRauteSemicolonRaute(List<String> list) {
        return join("#;#", list);
    }

    /**
     * Splittet die Eingabe am gegebenen Ausdruck.
     *
     * @param input
     *            Eingabe.
     * @param regex
     *            Regulärer Ausdruck an dem gesplittet wird.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBy(String input, String regex) {
        List<String> list = new ArrayList<>();
        if (!input.isEmpty()) {
            list.addAll(Arrays.asList(input.split(regex, -1)));
        }
        /*
         * Einschub wegen der absonderlichen Eigenheiten von input.split, vielleicht sollte ich
         * das Ding mal selbst besser schreiben...:
         */
        if (regex.isEmpty() && !list.isEmpty() && list.get(list.size() - 1).isEmpty()) {
            list.remove(list.size() - 1);
        }
        return list;
        /*
         * Wenn ich es selbst schreibe, dann auch so, dass man auch schon ein kompiliertes Pattern
         * übergeben kann.
         */
    }

    /**
     * Splittet die Eingabe an allem Whitespace.
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByWhitespace(String input) {
        return splitBy(input, "[ \\t]+");
    }

    /**
     * Splittet die Eingabe an aufeinander folgenden Leerzeichen.
     *
     * @param input
     *            Eingabe.
     * @param numberOfSpacesForColumnDistinction
     *            Anzahl an Leerzeichen, die mindestens nebeneinander vorhanden sein müssen, um als
     *            Spaltentrenner zu gelten.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByMultipleSpaces(String input,
            int numberOfSpacesForColumnDistinction) {
        if (numberOfSpacesForColumnDistinction < 1) {
            throw new IllegalArgumentException("Die Anzahl der Leerzeichen muss mindestens 1 sien!");
        }
        return splitBy(input, "[ ]{" + numberOfSpacesForColumnDistinction + ",}") ;
    }

    /**
     * Splittet die Eingabe an allen Pipesymbolen (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByPipe(String input) {
        return splitBy(input, " *\\| *");
    }

    /**
     * Splittet die Eingabe an allen Pipesymbolen (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByPipeNotConsumingWhitespace(String input) {
        return splitBy(input, "\\|");
    }

    /**
     * Splittet die Eingabe an allen in einzelne Anführungszeichen gehüllte Leerzeichen oder
     * Hashsymbole (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByQuotedBlankOrHash(String input) {
        return splitBy(input, " *'(?:#| )' *");
    }

    /**
     * Splittet die Eingabe an allen Pluszeichen (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByAddition(String input) {
        return splitBy(input, " *\\+ *");
    }

    /**
     * Splittet die Eingabe an allen Minuszeichen (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySubtraction(String input) {
        return splitBy(input, " *- *");
    }

    /**
     * Splittet die Eingabe an allen Kommata (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByKomma(String input) {
        return splitBy(input, " *, *");
    }

    /**
     * Splittet die Eingabe an allen Tabulatoren (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByTabulator(String input) {
        return splitBy(input, " *\t *");
    }

    /**
     * Splittet die Eingabe an allen Tabulatoren (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByTabulatorNotConsumingWhitespace(String input) {
        return splitBy(input, "\t");
    }

    /**
     * Splittet die Eingabe an allen Semikolons (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySemicolon(String input) {
        return splitBy(input, " *; *");
    }

    /**
     * Splittet die Eingabe an allen Semikolons (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySemicolonNotConsumingWhitespace(String input) {
        return splitBy(input, ";");
    }

    /**
     * Splittet die Eingabe an allen Doppelpunkten (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByColon(String input) {
        return splitBy(input, " *: *");
    }

    /**
     * Splittet die Eingabe an allen Doppelpunkten (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByColonNotConsumingWhitespace(String input) {
        return splitBy(input, ":");
    }

    /**
     * Splittet die Eingabe an allen Spezialtrennern #;# (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySpecial(String input) {
        return splitByRauteSemicolonRaute(input);
        //return splitBy(input, " *#;# *");
    }

    /**
     * Splittet die Eingabe an allen Spezialtrennern #;# (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySpecialNotConsumingWhitespace(String input) {
        return splitByRauteSemicolonRauteNotConsumingWhitespace(input);
        //return splitBy(input, "#;#");
    }

    /**
     * Splittet die Eingabe an allen Spezialtrennern #;# (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByRauteSemicolonRaute(String input) {
        return splitBy(input, " *#;# *");
    }

    /**
     * Splittet die Eingabe an allen Spezialtrennern #;# (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByRauteSemicolonRauteNotConsumingWhitespace(String input) {
        return splitBy(input, "#;#");
    }

    /**
     * Splittet die Eingabe an Zeilenumbrüchen.
     *
     * @param input
     *            Eingabe.
     * @param regex
     *            Regulärer Ausdruck an dem gesplittet wird.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByLineBreaks(String input) {
        return splitBy(input, "(\\Q" + LINE_BREAK + "\\E|\r?\n|\r)");
    }

    /**
     * Splittet die Eingabe an Punkten (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @param regex
     *            Regulärer Ausdruck an dem gesplittet wird.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByDot(String input) {
        return splitBy(input, " *\\. *");
    }

    /**
     * Splittet die Eingabe an Punkten (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByDotNotConsumingWhitespace(String input) {
        return splitBy(input, "\\.");
    }

    /**
     * Splittet die Eingabe an allen Gleichheitszeichen (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitByEqualSign(String input) {
        return splitBy(input, " *= *");
    }

    /**
     * Splittet die Eingabe an allen Slashes und Backslashes (mit ggf. Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySlashOrBackSlash(String input) {
        return splitBy(input, " *(?:/|\\\\) *");
    }

    /**
     * Splittet die Eingabe an allen Slashes und Backslashes (ohne Whitespace drumherum).
     *
     * @param input
     *            Eingabe.
     * @return Liste aller Teile der Eingabe.
     */
    public static List<String> splitBySlashOrBackSlashNotConsumingWhitespace(String input) {
        return splitBy(input, "(?:/|\\\\)");
    }

    /**
     * Schneidet den angegebenen Bereich aus dem String aus und gibt den Rest zurück. Hierbei wird
     * dafür gesorgt, dass an der Schnittstelle genau ein Leerzeichen verbleibt.
     *
     * @param text
     *            Text
     * @param start
     *            Startindex des auszuschneidenden Bereiches.
     * @param length
     *            Länge des auszuschneidenden Bereiches.
     * @return Rest des Textes ohne das ausgeschnittene Stück.
     */
    public static String removeFromTextRespectingSpaces(String text, int start, int length) {
        if (start < 0) {
            throw new IllegalArgumentException("Der Anfang liegt vor dem Anfang des "
                    + "übergebenen Textes.\n"
                    + "\t" + "start         = " + start + "\n"
                    + "\t" + "text          = " + text + "\n");
        }
        if (start > text.length()) {
            throw new IllegalArgumentException("Der Anfang liegt hinter dem Ende des "
                    + "übergebenen Textes.\n"
                    + "\t" + "start         = " + start + "\n"
                    + "\t" + "text.length() = " + text.length() + "\n"
                    + "\t" + "text          = " + text + "\n");
        }
        if (length < 0) {
            throw new IllegalArgumentException("length " + length + " ist zu klein, muss "
                    + "mindestens 0 sein");
        }

        int end = start + length; // Endindex des auszuschneidenden Bereiches.
        if (end > text.length()) {
            throw new IllegalArgumentException("Das Ende (Start + Länge) liegt hinter dem Ende des "
                    + "übergebenen Textes.\n"
                    + "\t" + "start         = " + start + "\n"
                    + "\t" + "length        = " + length + "\n"
                    + "\t" + "end           = " + end + "\n"
                    + "\t" + "text.length() = " + text.length() + "\n"
                    + "\t" + "text          = " + text + "\n");
        }

        StringBuilder result = new StringBuilder();

        /* Es befindet sich etwas davor und dahinter: */
        if (start > 0 && end < text.length()) {
            /* Das davor endet auf Leerzeichen und das danach beginnt damit: */
            if (' ' == text.charAt(start-1) && ' ' == text.charAt(end)) {
                result.append(text.substring(0, start-1));
                result.append(text.substring(end));
            }
            /* Keine Leerzeichen davor oder dahinter: */
            else if (' ' != text.charAt(start-1) && ' ' != text.charAt(end)) {
                result.append(text.substring(0, start));
                result.append(" ");
                result.append(text.substring(end));
            }
            /* Genau ein Leerzeichen: */
            else {
                result.append(text.substring(0, start));
                result.append(text.substring(end));
            }
        }
        else if (start > 0) {
            result.append(text.substring(0, start));
        }
        else {
            result.append(text.substring(end));
        }

        return result.toString();
    }

    /**
     * Entfernt den angegebenen Bereich aus dem String und fügt ein Leerzeichen an seiner Stelle
     * ein.
     *
     * @param text
     *            Zu bearbeitender Text.
     * @param position
     *            Position an der ausgeschnitten wird (0-basierter Index).
     * @param length
     *            Länge des ausgeschnittenen Textes.
     * @return Text ohne den angegebenen Bereich.
     */
    public static String removeFromText(String text, int position, int length) {
        if (length < 0) {
            throw new IllegalArgumentException("length " + length + " ist zu klein, muss "
                    + "mindestens 0 sein");
        }
        return replaceInText(text, " ", position, position + length);
    }

    /**
     * Entfernt das erste vorkommen des zu entfernenden Teils aus dem Text. Kommt dieser nicht im
     * Text vor, wird der Text unverändert zurückgegeben.
     *
     * @param text
     *            Zu bearbeitender Text.
     * @param toRemove
     *            Zu entfernender Teil des Textes.
     * @return Text ohne den zu entfernenden Teil.
     */
    public static String removeFirstOccurance(String text, String toRemove) {
        int index = text.indexOf(toRemove);
        if (index == -1) {
            return text;
        }
        else {
            return replaceInText(text, "", index, index + toRemove.length());
        }
    }

    /**
     * Fügt ein Stück Text in den Eingabetext ein.
     *
     * @param text
     *            Eingabetext.
     * @param replace
     *            Neu einzufügendes Textstück.
     * @param index
     *            Position an der der Text eingefügt werden soll (0-basierter Index).
     * @return Ersetzter String.
     */
    public static String insertIntoText(String text, String insertion, int index) {
        return replaceInText(text, insertion, index, index);
    }

    /**
     * Tauscht ein Stück des Eingabetextes aus.
     *
     * @param text
     *            Eingabetext.
     * @param replace
     *            Neu einzufügendes Textstück.
     * @param start
     *            Erste Position des zu entfernenden Teiles (0-basierter Index).
     * @param end
     *            Erste Position nach dem Ende des zu entfernenden Teiles (0-basierter Index).
     * @return Ersetzter String.
     */
    public static String replaceInText(String text, String replace, int start, int end) {
        checkStartAndEnd(text, start, end);

        String front = text.substring(0, start);
        String rear = text.substring(end);
        String newText = front + replace + rear;

        return newText;
    }

    private static void checkStartAndEnd(String text, int start, int end) {
        if (start < 0) {
            throw new IllegalArgumentException("start " + start + " ist zu klein, muss mindestens "
                    + "0 sein");
        }
        if (start > 0 && start > text.length()) {
            throw new IllegalArgumentException("start " + start + " ist zu groß, darf höchstens "
                    + text.length() + " sein");
        }
        if (end > text.length()) {
            throw new IllegalArgumentException("end " + end + " ist zu groß, darf höchstens "
                    + text.length() + " sein");
        }
        if (start > end) {
            throw new IllegalArgumentException("start (" + start + ") darf höchstens so groß wie "
                    + "end (" + end + ") sein");
        }
    }

    /**
     * Tauscht ein Stück des Eingabetextes durch eine Zeichenfolge gleicher Länge aus.
     *
     * @param text
     *            Eingabetext.
     * @param character
     *            Zeichen, das so oft eingefügt werden soll, wie das Fundstück lang ist.
     * @param start
     *            Erste Position des zu entfernenden Teiles.
     * @param end
     *            Erste Position nach dem Ende des zu entfernenden Teiles.
     * @return Ersetzter String.
     */
    public static String replaceInTextWithEqualLengthCharacterSequence(String text,
            char character, int start, int end) {
        checkStartAndEnd(text, start, end);
        int length = end - start;
        String replace = multipleString(character, length);
        return replaceInText(text, replace, start, end);
    }

    /**
     * Ersetzt alle zu ersetzenden Worte im Text durch eine Zeichenfolge gleicher Länge.
     *
     * @param text
     *            Zu ersetzenden Text.
     * @param replaceWords
     *            Worte die ersetzt werden sollen.
     * @return Text mit durch Zeichenfolgen gleicher Länge ersetzen Suchbegriffen.
     */
    public static String replaceInTextWithEqualLengthCharacterSequence(String text,
            List<String> replaceWords) {
        String replacedText = text;
        for (String replace : replaceWords) {
            String oldReplacedText = "";
            while (!replacedText.equals(oldReplacedText)) {
                oldReplacedText = replacedText;
                replacedText = replaceInTextWithEqualLengthCharacterSequenceOnce(replacedText,
                        replace);
            }
        }

        return replacedText;
    }

    /**
     * Ersetzt das erste Vorkommen des Suchbegriffs
     *
     * @param text
     *            Zu ersetzender Text.
     * @param replace
     *            Suchbegriffs.
     * @return Text mit einem durch eine Zeichenfolge gleicher Länge ersetzen Suchbegriff.
     */
    static String replaceInTextWithEqualLengthCharacterSequenceOnce(String text, String replace) {
        String replacedText = text;
        int index = text.indexOf(replace);
        if (index > -1) {
            replacedText = replaceInTextWithEqualLengthCharacterSequence(text, '#', index, index
                    + replace.length());
        }
        return replacedText;
    }


    /** Ersetzt Zeilenumbrüche durch einen Platzhalter. */
    public static String hideLineBreaks(String input) {
        return replaceLineBreaks(input, LINE_BREAK_PLACE_HOLDER);
    }

    /** Ersetzt Zeilenumbrüche durch ein literales \n. */
    public static String hideLineBreaksWithLiteralBackslashN(String input) {
        return replaceLineBreaks(input, "\\n");
    }

    /** Ersetzt die Platzhalter wieder durch echte Zeilenumbrüche. */
    public static String showLineBreaksAfterHiding(String input) {
        String text = input;

        text = text.replace(LINE_BREAK_PLACE_HOLDER, LINE_BREAK);

        return text;
    }

    /** Ersetzt Zeilenumbrüche durch ein Leerzeichen. */
    public static String lineBreaksToSpace(String input) {
        return replaceLineBreaks(input, " ");
    }

    /** Ersetzt Zeilenumbrüche durch \n. */
    public static String lineBreaksToBackslashN(String input) {
        return replaceLineBreaks(input, "\n");
    }

    /** Ersetzt Zeilenumbrüche durch den richtigen Zeilenumbruch. */
    public static String backslashNToLineBreaks(String input) {
        return input.replace("\n", LINE_BREAK);
    }

    /**
     * Ersetzt Zeilenumbrüche durch ein anderes Zeichen.
     *
     * @param input
     *            Eingabetext, der bearbeitet werden soll.
     * @param replacement
     *            Ersetzungstext für den Zeilenumbruch.
     * @return Text mit ersetzten Zeilenumbrüchen.
     */
    public static String replaceLineBreaks(String input, String replacement) {
        String text = input;

        text = text.replace(LINE_BREAK, replacement);
        text = text.replace("\r\n", replacement);
        text = text.replace("\n", replacement);
        text = text.replace("\r", replacement);

        return text;
    }

    /**
     * Ermittelt den Text aus dem Feld, trimmt ihn und wandelt Tabulatoren und Umbrüche in einfache
     * Leerzeichen um.
     */
    public static String removeLineBreaksAndTabulators(String input) {
        String output = input;
        output = lineBreaksToSpace(output);
        output = output.replace("\t", " ");
        output = output.replaceAll(" +", " ");
        output = output.strip();
        return output;
    }

    /**
     * Entfernt alle Kommentarzeilen (die mit "#" beginnen) und liefert alle anderen zurück.
     *
     * @param lines
     *            Liste mit den zu bearbeitenden Zeilen.
     * @return Liste mit allen Zeilen, die keine Kommentare sind.
     */
    public static List<String> stripCommentsFromLines(List<String> lines) {
        List<String> stripped = new ArrayList<>();

        for (String line : lines) {
            if (!line.startsWith("#")) {
                stripped.add(line);
            }
        }

        return stripped;
    }

    /**
     * Entfernt die letzten x Zeichen aus einem String. Ist der String kürzer als die angegebene
     * Anzahl an Zeichen, wird der leere String zurückgegeben.
     *
     * @param input
     *            Zu bearbeitender String
     * @param numberOfCharacters
     *            Anzahl der am Ende zu entfernenden Zeichen.
     * @return String ohne die letzten x Zeichen.
     */
    public static String removeLastCharacters(String input, int numberOfCharacters) {
        if (input.length() <= numberOfCharacters) {
            return "";
        }
        else {
            return input.substring(0,  input.length() - numberOfCharacters);
        }
    }

    /**
     * Füllt einen String hinten bis zur angegebenen Gesamtlänge mit Leerzeichen auf.
     *
     * fillWithSpaces("abc", 5) ergibt den String "abc  ".
     *
     * Ist der Text mindestens so lang wie die angegebene Länge, wird er unverändert zurückgegeben.
     *
     * @param text
     *            Aufzufüllender Text.
     * @param length
     *            Länge, bis zu der der Text mit Leerzeichen aufzufüllen ist.
     * @return Aufgefüllter Text.
     */
    public static String fillWithSpaces(String text, int length) {
        if (length <= text.length()) {
            return text;
        }
        else {
            String fill = multipleString(" ", length - text.length());
            return text + fill;
        }
    }

    /**
     * Füllt einen String vorn bis zur angegebenen Gesamtlänge mit Leerzeichen auf.
     *
     * fillWithSpacesAtFront("abc", 5) ergibt den String "  abc".
     *
     * Ist der Text mindestens so lang wie die angegebene Länge, wird er unverändert zurückgegeben.
     *
     * @param text
     *            Aufzufüllender Text.
     * @param length
     *            Länge, bis zu der der Text mit Leerzeichen aufzufüllen ist.
     * @return Aufgefüllter Text.
     */
    public static String fillWithSpacesAtFront(String text, int length) {
        if (length <= text.length()) {
            return text;
        }
        else {
            String fill = multipleString(" ", length - text.length());
            return fill + text;
        }
    }

    /**
     * Füllt einen String hinten bis zur angegebenen Gesamtlänge mit Minuszeichen auf.
     *
     * fillWithSpaces("abc", 5) ergibt den String "abc--".
     *
     * Ist der Text mindestens so lang wie die angegebene Länge, wird er unverändert zurückgegeben.
     *
     * @param text
     *            Aufzufüllender Text.
     * @param length
     *            Länge, bis zu der der Text mit Minuszeichen aufzufüllen ist.
     * @return Aufgefüllter Text.
     */
    public static String fillWithLines(String text, int length) {
        if (length <= text.length()) {
            return text;
        }
        else {
            String fill = multipleString("-", length - text.length());
            return text + fill;
        }
    }

    /**
     * Bereinigt die Liste mit den Begriffen derart, dass keine Teilbegriffe eines Begriffs in
     * einem anderen mehr
     * auftauchen.
     *
     * @param terms
     *            Menge mit den zu bereinigenden Begriffen.
     * @return Bereinigte Liste mit den Begriffen.
     */
    public static List<String> cleanCollectionOfStringsToNotContainingEachOther(
            Collection<String> terms) {
        List<String> cleanedTerms = new ArrayList<>();
        cleanedTerms.addAll(terms);

        boolean removing = true;
        while (removing) {
            removing = cleanCollectionOfStringsToNotContainingEachOtherOneTime(cleanedTerms);
        }

        return cleanedTerms;
    }

    /**
     * Entfernt einen Term, der in einem anderen enthalten ist.
     *
     * @param cleanedTerms
     *            Liste mit den zu bereinigenden Termen.
     * @return Wahrheitswert: true genau dann, wenn ein Term entfernt wurde.
     */
    private static boolean cleanCollectionOfStringsToNotContainingEachOtherOneTime(
            List<String> cleanedTerms) {
        for (int i=0; i<cleanedTerms.size(); ++i) {
            String partI = cleanedTerms.get(i);
            for (int j=i+1; j<cleanedTerms.size(); ++j) {
                String partJ = cleanedTerms.get(j);
                if (partI.contains(partJ)) {
                    cleanedTerms.remove(j);
                    return true;
                }
                if (partJ.contains(partI)) {
                    cleanedTerms.remove(i);
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Gibt den Teil des Strings hinter dem letzten Vorkommen des Trenners zurück. Kommt der
     * Trenner nicht vor, wird die ganze Eingabe zurückgegeben.
     *
     * @param input
     *            Eingabetext.
     * @param divider
     *            Trenner
     * @return Teil des Strings hinter dem letzten Trenner.
     */
    public static String getLastPartAfterDivider(String input, String divider) {
        int lastDividerIndex = input.lastIndexOf(divider);
        if (NOT_FOUND_INDEX == lastDividerIndex) {
            return input;
        }
        else {
            return input.substring(lastDividerIndex + divider.length());
        }
    }

    /**
     * Entfernt Leerzeichen zu Beginn und am Ende und macht aus allen mehrfachen Leerzeichen ein
     * einzelnes.
     *
     * @param input
     *            Zu bearbeitender Text.
     * @return Bearbeiteter Text.
     */
    public static String trimAndCompactSpaces(String input) {
        String cleaned = input;

        cleaned = cleaned.replaceAll("^ +", "");
        cleaned = cleaned.replaceAll(" +$", "");
        cleaned = cleaned.replaceAll(" +", " ");
        //cleaned = cleaned.replaceAll("[ ]{2,}", " ");

        return cleaned;
    }

    /** Setzt die Zeilen zu einem String zusammen. */
    public static String listOfLinesToString(List<String> lines) {
        StringBuilder builder = new StringBuilder();

        for (String line : lines) {
            builder.append(line);
            builder.append(LINE_BREAK);
        }

        return builder.toString();
    }

    /**
     * Nimmt einen durch System.getProperty("line.separator") zusammengesetzten Text in einem
     * String wieder auseinander, so dass einzelne Zeilen entstehen.
     */
    public static List<String> stringToListOfLines(String text) {
        List<String> lines = splitBy(text, Pattern.quote(LINE_BREAK));

        if (!lines.isEmpty() && lines.get(lines.size() - 1).isEmpty()) {
            lines.remove(lines.size() - 1);
        }

        return lines;
    }

    /**
     * Sucht die erste Zeile aus der übergebenen Liste von Zeilen heraus, die den gesuchten String
     * enthält, und gibt deren Index (0-basiert) zurück.
     *
     * @param lines
     *            Liste von zu durchsuchenden Zeilen.
     * @param search
     *            Suchbegriff.
     * @return Index der ersten Zeile, die den Suchbegriff enthält oder NOT_FOUND_INDEX (-1), falls
     *         der Suchbegriff in keiner Zeile enthalten ist.
     */
    public static int searchFirstLineIndexContainig(List<String> lines, String search) {
        for (int index = 0; index < lines.size(); ++index) {
            String line = lines.get(index);
            if (line.contains(search)) {
                return index;
            }
        }

        return NOT_FOUND_INDEX;
    }

    /**
     * Ermittelt ein Textstück dass zwischen den genannten Start- und Endzeichen liegt
     * (ausschließlich dieser).
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param from
     *            Start-Text.
     * @param to
     *            End-Text.
     * @throws RuntimeException
     *             falls die Randstücke nicht oder nicht eindeutig sind im Text.
     */
    public static String findInText(String text, String from, String to) {
        int indexFrom = text.indexOf(from);
        if (-1 == indexFrom) {
            throw new RuntimeException("Das Anfangsstück '" + from + "' wurde nicht im Text "
                    + "gefunden.\n\ttext = " + text);
        }
        int indexNextFrom = text.indexOf(from, indexFrom + from.length());
        if (-1 != indexNextFrom) {
            throw new RuntimeException("Das Anfangsstück '" + from + "' ist nicht eindeutig im "
                    + "Text.\n\ttext = " + text);
        }
        int indexTo = text.indexOf(to);
        if (-1 == indexTo) {
            throw new RuntimeException("Das Endstück '" + to + "' wurde nicht im Text "
                    + "gefunden.\n\ttext = " + text);
        }
        int indexNextTo = text.indexOf(to, indexTo + to.length());
        if (-1 != indexNextTo) {
            throw new RuntimeException("Das Endstück '" + to + "' ist nicht eindeutig im "
                    + "Text.\n\ttext = " + text);
        }
        int start = indexFrom + from.length();
        int end = indexTo;
        return text.substring(start, end);
    }

    /**
     * Stellt einen double Wert auf zwei Nachkommastellen gerundet dar.
     *
     * @param value
     *            Wert der double-Variablen.
     * @return String mit dem Wert der Variablen auf zwei Nachkommastellen gerundet.
     */
    public static String doubleTwoDecimalPlaces(double value) {
        return String.format("%.2f", value);
    }

    /**
     * Diese Methode bildet aus einem Text einen einheitlich klein geschriebenen Text, in dem die
     * Umlaute und das ß durch ausgeschrieben Versionen ('ae', 'oe', 'ue' und 'ss') ersetzt sind.
     *
     * @param text
     *            Zu verändernden Text.
     * @return Vergleichstext
     */
    public static String buildCompareText(String text) {
        String compare = toLowerCase(text);

        compare = compare.replace("ä", "ae");
        compare = compare.replace("ö", "oe");
        compare = compare.replace("ü", "ue");
        compare = compare.replace("ß", "ss");

        return compare;
    }

    /**
     * Ermittelt, ob das übergebene Zeichen ein Zeichen innerhalb eines Wortes ist.
     *
     * @param c
     *            Zeichen
     * @return Wahrheitswert
     */
    public static boolean isWordChar(char c) {
        if (isWhitespace(c) || isPunctuation(c) || isBrace(c))
            return false;
        else
            return true;
    }

    /**
     * Ermittelt, ob das übergebene Zeichen Whitespace ist.
     *
     * @param c
     *            Zeichen
     * @return Wahrheitswert
     */
    public static boolean isWhitespace(char c) {
        return c == ' ' || c == '\n' || c == '\t';
    }

    /**
     * Ermittelt, ob das übergebene Zeichen eine Klammer ist.
     *
     * @param c
     *            Zeichen
     * @return Wahrheitswert
     */
    public static boolean isBrace(char c) {
        return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}'
                || c == '<' || c == '>';
    }

    /**
     * Ermittelt, ob das übergebene Zeichen ein Satzzeichen ist.
     *
     * @param c
     *            Zeichen
     * @return Wahrheitswert
     */
    public static boolean isPunctuation(char c) {
        return c == '.' || c == ',' || c == ':' || c == ';' || c == '!' || c == '?' || c == '-'
                || c == '_';
    }

    /**
     * Ermittelt, ob das übergebene Zeichen ein Großbuchstabe ist.
     *
     * @param c
     *            Zeichen
     * @return Wahrheitswert
     */
    public static boolean isCapitalLetter(char c) {
        return c >= 'A' && c <= 'Z' || 'Ä' == c || 'Ö' == c || 'Ü' == c;
    }

    /**
     * Ermittelt, ob dar übergebene text mit einem Großbuchstabe beginnt.
     *
     * @param text
     *            Zu prüfender Text.
     * @return Wahrheitswert
     */
    public static boolean firstLetterIsCapitalLetter(String text) {
        if (text.isEmpty()) {
            return false;
        }
        char firstLetter = text.charAt(0);
        return isCapitalLetter(firstLetter);
    }

    /** Liefert den überlappenden Teil der beiden Texte zurück. */
    public static String findIntersection(String text1, String text2) {
        String intersection = "";

        int maxLength = Math.min(text1.length(), text2.length());

        for (int length = 0; length <= maxLength; ++length) {
            String testIntersection = text2.substring(0,  length);
            if (text1.endsWith(testIntersection)) {
                intersection = testIntersection;
            }
        }

        return intersection;
    }

    /** Prüft, ob der Text einen Großbuchstaben enthält. */
    public static boolean containsUpperLetter(String input) {
        for (char c : input.toCharArray()) {
            if (isCapitalLetter(c)) {
                return true;
            }
        }

        return false;
    }

    /** Prüft, ob ein Begriff aus der Liste im Text vorkommt. */
    public static boolean contains(String input, List<String> searches) {
        for (String searchWord : searches) {
            if (input.contains(searchWord)) {
                return true;
            }
        }
        return false;
    }

    /** Prüft, ob der Text auf einen  der Begriffe aus der Liste endet. */
    public static boolean endsWith(String input, List<String> searches) {
        for (String searchWord : searches) {
            if (input.endsWith(searchWord)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Erzeugt aus einer Eingabe einen String, dessen erstes Zeichen groß und der Rest unverändert
     * ist.
     */
    public static String firstCharToUpperCase(String input) {
        if (input.isEmpty()) {
            return input;
        }
        else {
            String firstChar = input.substring(0, 1);
            String rest = input.substring(1);
            return toUpperCase(firstChar) + rest;
        }
    }

    /**
     * Formt alle Worte in der Eingabe so um, dass das erste Zeichen groß und der Rest des Wortes
     * unverändert ist.
     */
    public static String toAllWordsFirstCharToUpperCase(String input) {
        if (input.isEmpty()) {
            return "";
        }

        List<String> parts = new ArrayList<>();
        for (String part : splitBy(input, "[ \\t]+")) {
            if (part.isEmpty()) {
                throw new RuntimeException("Es wurde an Whitespace getrennt, da darf kein "
                        + "leerer String auftauchen!");
            }
            part = firstCharToUpperCase(part);
            parts.add(part);
        }

        return joinWithBlank(parts);
    }

    /**
     * Erzeugt aus einer Eingabe einen String, dessen erstes Zeichen groß und der Rest klein ist.
     */
    public static String toFirstUpperCase(String input) {
        if (input.isEmpty()) {
            return input;
        }
        if (input.length() == 1) {
            return toUpperCase(input);
        }
        else  {
            String firstChar = input.substring(0, 1);
            String rest = input.substring(1);
            return toUpperCase(firstChar) + toLowerCase(rest);
        }
    }

    /**
     * Formt alle Worte in der Eingabe so um, dass das erste Zeichen groß und der Rest des Wortes
     * klein ist.
     */
    public static String toAllWordsUpperThenLowerCase(String input) {
        if (input.isEmpty()) {
            return "";
        }

        List<String> parts = new ArrayList<>();
        for (String part : splitBy(input, "[ \\t]+")) {
            if (part.isEmpty()) {
                throw new RuntimeException("Es wurde an Whitespace getrennt, da darf kein "
                        + "leerer String auftauchen!");
            }
            part = toFirstUpperCase(part);
            parts.add(part);
        }

        return joinWithBlank(parts);
    }

    /** Entfernt aus einer Eingabe die Leerzeichen und macht CamelCase daraus. */
    public static String toCamelCase(String input) {
        if (input.isEmpty()) {
            return "";
        }

        List<String> parts = new ArrayList<>();
        for (String part : splitBy(input, "[_ \\t]+")) {
            if (part.isEmpty()) {
                throw new RuntimeException("Es wurde an Whitespace getrennt, da darf kein "
                        + "leerer String auftauchen!");
            }
            String firstChar = part.substring(0, 1);
            String rest = part.substring(1);
            part = firstChar.toUpperCase() + rest; // kein lowerCase beim Rest!
            parts.add(part);
        }

        StringBuilder builder = new StringBuilder();
        for (String part : parts) {
            builder.append(part);
        }

        return builder.toString();
    }

    /** Säubert einen String, so dass er als Sas-Variablennamen geeignet ist. */
    public static String cleanForSasVariableName(String input) {
        String output = input;

        output = output.replaceAll("[-!?.,#\"'\\d;:/\\\\]", "_");
        output = replaceUmlauts(output);
        output = output.strip();
        output = output.replace(" ", "_");
        output = output.replaceAll("__+", "_");
        output = output.replaceAll("^_+", "");
        output = output.replaceAll("_+$", "");

        /* Sas-Dataset-Variablen dürfen nur 32 Zeichen lang sein: */
        if (output.length() > 32) {
            output = output.substring(0, 32);
        }

        return output;
    }

    /**
     * Trimmt den String und ersetzt alle '/' durch '\'.
     *
     * @param input
     *            Eingabe.
     * @return Umgesetzte Ausgabe
     */
    public static String nicePath(String input) {
        String output = input.strip();
        output = allSlashesToBackslashes(output);
        return output;
    }

    /**
     * Trimmt alle String aus der Liste und ersetzt bei allen Strings alle '/' durch '\'.
     *
     * @param list
     *            Liste mit den Eingaben.
     * @return Liste mit den umgesetzten Ausgaben.
     */
    public static List<String> nicePathes(List<String> list) {
        List<String> outputPathes = new ArrayList<>();

        for (String input : list) {
            String output = nicePath(input);
            outputPathes.add(output);
        }

        return outputPathes;
    }

    /**
     * Entfernt aus einem Link, etwa einem zum String gewandelten URL, alle ggf. vorhandenen Teile
     * am Ende des Links, die auf einen bestimmten Abschnitt auf der davor angegebenen Seite
     * verweisen.
     *
     * Aus "hilfe.html#why" wird damit "hilfe.html".
     *
     * @param input
     *            Vollständiger Link, ggf. mit "#" Teil.
     * @return Link ohne "#" Teil.
     */
    public static String skipSublinkPart(String input) {
        int hashPosition = input.indexOf("#");
        if (hashPosition == -1) {
            return input;
        }
        else {
            return input.substring(0, hashPosition);
        }
    }

    private static final Pattern CONTAINES_ONLY_NORMAL_LETTERS_PATTERN = Pattern.compile(
            "[A-Za-z]+");

    /**
     * Überprüft, ob der gegebene String nur aus Groß- und Kleinbuchstaben ohne Umlauten und
     * anderen Sonderzeichen besteht.
     */
    public static boolean containsOnlyNormalLetters(String input) {
        Matcher matcher = CONTAINES_ONLY_NORMAL_LETTERS_PATTERN.matcher(input);
        return matcher.matches();
    }

    private static final Pattern CONTAINES_ONLY_NORMAL_LETTERS_OR_DIGITS_PATTERN = Pattern.compile(
            "[A-Za-z0-9]+");

    /**
     * Überprüft, ob der gegebene String nur aus Groß- und Kleinbuchstaben ohne Umlauten und
     * anderen Sonderzeichen besteht.
     */
    public static boolean containsOnlyNormalLettersOrDigits(String input) {
        Matcher matcher = CONTAINES_ONLY_NORMAL_LETTERS_OR_DIGITS_PATTERN.matcher(input);
        return matcher.matches();
    }

    private static final Pattern CONTAINES_ONLY_WORD_CHARS_PATTERN = Pattern.compile(
            MIXED + "+");

    /** Überprüft, ob der gegebene String nur aus Groß- und Kleinbuchstaben mit Umlauten besteht. */
    public static boolean containsOnlyWordChars(String input) {
        Matcher matcher = CONTAINES_ONLY_WORD_CHARS_PATTERN.matcher(input);
        return matcher.matches();
    }

    /** Ersetzt Tabulatoren durch vier Leerzeichen. */
    public static String tabToSpace(String line) {
        return line.replace("\t", "    ");
    }

    /**
     * Extrahiert die in einfache Quotes gehüllten Parameter in einem konstruktoraritgem
     * Klammerausdruck.
     *
     * @param input
     *            Klammerausdruck der Art
     *            MM_openBrWindow('impressum.html','PopUp','scrollbars=yes,width=560,height=565').
     */
    public static List<String> extractQuotetBraceParameters(String input) {
        List<Integer> openingBracePositions = findAllPositions("(", input);
        Assure.hasOneElement(openingBracePositions);
        List<Integer> closingBracePositions = findAllPositions(")", input);
        Assure.hasOneElement(closingBracePositions);
        int openIndex = openingBracePositions.get(0);
        int closeIndex = closingBracePositions.get(0);
        Assure.isAtLeast(closeIndex, openIndex);
        String inner = input.substring(openIndex + 1, closeIndex);
        if (inner.startsWith("'") && inner.endsWith("'")) {
            inner = inner.substring(1, inner.length() - 1);
            List<String> parts = splitBy(inner, "' *, *'");
            return parts;
        }
        else {
            throw new IllegalArgumentException("Der Text in der Klammer muss mit einem einfachen "
                    + "Quote beginnen und aufhören.\n\t"
                    + "input = [" + input + "]"
                    + "inner = [" + inner + "]\n\t");
        }
    }

    /**
     * Entfernt den gegebene Anfang aus dem gegebenen Text, falls dieser wirklich damit anfängt.
     *
     * @param textToChange
     *            Text, dessen Anfang ggf. entfernt werden soll.
     * @param frontToRemove
     *            Anfang, der entfernt werden soll.
     * @return Text, ggf. ohne den Anfang.
     */
    public static String removeTextAtFrontIfStartsWith(String textToChange, String frontToRemove) {
        if (textToChange.startsWith(frontToRemove)) {
            return textToChange.substring(frontToRemove.length());
        }
        else {
            return textToChange;
        }
    }

    /**
     * Entfernt den gegebenen Anfang aus dem gegebenen Text, falls dieser wirklich damit beginnt.
     *
     * @param textToChange
     *            Text, dessen Anfang ggf. entfernt werden soll.
     * @param frontToRemove
     *            Anfang, der entfernt werden soll.
     * @return Text, ggf. ohne den Anfang.
     */
    public static String removeTextAtStartIfStartsWith(String textToChange, String frontToRemove) {
        return removeTextAtFrontIfStartsWith(textToChange, frontToRemove);
    }

    /**
     * Entfernt solange alle gegebenen Anfänge sowie Leerzeichen aus dem Anfang des gegebenen
     * Text, falls dieser wirklich damit anfängt, bis er nich nicht mehr verändert.
     *
     * @param textToChange
     *            Text, dessen Anfang ggf. entfernt werden soll.
     * @param frontsToRemove
     *            Anfänge, die entfernt werden soll.
     * @return Text, ggf. ohne den Anfang.
     */
    public static String removeTextAtFrontIfStartsWith(String textToChange,
            List<String> frontsToRemove) {
        List<String> toRemoveList = new ArrayList<>();
        toRemoveList.add(" ");
        toRemoveList.addAll(frontsToRemove);

        boolean changing = true;
        String output = textToChange;

        while (changing) {
            String old = output;

            for (String toRemove : toRemoveList) {
                output = removeTextAtFrontIfStartsWith(output, toRemove);
            }

            changing = !old.equals(output);
        }

        return output;
    }

    /**
     * Entfernt das gegebene Ende aus dem gegebenen Text, falls dieser wirklich damit endet.
     *
     * @param textToChange
     *            Text, dessen Ende ggf. entfernt werden soll.
     * @param rearToRemove
     *            Ende, das entfernt werden soll.
     * @return Text, ggf. ohne das Ende.
     */
    public static String removeTextAtEndIfEndsWith(String textToChange, String rearToRemove) {
        if (textToChange.endsWith(rearToRemove)) {
            return textToChange.substring(0, textToChange.length() - rearToRemove.length());
        }
        else {
            return textToChange;
        }
    }

    /**
     * Entfernt einen Zeilenumbruch am Ende des übergebenen Textes, falls der Text auf einen
     * solchen endet.
     *
     * @param textToChange
     *            Text, dessen Zeilenumbruch am Ende ggf. entfernt werden soll.
     * @return Text, ggf. ohne Zeilenumbruch am Ende.
     */
    public static String removeLineBreakAtEndIfEndsWith(String textToChange) {
        String text = removeTextAtEndIfEndsWith(textToChange, LINE_BREAK);
        if (text.equals(textToChange)) {
            text = removeTextAtEndIfEndsWith(text, "\n");
        }
        return text;
    }

    /**
     * Entfernt den Teil aus dem Text, der vor dem Suchtext steht, sofern der Suchtext im Text
     * vorkommt.
     *
     * @param text
     *            Text in dem etwas vorn gelöscht werden soll.
     * @param searchText
     *            Suchtext, vor diesem soll alles im Text entfernt werden.
     * @return Ggf. veränderten Text.
     */
    public static String removePartBeforeSearchText(String text, String searchText) {
        int index = text.indexOf(searchText);
        if (index > -1) {
            return text.substring(index);
        }
        else {
            return text;
        }
    }

    /**
     * Entfernt den Teil aus dem Text, der vor dem Suchtext steht, sofern der Suchtext im Text
     * vorkommt, inklusive des gesuchten Strings.
     *
     * @param text
     *            Text in dem etwas vorn gelöscht werden soll.
     * @param searchText
     *            Suchtext, vor diesem soll alles im Text entfernt werden.
     * @return Ggf. veränderten Text.
     */
    public static String removePartBeforeSearchTextIncludingSearchText(String text,
            String searchText) {
        int index = text.indexOf(searchText);
        if (index > -1) {
            return text.substring(index + searchText.length());
        }
        else {
            return text;
        }
    }

    /**
     * Entfernt den Teil aus dem Text, der vor dem letzten Auftreten des Suchtextes steht, sofern
     * der Suchtext im Text vorkommt.
     *
     * @param text
     *            Text in dem etwas vorn gelöscht werden soll.
     * @param searchText
     *            Suchtext, vor diesem soll alles im Text entfernt werden.
     * @return Ggf. veränderten Text.
     */
    public static String removePartBeforeLastSearchText(String text, String searchText) {
        int index = text.lastIndexOf(searchText);
        if (index > -1) {
            return text.substring(index);
        }
        else {
            return text;
        }
    }

    /**
     * Entfernt den Teil aus dem Text, der vor dem letzten Auftreten des Suchtextes steht, sofern
     * der Suchtext im Text vorkommt, inklusive des gesuchten Strings.
     *
     * @param text
     *            Text in dem etwas vorn gelöscht werden soll.
     * @param searchText
     *            Suchtext, vor diesem soll alles im Text entfernt werden.
     * @return Ggf. veränderten Text.
     */
    public static String removePartBeforeLastSearchTextIncludingSearchText(String text,
            String searchText) {
        int index = text.lastIndexOf(searchText);
        if (index > -1) {
            return text.substring(index + searchText.length());
        }
        else {
            return text;
        }
    }

    /** Vereinheitlicht alle Zeilenumbrüche im Text. */
    public static String unifyLineBreaks(String text) {
        String editedText = text;

        editedText = editedText.replace(LINE_BREAK, LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace("\r\n", LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace("\n", LINE_BREAK_PLACE_HOLDER);
        editedText = editedText.replace("\r", LINE_BREAK_PLACE_HOLDER);

        editedText = editedText.replace(LINE_BREAK_PLACE_HOLDER, LINE_BREAK);

        return editedText;
    }

    /** Entfernt alle Leerzeilen aus dem Text. Auch solche mit Leerzeichen darin. */
    public static String removeEmptyLines(String text) {
        String editedText = unifyLineBreaks(text);

        /* Mitte: */
        editedText = editedText.replaceAll(
                "(?:" + LINE_BREAK + " *)+" + LINE_BREAK,
                LINE_BREAK);

        /* Vorn: */
        editedText = editedText.replaceAll(
                "^(?: *" + LINE_BREAK + ")+",
                "");

        /* Hinten: */
        editedText = editedText.replaceAll(
                "(?:" + LINE_BREAK + " *)+$",
                "");

        return editedText;
    }

    /**
     * Entfernt alle Leerzeilen aus dem Text, die vor der ersten Zeile mit echtem Inhalt und nach
     * der letzten Zeile mit echtem Inhalt kommen. Auch solche mit Leerzeichen darin.
     */
    public static String removeEmptyLinesAtFrontAndEnd(String text) {
        List<String> lines = splitByLineBreaks(text);

        int firstFilledLineIndex = findFirstFilledLineIndex(lines);
        if (firstFilledLineIndex == -1) {
            return "";
        }
        int lastFilledLineIndex = findLastFilledLineIndex(lines);

        List<String> goodLines = new ArrayList<>();
        for (int index = firstFilledLineIndex; index <= lastFilledLineIndex; ++index) {
            goodLines.add(lines.get(index));
        }

        return joinWithLineBreak(goodLines);
    }

    private static int findFirstFilledLineIndex(List<String> lines) {
        for (int index = 0; index < lines.size(); ++index) {
            String line = lines.get(index);
            if (!line.strip().isEmpty()) {
                return index;
            }
        }

        return -1;
    }

    private static int findLastFilledLineIndex(List<String> lines) {
        for (int index = lines.size() - 1; index >= 0; --index) {
            String line = lines.get(index);
            if (!line.strip().isEmpty()) {
                return index;
            }
        }

        return -1;
    }

    /** Entfernt Zeilen aus dem Text, die nicht länger als die angegebene Länge sind. */
    public static String removeShortLines(String text, int maxLegthToRemove) {
        String editedText = unifyLineBreaks(text);

        //String noLineBreakCharacters = "[^" + LINE_BREAK + "]{," + maxLegthToRemove + "}";
        String noLineBreakCharacters = "[^\\r\\n]{1," + maxLegthToRemove + "}";

        /* Mitte: */
        editedText = editedText.replaceAll(
                "(?:" + LINE_BREAK + noLineBreakCharacters + ")+" + LINE_BREAK,
                LINE_BREAK);

        /* Vorn: */
        editedText = editedText.replaceAll(
                "^(?:" + noLineBreakCharacters + LINE_BREAK + ")+",
                "");

        /* Hinten: */
        editedText = editedText.replaceAll(
                "(?:" + LINE_BREAK + noLineBreakCharacters + ")+$",
                "");

        return editedText;
    }

    public static String loremImpsum() {
        return "Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor "
                + "incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis "
                + "nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. "
                + "Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat "
                + "nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa "
                + "qui officia deserunt mollit anim id est laborum." + LINE_BREAK
                + LINE_BREAK
                + "--" + LINE_BREAK
                + LINE_BREAK
                + "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie "
                + "consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan "
                + "et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis "
                + "dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer "
                + "adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore "
                + "magna aliquam erat volutpat."
                + LINE_BREAK
                + "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit "
                + "lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure "
                + "dolor in hendrerit in vulputate velit esse molestie consequat, vel illum "
                + "dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio "
                + "dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te "
                + "feugait nulla facilisi."
                + LINE_BREAK
                + "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet "
                + "doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, "
                + "consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut "
                + "laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, "
                + "quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea "
                + "commodo consequat."
                + LINE_BREAK
                + "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie "
                + "consequat, vel illum dolore eu feugiat nulla facilisis."
                + LINE_BREAK
                + "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd "
                + "gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum "
                + "dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor "
                + "invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero "
                + "eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no "
                + "sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit "
                + "amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore "
                + "dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore "
                + "Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut "
                + "vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, "
                + "consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore "
                + "et dolore magna aliquyam erat."
                + LINE_BREAK
                + "Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore "
                + "et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et "
                + "justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata "
                + "sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur "
                + "sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore "
                + "magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo "
                + "dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est "
                + "Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing "
                + "elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna "
                + "aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores "
                + "et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem "
                + "ipsum dolor sit amet.";
        // Quelle: https://la.wikisource.org/wiki/Lorem_ipsum
    }

    /** Erzeugt eine Liste mit allen Zeichen aus dem String als Strings der Länge 1. */
    public static List<String> eachCharFromString(String text) {
        List<String> characters = new ArrayList<>();

        for (int i = 0; i < text.length(); ++i) {
            String character = text.substring(i, i + 1);
            characters.add(character);
        }

        return characters;
    }

    /** Liest einen UTF-8 kodierten InputStream in einen String ein. */
    public static String utf8InputStreamToString(InputStream inputStream) {
        return inputStreamToString(inputStream, Charset.UTF_8);
    }

    /** Liest einen kodierten InputStream in einen String ein. */
    public static String inputStreamToString(InputStream inputStream, Charset charset) {
        try {
            return tryToInputStreamToString(inputStream, charset);
        }
        catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }

    private static String tryToInputStreamToString(InputStream inputStream, Charset charset)
            throws IOException {
        final int bufferSize = 1024;
        final char[] buffer = new char[bufferSize];
        final StringBuilder out = new StringBuilder();
        Reader in = new InputStreamReader(inputStream, charset.getCharsetAsString());
        for (; ; ) {
            int rsz = in.read(buffer, 0, buffer.length);
            if (rsz < 0)
                break;
            out.append(buffer, 0, rsz);
        }
        return out.toString();
    }

    /**
     * Sucht die erste Position von den möglichen Fundstellen der übergebenen Suchbegriffe.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Text der durchsucht wird.
     * @param searchPhrases
     *            Suchbegriffe die im Text gesucht werden.
     * @return Erster gefundener Suchbegriff, wird keiner gefunden, so wird FoundSearch.NOT_FOUND
     *         zurückgegeben.
     */
    public static FoundSearch findFirstPosition(String text, List<String> searchPhrases) {
        return findFirstPosition(text, searchPhrases, 0);
    }

    /**
     * Sucht die erste Position von den möglichen Fundstellen der übergebenen Suchbegriffe ab dem
     * genannten Index.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Text der durchsucht wird.
     * @param searchPhrases
     *            Suchbegriffe die im Text gesucht werden.
     * @param fromIndex
     *            Kleinster Index, ab dem gesucht wird.
     * @return Erster gefundener Suchbegriff, wird keiner gefunden, so wird FoundSearch.NOT_FOUND
     *         zurückgegeben.
     */
    public static FoundSearch findFirstPosition(String text, List<String> searchPhrases,
            int fromIndex) {
        FoundSearch found = FoundSearch.NOT_FOUND;
        boolean firstTime = true;

        for (String searchPhrase : searchPhrases) {
            int searchPhraseIndex = text.indexOf(searchPhrase, fromIndex);
            int foundIndex = found.getIndex();
            if (searchPhraseIndex > -1 &&
                    (firstTime
                            || foundIndex > -1 && searchPhraseIndex < foundIndex)) {
                firstTime = false;
                found = new FoundSearch(searchPhraseIndex, searchPhrase);
            }
        }

        return found;
    }

    /**
     * Sucht die erste Position von den möglichen Fundstellen der übergebenen Suchbegriffe.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Text der durchsucht wird.
     * @param searchPhrases
     *            Suchbegriffe die im Text gesucht werden.
     * @param textAfterSearchNotStaringWith
     *            Der Begriff, mit dem der Text nach der Fundstelle nicht weitergehen darf.
     * @return Erster gefundener Suchbegriff, wird keiner gefunden, so wird FoundSearch.NOT_FOUND
     *         zurückgegeben.
     */
    public static FoundSearch findFirstPositionNotStartingWith(String text,
            List<String> searchPhrases, String textAfterSearchNotStaringWith) {
        return findFirstPositionNotStartingWith(text, searchPhrases, 0,
                textAfterSearchNotStaringWith);
    }

    /**
     * Sucht die erste Position von den möglichen Fundstellen der übergebenen Suchbegriffe ab dem
     * genannten Index.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Text der durchsucht wird.
     * @param searchPhrases
     *            Suchbegriffe die im Text gesucht werden.
     * @param fromIndex
     *            Kleinster Index, ab dem gesucht wird.
     * @param textAfterSearchNotStaringWith
     *            Der Begriff, mit dem der Text nach der Fundstelle nicht weitergehen darf.
     * @return Erster gefundener Suchbegriff, wird keiner gefunden, so wird FoundSearch.NOT_FOUND
     *         zurückgegeben.
     */
    public static FoundSearch findFirstPositionNotStartingWith(String text,
            List<String> searchPhrases, int fromIndex, String textAfterSearchNotStaringWith) {
        FoundSearch found = FoundSearch.NOT_FOUND;
        boolean firstTime = true;

        for (String searchPhrase : searchPhrases) {
            int searchPhraseIndex = text.indexOf(searchPhrase, fromIndex);
            int foundIndex = found.getIndex();
            if (searchPhraseIndex > -1 &&
                    (firstTime
                            || foundIndex > -1 && searchPhraseIndex < foundIndex)) {
                String rest = text.substring(searchPhraseIndex + searchPhrase.length());
                if (!rest.startsWith(textAfterSearchNotStaringWith)) {
                    firstTime = false;
                    found = new FoundSearch(searchPhraseIndex, searchPhrase);
                }
            }
        }

        return found;
    }

    /**
     * Ermittelt den Index und die gefundene Phrase, der letzten im Text gefundenen Phrase der
     * übergebenen Phrasen.
     *
     * Wird keiner der übergebenen Phrasen im Text gefunden, so wird FoundSearch.NOT_FOUND
     * zurückgegeben.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     */
    public static FoundSearch findLast(String text, List<String> searchPhrases) {
        FoundSearch found = FoundSearch.NOT_FOUND;

        for (String searchPhrase : searchPhrases) {
            int searchPhraseIndex = text.lastIndexOf(searchPhrase);
            if (found.getIndex() < searchPhraseIndex) {
                found = new FoundSearch(searchPhraseIndex, searchPhrase);
            }
        }

        return found;
    }

    /**
     * Ermittelt den Index und die gefundene Phrase, der letzten im Text gefundenen Phrase der
     * übergebenen Phrasen.
     *
     * Wird keiner der übergebenen Phrasen im Text gefunden, so wird FoundSearch.NOT_FOUND
     * zurückgegeben.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     * @param textAfterSearchNotStaringWith
     *            Der Begriff, mit dem der Text nach der Fundstelle nicht weitergehen darf.
     */
    public static FoundSearch findLastNotStartingWith(String text, List<String> searchPhrases,
            String textAfterSearchNotStaringWith) {
        return findLastNotStartingWith(text, searchPhrases,
                CollectionsHelper.buildListFrom(textAfterSearchNotStaringWith));
    }

    /**
     * Ermittelt den Index und die gefundene Phrase, der letzten im Text gefundenen Phrase der
     * übergebenen Phrasen, bei denen der Text danach auf einen der übergebenen Begriffe
     * weitergeht.
     *
     * Wird keiner der übergebenen Phrasen im Text gefunden, so wird FoundSearch.NOT_FOUND
     * zurückgegeben.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     * @param textAfterSearchNotStartingWithList
     *            Liste mit Begriffen, mit dem der Text nach der Fundstelle nicht weitergehen darf.
     */
    public static FoundSearch findLastNotStartingWith(String text, List<String> searchPhrases,
            List<String> textAfterSearchNotStartingWithList) {
        return findLastNotBeginningAndNotStartingWith(text, searchPhrases, new ArrayList<>(),
                textAfterSearchNotStartingWithList);
    }

    /**
     * Ermittelt den Index und die gefundene Phrase, der letzten im Text gefundenen Phrase der
     * übergebenen Phrasen, bei denen weder der Text vorher auf einen der übergebenen Begriffe
     * endet (textBeforeSearchNotEndingWithList), noch der Text danach auf einen der übergebenen
     * Begriffe weitergeht (textAfterSearchNotStartingWithList).
     *
     * Wird keiner der übergebenen Phrasen im Text gefunden, so wird FoundSearch.NOT_FOUND
     * zurückgegeben.
     *
     * Die Reihenfolge der Suchbegriffe spielt keine Rolle.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     * @param textBeforeSearchNotEndingWithList
     *            Liste mit Begriffen, auf die der Text vor der Fundstelle nicht enden darf.
     * @param textAfterSearchNotStartingWithList
     *            Liste mit Begriffen, mit dem der Text nach der Fundstelle nicht weitergehen darf.
     */
    public static FoundSearch findLastNotBeginningAndNotStartingWith(String text,
            List<String> searchPhrases, List<String> textBeforeSearchNotEndingWithList,
            List<String> textAfterSearchNotStartingWithList) {
        List<FoundSearch> allFoundSearches = new ArrayList<>();
        for (String searchPhrase : searchPhrases) {
            List<Integer> indices = findAllPositionsWithoutOverlapping(searchPhrase, text);
            for (int index : indices) {
                FoundSearch foundSearch = new FoundSearch(index, searchPhrase);
                allFoundSearches.add(foundSearch);
            }
        }
        FoundSearch.sortFoundSearchListReverseByEndAndIndex(allFoundSearches);

        for (FoundSearch foundSearch : allFoundSearches) {
            String searchPhrase = foundSearch.getSearch();
            int searchPhraseIndex = foundSearch.getIndex();
            boolean notWanted = false;
            String front = text.substring(0, searchPhraseIndex);
            for (String textBeforeSearchNotEndingWith : textBeforeSearchNotEndingWithList) {
                if (front.endsWith(textBeforeSearchNotEndingWith)) {
                    notWanted = true;
                    break;
                }
            }
            if (!notWanted) {
                String rest = text.substring(searchPhraseIndex + searchPhrase.length());
                for (String textAfterSearchNotStartingWith : textAfterSearchNotStartingWithList) {
                    if (rest.startsWith(textAfterSearchNotStartingWith)) {
                        notWanted = true;
                        break;
                    }
                }
            }
            if (!notWanted) {
                return foundSearch;
            }
        }

        return FoundSearch.NOT_FOUND;
    }

    /** Entfernt alle gefundenen Bereiche mit dem angegebenen Anfang und Ende. */
    public static String removeAllPartsWithStartAndEnd(String html, String start, String end) {
        String changed = html;

        boolean searchForComments = true;

        while (searchForComments) {
            int startIndex = changed.indexOf(start);
            if (startIndex == -1) {
                searchForComments = false;
            }
            else {
                int endIndex = changed.indexOf(end, startIndex + start.length());
                if (endIndex == -1) {
                    changed = changed.substring(0, startIndex);
                }
                else {
                    changed = changed.substring(0, startIndex)
                            + changed.substring(endIndex + end.length());
                }
            }
        }

        return changed;
    }

    /** Entfernt alle gefundenen Bereiche mit dem jeder möglichen Kombination aus Anfang und Ende. */
    public static String removeAllPartsWithStartsAndEnds(String html, List<String> starts,
            List<String> ends) {
        String changed = html;

        boolean searchForLinks = true;

        while (searchForLinks) {
            FoundSearch foundStart = findFirstPosition(changed, starts);
            int startIndex = foundStart.getIndex();
            if (startIndex == -1) {
                searchForLinks = false;
            }
            else {
                String start = foundStart.getSearch();
                FoundSearch foundEnd = findFirstPosition(changed, ends,
                        startIndex + start.length());
                int endIndex = foundEnd.getIndex();
                if (endIndex == -1) {
                    changed = changed.substring(0, startIndex);
                }
                else {
                    String end = foundEnd.getSearch();
                    changed = changed.substring(0, startIndex)
                            + changed.substring(endIndex + end.length());
                }
            }
        }

        return changed;
    }

    /**
     * Prüft, ob abgesehen von der Groß- und Kleinschreibung der übergebene Text mit dem
     * übergebenen Anfang beginnt.
     *
     * @param text
     *            Zu prüfender Text.
     * @param start
     *            Anfang mit dem der Text beginnen soll.
     * @return Wahrheitswert, true genau dann, wenn der Text abgesehen von der Groß- und
     *         Kleinschreibung mit dem gegebenen Anfang beginnt.
     */
    public static boolean startsWithIgnoreCase(String text, String start) {
        String loweredText = toLowerCase(text);
        String loweredStart = toLowerCase(start);
        return loweredText.startsWith(loweredStart);
    }

    /** Entfernt Kommentare aus dem Text. */
    public static String removeSlashStarComments(String line) {
        return removeAllPartsWithStartAndEnd(line, "/*", "*/");
    }

    /** Entfernt Leerzeichen aus dem Text. */
    public static String removeSpaces(String input) {
        return input.replace(" ", "");
    }                                                           // siehe auch: trimAndCompactSpaces

    /**
     * Prüft, ob die angegebenen Indices im Namen der Beginn und das Ende eines Wortes sind. Nicht
     * geprüft wird, ob ggf. noch Leerzeichen oder andere Wortgrenzen dazwischen liegen.
     */
    public static boolean isStartAndEndOfWord(int startIndex, int endIndex, String name) {
        return isStartOfWord(startIndex, name) && isEndOfWord(endIndex, name);
    }

    /** Prüft, ob der angegebene Index im Namen der Beginn eines Wortes ist. */
    public static boolean isStartOfWord(int index, String name) {
        if (index < 0 || index >= name.length()) {
            return false;
        }
        else if (index == 0) {
            return true;
        }
        else {
            String charBefore = name.substring(index - 1, index);
            return !MIXED_CHARS.contains(charBefore);
        }
    }

    /** Prüft, ob der angegebene Index im Namen nach dem Ende eines Wortes ist. */
    public static boolean isEndOfWord(int index, String name) {
        if (index <= 0 || index > name.length()) {
            return false;
        }
        else if (index == name.length()) {
            return true;
        }
        else {
            String charBehind = name.substring(index, index + 1);
            return !MIXED_CHARS.contains(charBehind);
        }
    }

    /** Versieht Anführungszeichen mit einem Backlash. */
    public static String escapeDoubleQuotes(String input) {
        return input.replace("\"", "\\\"");
    }

    private static final Pattern IS_SUPPER_CASE_PATTERN = Pattern.compile(NO_SMALL + "*");

    /** Prüft, ob der übergebene Text komplett großgeschrieben ist. */
    public static boolean isUpperCase(String input) {
        Matcher matcher = IS_SUPPER_CASE_PATTERN.matcher(input);
        return matcher.matches();
    }

    /** Prüft, ob der übergebene Text im CamelCase vorliegt. */
    public static boolean isCamelCase(String input) {
        Matcher matcher = CAMEL_CASE_PATTERN.matcher(input);
        return matcher.matches();
    }

    /**
     * Prüft, ob der übergebene Text im CamelCase mit Punkt am Ende vorliegt.
     *
     * Beispiele: Grund.Wert.Vermögen.
     *            Grund.Wert.Vermögen
     */
    public static boolean isCamelCaseWithDots(String input) {
        Matcher matcher = CAMEL_CASE_PATTERN_WITH_DOTS.matcher(input);
        return matcher.matches();
    }

    /** Gibt den ersten Buchstaben (das erste Zeichen) in Kleinbuchstaben gewandelt zurück. */
    public static String firstLoweredChar(String input) {
        if (input.isEmpty()) {
            return "";
        }

        String firstCharacter = input.substring(0, 1);
        return toLowerCase(firstCharacter);
    }

    /** Wandelt den ersten Buchstaben (das erste Zeichen) in Kleinbuchstaben um. */
    public static String firstCharToLowerCase(String input) {
        if (input.isEmpty()) {
            return "";
        }
        else {
            String firstCharacter = firstLoweredChar(input);
            String rest = input.substring(1);
            return firstCharacter + rest;
        }
    }

    /** Gibt die Liste der Buchstaben des übergebenen Wortes zurück. */
    public static List<String> wordToLetters(String word) {
        List<String> letters = new ArrayList<>();

        for (int index = 0; index < word.length(); ++index) {
            String letter = word.substring(index, index + 1);
            letters.add(letter);
        }

        return letters;
    }

    /** Prüft, ob der übergebene String von doppelten Anführungszeichen umgeben ist. */
    public static boolean isQuoted(String input) {
        return input.startsWith("\"") && input.endsWith("\"");
    }

    /**
     * Entfernt die doppelten Anführungszeichen am Anfang und am Ende.
     *
     * Fehlen diese, wird eine Ausnahme geworfen.
     */
    public static String deQuoteQuotedString(String input) {
        if (!Text.isQuoted(input)) {
            throw new IllegalArgumentException("Die Eingabe '" + input
                    + "' beginnt oder endet nicht mit doppeltem Anführungszeichen.");
        }
        return input.substring(1, input.length() - 1);
    }

    /** Entfernt die doppelten Anführungszeichen am Anfang und am Ende, falls vorhanden. */
    public static String deQuoteQuotedStringIfQuoted(String input) {
        if (Text.isQuoted(input)) {
            return deQuoteQuotedString(input);
        }
        else {
            return input;
        }
    }

    /** Prüft, ob der übergebene String von einfachen Anführungszeichen umgeben ist. */
    public static boolean isSingleQuoted(String input) {
        return input.startsWith("'") && input.endsWith("'");
    }

    /**
     * Entfernt die einfachen Anführungszeichen am Anfang und am Ende.
     *
     * Fehlen diese, wird eine Ausnahme geworfen.
     */
    public static String deSingleQuoteQuotedString(String input) {
        if (!Text.isSingleQuoted(input)) {
            throw new IllegalArgumentException("Die Eingabe '" + input
                    + "' beginnt oder endet nicht mit einfachem Anführungszeichen.");
        }
        return input.substring(1, input.length() - 1);
    }

    /** Prüft, ob die Eingabe ausschließlich aus kleingeschriebenen Buchstaben besteht. */
    public static boolean containsOnlySmallLetters(String input) {
        if (input.isEmpty()) {
            return false;
        }

        for (int index = 0; index < input.length(); ++index ) {
            String charakter = input.substring(index, index + 1);
            if (!SMALL_CHARS.contains(charakter)) {
                return false;
            }
        }

        return true;
    }

    /** Prüft, ob das erste Zeichen des Textes ein kleingeschriebenes Zeichen ist. */
    public static boolean startsWithLowercaseLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }

        String firstCharakter = input.substring(0, 1);
        return SMALL_CHARS.contains(firstCharakter);
    }

    /** Prüft, ob das erste Zeichen des Textes ein großgeschriebenes Zeichen ist. */
    public static boolean startsWithUppercaseLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }

        String firstCharakter = input.substring(0, 1);
        return BIG_CHARS.contains(firstCharakter);
    }

    /** Prüft, ob das letzte Zeichen des Textes ein großgeschriebenes Zeichen ist. */
    public static boolean endsWithUppercaseLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }

        String lastCharakter = input.substring(input.length() - 1, input.length());
        return BIG_CHARS.contains(lastCharakter);
    }

    /** Prüft, ob das erste Zeichen des Textes eine Ziffer ist. */
    public static boolean startsWithDigit(String input) {
        if (input.isEmpty()) {
            return false;
        }

        String firstCharakter = input.substring(0, 1);
        return DIGITS.contains(firstCharakter);
    }

    /** Gibt an, ob das Zeichen vor dem (ersten) Vorkommen des Teils in der Eingabe leer ist. */
    public static boolean positionBeforePartIsEmpty(String input, String part) {
        if (input.isEmpty() || part.isEmpty()) {
            return false;
        }

        int index = input.indexOf(part);
        if (index == -1) {
            return false; // kann nicht passieren, falls part wirklich Teil von input ist.
        }

        if (index == 0) {  // falls Part am Anfang des Namens ist.
            return false;
        }

        String charBefore = input.substring(index - 1, index);
        return " ".equals(charBefore);
    }

    /** Wandelt den übergebenen Text in deutsche Großschreibweise. */
    public static String toUpperCase(String input) {
        return input.toUpperCase(Locale.GERMAN);
    }

    /** Wandelt den übergebenen Text in deutsche Kleinschreibweise. */
    public static String toLowerCase(String input) {
        return input.toLowerCase(Locale.GERMAN);
    }

    /** Wandelt alle übergebene Texte in deutsche Kleinschreibweise. */
    public static List<String> toLowerCase(List<String> inputs) {
        List<String> loweredInputs = new ArrayList<>();

        for (String input : inputs) {
            loweredInputs.add(toLowerCase(input));
        }

        return loweredInputs;
    }

    /** Prüft ob der übergebene Text mit einem der übergebenen Anfänge beginnt. */
    public static boolean startsWith(String text, List<String> possibleStarts) {
        for (String possibleStart : possibleStarts) {
            if (text.startsWith(possibleStart)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Prüft ob der übergebene Text mit dem übergebenen Anfang beginnt und dahinter kein weiterer
     * Buchstabe folgt. Ein Leerzeichen, eine Zahl ein Bindestrich, all das ist hingegen erlaubt.
     *
     * @param text
     *            Der Text mit dem möglicherweise enthaltenen Anfang.
     * @param possibleStartOfText
     *            Der fragliche Anfang des Textes.
     */
    public static boolean startsWithCompleteWord(String text, String possibleStartOfText) {
        if (text.startsWith(possibleStartOfText)) {
            if (text.length() == possibleStartOfText.length()) {
                return true;
            }
            String charBehind = text.substring(possibleStartOfText.length(),
                    possibleStartOfText.length() + 1);
            return !MIXED_CHARS.contains(charBehind);
        }
        else {
            return false;
        }
    }

    /**
     * Prüft ob der übergebene Text das übergebene Wort so enthält, dass davor und dahinter kein
     * weiterer Buchstabe folgt. Ein Leerzeichen, eine Zahl ein Bindestrich, all das ist hingegen
     * erlaubt.
     */
    public static int indexAsCompleteWord(String text, String perhapsContained) {
        int index = text.indexOf(perhapsContained);

        while (index > -1) {
            boolean frontOk;
            boolean rearOk;

            if (index == 0) {
                frontOk = true;
            }
            else {
                String charBefore = text.substring(index - 1, index);
                frontOk = !MIXED_CHARS.contains(charBefore);
            }

            if (text.length() == index + perhapsContained.length()) {
                rearOk = true;
            }
            else {
                String charBehind = text.substring(index + perhapsContained.length(),
                        index + perhapsContained.length() + 1);
                rearOk =  !MIXED_CHARS.contains(charBehind);
            }

            if (frontOk && rearOk) {
                return index;
            }

            index = text.indexOf(perhapsContained, index + 1);
        }

        return -1;
    }

    /** Prüft, ob der Text den übergebenen Teil so enthält, dass er nicht darauf endet. */
    public static boolean containsBeforeEnd(String text, String part) {
        int index = text.indexOf(part);
        if (-1 == index) {
            return false;
        }
        else {
            return text.length() > index + part.length();
        }
    }

    /**
     * Sucht den Index der ersten Ziffer im Text. Ist keine Ziffer enthalten, so wird -1
     * zurückgegeben.
     */
    public static int findIndexOfFirstDigit(String text) {
        int minIndex = Integer.MAX_VALUE;

        for (String digit : DIGITS) {
            int index = text.indexOf(digit);
            if (index > -1 && index < minIndex) {
                minIndex = index;
            }
        }

        if (minIndex == Integer.MAX_VALUE) {
            return -1;
        }
        else {
            return minIndex;
        }
    }

    /**
     * Sucht den Index der letzten Ziffer im Text. Ist keine Ziffer enthalten, so wird -1
     * zurückgegeben.
     */
    public static int findIndexOfLastDigit(String text) {
        int maxIndex = -1;

        for (String digit : DIGITS) {
            int index = text.lastIndexOf(digit);
            if (index > -1 && index > maxIndex) {
                maxIndex = index;
            }
        }

        return maxIndex;
    }

    /** Stellt fest, ob der übergebene Text mit einer Ziffer endet. */
    public static boolean endsWithDigit(String text) {
        for (String digit : DIGITS) {
            if (text.endsWith(digit)) {
                return true;
            }
        }

        return false;
    }

    /** Stellt fest, ob der übergebene Text eine Ziffer enthält. */
    public static boolean containsDigit(String text) {
        for (String digit : DIGITS) {
            if (text.contains(digit)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gibt an, ob der übergebene String eine Ziffer ist. Dafür darf er natürlich nur ein Zeichen
     * lang sein.
     */
    public static boolean isDigit(String digit) {
        return DIGITS.contains(digit);
    }

    /**
     * Ermittelt den Index der ersten Ziffer einer zusammenhängenden Ziffernfolge am Ende des
     * Textes.
     */
    public static int indexOfFirstDigitInContinousDigitsAtEnd(String text) {
        int indexOfFirstDigitInContinousDigitsAtEnd = -1;

        for (int index = text.length() - 1; index >= 0; --index) {
            String characterAtIndex = text.substring(index, index + 1);
            if (isDigit(characterAtIndex)) {
                indexOfFirstDigitInContinousDigitsAtEnd = index;
            }
            else {
                break;
            }
        }

        return indexOfFirstDigitInContinousDigitsAtEnd;
    }

    /** Prüft, ob der übergebene Text ein Gemisch aus großen und kleinen Buchstaben ist. */
    public static boolean isMixedWord(String text) {
        String mixedRegex = MIXED + "+";
        String regex = "^" + mixedRegex + "$";
        return text.matches(regex);
    }

    private static final Pattern IS_SUBSTANTIVE_PATTERN = Pattern.compile(SUBSTANTIVE_REGEX);

    /** Prüft, ob der übergebene Text ein Substantiv ist. */
    public static boolean isSubstantive(String text) {
        Matcher matcher = IS_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.matches();
    }

    private static final Pattern IS_SUBSTANTIVE_HYPHEN_SUBSTANTIVE_PATTERN = Pattern.compile(
            SUBSTANTIVE_REGEX + "-" + SUBSTANTIVE_REGEX);

    /** Prüft, ob der übergebene Text "Substantiv-Substantiv" ist. */
    public static boolean isSubstantiveHyphenSubstantive(String text) {
        Matcher matcher = IS_SUBSTANTIVE_HYPHEN_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.matches();
    }

    private static final Pattern IS_SUBSTANTIVE_SPACE_SUBSTANTIVE_PATTERN = Pattern.compile(
            SUBSTANTIVE_REGEX + " " + SUBSTANTIVE_REGEX);

    /** Prüft, ob der übergebene Text "Substantiv Substantiv" ist. */
    public static boolean isSubstantiveSpaceSubstantive(String text) {
        Matcher matcher = IS_SUBSTANTIVE_SPACE_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.matches();
    }

    private static final Pattern IS_SUBSTANTIVE_AND_SUBSTANTIVE_PATTERN = Pattern
            .compile("(" + SUBSTANTIVE_REGEX + ") und (" + SUBSTANTIVE_REGEX + ")");

    public static final Pair<String> NO_SUBSTANTIVE_PAIR_FOUND = new Pair<>("", "");

    /**
     * Prüft, ob der übergebene Text ein Substantiv ist. Ist dies der Fall, werden die beiden
     * Substantive als Paar zurückgegeben. Anderenfalls wird das Paar NO_SUBSTANTIVE_PAIR_FOUND
     * zurückgegeben (zweimal der leere String).
     */
    public static Pair<String> isSubstantiveAndSubstantive(String text) {
        Matcher matcher = IS_SUBSTANTIVE_AND_SUBSTANTIVE_PATTERN.matcher(text);
        if (matcher.matches()) {
            String firstSubstantive = matcher.group(1);
            String secondSubstantive = matcher.group(2);
            return new Pair<>(firstSubstantive, secondSubstantive);
        }
        else {
            return NO_SUBSTANTIVE_PAIR_FOUND;
        }
    }

    /* Das klappt zusammen mit matcher.find() nicht, warum auch immer...
    private static final Pattern CONTAINES_SUBSTANTIVE_PATTERN = Pattern.compile(
            "\b" + SUBSTANTIVE_REGEX + "\b");
    WEIL MAN \\b SCHREIBEN MÜSSTE!
    */
    private static final Pattern CONTAINES_SUBSTANTIVE_PATTERN = Pattern.compile(
            "(?:^|.*" + DELIMITER_SYMBOLS_REGEX + ")"
                    + SUBSTANTIVE_REGEX
                    + "(?:" + DELIMITER_SYMBOLS_REGEX + ".*|$)");

    /** Prüft, ob im übergebenen Text ein Substantiv vorkommt. */
    public static boolean containesSubstantive(String text) {
        Matcher matcher = CONTAINES_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.matches();
    }

    private static final Pattern STARTS_WITH_SUBSTANTIVE_PATTERN = Pattern.compile(
            "^"
                    + "(" + SUBSTANTIVE_REGEX + ")"
                    + "(?:" + DELIMITER_SYMBOLS_REGEX + ".*|$)");
    /* Klappt nicht, warum auch immer...
    private static final Pattern STARTS_WITH_SUBSTANTIVE_PATTERN = Pattern.compile(
            "^"
                    + SUBSTANTIVE_REGEX
                    + "\b.*");
    vielleicht hätte es \\b sein müssen? */

    /** Prüft, ob der übergebene Text mit einem Substantiv beginnt. */
    public static boolean startsWithSubstantive(String text) {
        Matcher matcher = STARTS_WITH_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.find();
    }

    private static final Pattern STARTS_WITH_SUBSTANTIVE_HYPHEN_SUBSTANTIVE_PATTERN =
            Pattern.compile("^"
                    + "(" + SUBSTANTIVE_REGEX + "-" + SUBSTANTIVE_REGEX + ")"
                    + "(?:" + DELIMITER_SYMBOLS_REGEX + ".*|$)");

    /** Prüft, ob der übergebene Text mit Substantiv-Substantiv beginnt. */
    public static boolean startsWithSubstantiveHyphenSubstantive(String text) {
        Matcher matcher = STARTS_WITH_SUBSTANTIVE_HYPHEN_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.find();
    }

    private static final Pattern STARTS_WITH_SUBSTANTIVE_WITH_UMBRUCH_PATTERN = Pattern.compile(
            "^"
                    + "(" + BIG + SMALL_WORD_REGEX + "- " + SMALL_WORD_REGEX + ")"
                    + "(?:" + DELIMITER_SYMBOLS_REGEX + ".*|$)");

    /** Prüft, ob der übergebene Text mit einem Substantiv mit einem Umbruch darin beginnt. */
    public static boolean startsWithSubstantiveWithUmbruch(String text) {
        Matcher matcher = STARTS_WITH_SUBSTANTIVE_WITH_UMBRUCH_PATTERN.matcher(text);
        return matcher.find();
    }

    /**
     * Gibt das Substantiv zurück, mit dem der übergebene Text beginnt. Tut er das nicht, wird ein
     * leerer String zurückgegeben.
     */
    public static String determineStartingSubstantive(String text) {
        Matcher matcher = STARTS_WITH_SUBSTANTIVE_PATTERN.matcher(text);
        if (matcher.find()) {
            return matcher.group(1);
        }
        else {
            return "";
        }
    }

    /**
     * Gibt das Substantiv-Substantive zurück, mit dem der übergebene Text beginnt. Tut er das
     * nicht, wird ein leerer String zurückgegeben.
     */
    public static String determineStartingSubstantiveHyphenSubstantive(String text) {
        Matcher matcher = STARTS_WITH_SUBSTANTIVE_HYPHEN_SUBSTANTIVE_PATTERN.matcher(text);
        if (matcher.find()) {
            return matcher.group(1);
        }
        else {
            return "";
        }
    }

    /**
     * Gibt das Substantiv mit Umbruch zurück, mit dem der übergebene Text beginnt. Tut er das
     * nicht, wird ein leerer String zurückgegeben.
     */
    public static String determineStartingSubstantiveWithUmbruch(String text) {
        Matcher matcher = STARTS_WITH_SUBSTANTIVE_WITH_UMBRUCH_PATTERN.matcher(text);
        if (matcher.find()) {
            return matcher.group(1);
        }
        else {
            return "";
        }
    }

    private static final Pattern ENDS_WITH_SUBSTANTIVE_PATTERN = Pattern.compile(
            "(?:^|" + DELIMITER_SYMBOLS_REGEX + ")"
                    + "(" + SUBSTANTIVE_REGEX + ")"
                    + "$"
                    );

    /**
     * Gibt das Substantiv zurück, mit dem der übergebene Text endet. Tut er das nicht, wird ein
     * leerer String zurückgegeben.
     */
    public static String determineEndingSubstantive(String text) {
        Matcher matcher = ENDS_WITH_SUBSTANTIVE_PATTERN.matcher(text);
        if (matcher.find()) {
            return matcher.group(1);
        }
        else {
            return "";
        }
    }

    private static final Pattern ENDS_WITH_SUBSTANTIVE_HYPHEN_PATTERN = Pattern.compile(
            "(?:^|.*" + DELIMITER_SYMBOLS_REGEX + ")"
                    + SUBSTANTIVE_REGEX + "-$");

    /** Prüft, ob der übergebene Text auf ein Substantiv und anschließenden Bindestrich endet. */
    public static boolean endsWithSubstantiveHyphen(String text) {
        Matcher matcher = ENDS_WITH_SUBSTANTIVE_HYPHEN_PATTERN.matcher(text);
        return matcher.find();
    }

    private static final Pattern ENDS_WITH_COMMA_SUBSTANTIVE_PATTERN = Pattern.compile(
            ", " + SUBSTANTIVE_REGEX + "$");

    /** Prüft, ob der übergebene Text auf Komma, Leerzeichen und Substantiv endet. */
    public static boolean endsWithCommaSubstantive(String text) {
        Matcher matcher = ENDS_WITH_COMMA_SUBSTANTIVE_PATTERN.matcher(text);
        return matcher.find();
    }

    /**
     * Sucht den Index der ersten Ziffer im Text, wobei nach der Ziffer kein Substantiv mehr folgen
     * darf.
     *
     * Ist keine Ziffer enthalten, so wird -1 zurückgegeben.
     */
    public static int findIndexOfFirstDigitAfterLastSubstantive(String text) {
        int minIndex = Integer.MAX_VALUE;

        for (String digit : DIGITS) {
            for (int index : Text.findAllPositions(digit, text)) {
                if (index > -1
                        && index < minIndex
                        && !containesSubstantive(text.substring(index))) {
                    minIndex = index;
                }
            }
        }

        if (minIndex == Integer.MAX_VALUE) {
            return -1;
        }
        else {
            return minIndex;
        }
    }

    /**
     * Sucht den Index des ersten Buchstabens im Text.
     *
     * Ist kein Buchstaben enthalten, so wird -1 zurückgegeben.
     */
    public static int findIndexOfFirstWordLetter(String text) {
        int minIndex = Integer.MAX_VALUE;

        for (String letter : MIXED_CHARS_LIST) {
            int index = text.indexOf(letter);
            if (index > -1 && index < minIndex) {
                minIndex = index;
            }
        }

        if (minIndex == Integer.MAX_VALUE) {
            return -1;
        }
        else {
            return minIndex;
        }
    }

    /**
     * Sucht den Index des letzten Nicht-Buchstabens vor dem angegebenen Index im Text.
     *
     * Ist kein solches Zeichen enthalten, so wird -1 zurückgegeben.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param startIndex
     *            Index vor dem gesucht werden soll.
     */
    public static int findIndexOfLastNonWordLetter(String text, int startIndex) {
        List<String> characters = CollectionsHelper.splitStringIntoLetters(text);
        int index = startIndex;
        boolean loop = true;

        while (loop) {
            --index;
            if (index < 0) {
                index = -1;
                loop = false;
            }
            else {
                String charakter = characters.get(index);
                if (!MIXED_CHARS.contains(charakter )) {
                    loop = false;
                }
            }
        }

        return index;
    }

    /**
     * Sucht den Index des ersten Nicht-Buchstabens im Text.
     *
     * Ist kein solches Zeichen enthalten, so wird -1 zurückgegeben.
     */
    public static int findIndexOfFirstNonWordLetter(String text) {
        int index = 0;
        for (String charakter : CollectionsHelper.splitStringIntoLetters(text)) {
            if (!MIXED_CHARS.contains(charakter)) {
                return index;
            }
            ++index;
        }

        return -1;
    }

    /**
     * Sucht den Index des Zeichens, das nicht das Leerzeichen ist, im Text.
     *
     * Ist kein solches Zeichen enthalten, so wird -1 zurückgegeben.
     */
    public static int findIndexOfFirstNonSpace(String text) {
        int index = 0;
        for (String charakter : CollectionsHelper.splitStringIntoLetters(text)) {
            if (!charakter.equals(" ")) {
                return index;
            }
            ++index;
        }

        return -1;
    }

    /** Rückt den übergebenen Text n mal ein. */
    public static String indent(String input, int times) {
        String output = input;

        for (int i = 0; i < times; ++i) {
            output = indent(output);
        }
        return output;
    }

    /** Rückt den übergebenen Text ab der zweiten Zeile n mal ein. */
    public static String indentAfterFirstLine(String input, int times) {
        String output = input;

        for (int i = 0; i < times; ++i) {
            output = indentAfterFirstLine(output);
        }
        return output;
    }

    /** Fügt an jeden Zeilenanfang vier Leerzeichen hinzu. */
    public static String indent(String input) {
        return indent(input, true);
    }

    /** Fügt an jeden Zeilenanfang vier Leerzeichen hinzu. */
    public static String indentAfterFirstLine(String input) {
        return indent(input, false);
    }

    private static String indent(String input, boolean indentFirstLine) {
        List<String> lines = splitByLineBreaks(input);
        List<String> outputLines = new ArrayList<>();

        boolean first = true;

        for (String line : lines) {
            String outputLine;
            if (first && !indentFirstLine) {
                outputLine = line;
            }
            else {
                outputLine = "    " + line;
            }
            outputLines.add(outputLine);
            first = false;
        }

        String output = joinWithLineBreak(outputLines);
        if (!input.contains(LINE_BREAK) && input.contains("\n")) {
            output = output.replace(LINE_BREAK, "\n");
        }

        return output;
    }

    /** Rückt den übergebenen Text n mal aus. */
    public static String unindent(String input, int times) {
        String output = input;

        for (int i = 0; i < times; ++i) {
            output = unindent(output);
        }
        return output;
    }

    /** Entfernt von jeden Zeilenanfang vier Leerzeichen, falls vorhanden. */
    public static String unindent(String input) {
        List<String> lines = splitByLineBreaks(input);
        List<String> outputLines = new ArrayList<>();

        for (String line : lines) {
            int beginIndex;
            if (line.startsWith("    ")) {
                beginIndex = 4;
            }
            else if (line.startsWith("   ")) {
                beginIndex = 3;
            }
            else if (line.startsWith("  ")) {
                beginIndex = 2;
            }
            else if (line.startsWith(" ")) {
                beginIndex = 1;
            }
            else {
                beginIndex = 0;
            }
            String outputLine = line.substring(beginIndex);
            outputLines.add(outputLine);
        }

        String output = joinWithLineBreak(outputLines);
        if (!input.contains(LINE_BREAK) && input.contains("\n")) {
            output = output.replace(LINE_BREAK, "\n");
        }

        return output;
    }

    /** Gibt die Eingabe gesperrt geschrieben zurück. */
    public static String insertLetterSpacing(String input) {
        List<String> letters = wordToLetters(input);
        return joinWithBlank(letters);
    }

    /** Bereinigt die Eingabe um Interpunktion und trimmed hinterher die Leerzeichen. */
    public static String cleanFromPunctuation(String input) {
        String output = input;

        List<String> toReplaceBySpace = CollectionsHelper.buildListFrom("\n", "\r", "\t");
        toReplaceBySpace.addAll(PUNCTUATION);
        for (String puncuation : toReplaceBySpace) {
            output = output.replace(puncuation, " ");
        }

        output = trimAndCompactSpaces(output);

        return output;
    }

    /**
     * Gibt das ganze Wort um den genannten Index herum zurück.
     *
     * @param text
     *            Text in dem gesucht wird.
     * @param index
     *            Index an dem gesucht wird.
     */
    public static String getWordAtIndex(String text, int index) {
        if (index < 0 || index >= text.length()) {
            throw new IllegalArgumentException("Der Index " + index
                    + " liegt nicht innerhalb des übergebenen Textes '" + text + "'.");
        }

        List<String> letter = CollectionsHelper.splitStringIntoLetters(text);
        String charAtIndex = letter.get(index);
        if (!MIXED_CHARS_LIST.contains(charAtIndex)) {
            return "";
        }

        /* search back to front: */
        int beginIndex = index;
        boolean goBack = true;
        while (goBack) {
            if (beginIndex >= 1) {
                String charBefore = letter.get(beginIndex - 1);
                if (MIXED_CHARS_LIST.contains(charBefore)) {
                    --beginIndex;
                }
                else {
                    goBack = false;
                }
            }
            else {
                goBack = false;
            }
        }

        /* search ahead to tail: */
        int indexOfLastChar = index;
        boolean goAhead = true;
        while (goAhead) {
            if (indexOfLastChar <= text.length() - 2) {
                String charBehind = letter.get(indexOfLastChar + 1);
                if (MIXED_CHARS_LIST.contains(charBehind)) {
                    ++indexOfLastChar;
                }
                else {
                    goAhead = false;
                }
            }
            else {
                goAhead = false;
            }
        }

        int endIndex = indexOfLastChar + 1;
        return text.substring(beginIndex, endIndex);
    }

    /**
     * Liefert alle (disjunkten) Worte im Text, welche als Teilstring den Suchbegriff enthalten.
     *
     * @param text
     *            Text in dem gesucht wird.
     * @param search
     *            Suchbegriff, zu dem alle ganzen Worte aus dem Text zurückgeliefert werden, die
     *            den Suchbegriff enthalten.
     */
    public static List<String> getDistinctWordsContaining(String text, String search) {
        List<String> fullWords = new ArrayList<>();

        for (Integer position : findAllPositions(search, text)) {
            if (position < text.length()) {
                String fullWord = getWordAtIndex(text, position);
                fullWords.add(fullWord);
            }
        }

        CollectionsHelper.makeListDisjunct(fullWords);
        /*
        if (!fullWords.isEmpty()) {
            System.out.println("getDistinctWordsContaining: " + search + " -> " + fullWords
                    + ", text = " + text);
        }
        */

        return fullWords;
    }

    /**
     * Gibt die letzten Zeichen des Textes zurück, oder den ganzen Text, falls dieser kürzer oder
     * genauso lang ist.
     *
     * @param text
     *            Text dessen Ende zurückgegeben werden soll.
     * @param numberOfChars
     *            Anzahl Zeichen die am Ende angezeigt werden sollen.
     */
    public static String lastChars(String text, int numberOfChars) {
        if (text.length() <= numberOfChars) {
            return text;
        }
        else {
            int startIndex = text.length() - numberOfChars;
            return "... " + text.substring(startIndex);
        }
    }

    /**
     * Gibt die letzten drei Zeichen der Eingabe zurück.
     *
     * Ist die Eingabe dafür nicht lang genug, wird eine Ausnahme geworfen.
     */
    public static String lastThreeChars(String input) {
        if (input.length() < 3) {
            throw new IllegalArgumentException("Der Input ist zu kurz! input = '" + input + "'!");
        }
        return input.substring(input.length() -3);
    }

    /**
     * Ermittelt den Index des zuletzt auftauchenden Zeichens.
     *
     * @param text
     *            Text der durchsucht wird.
     * @param searchedCharacters
     *            String mit den Zeichen, die gesucht werden.
     */
    public static int findIndexOfLastCharContained(String text, String searchedCharacters) {
        int index = text.length();

        boolean found = false;

        while (!found) {
            --index;
            if (index < 0) {
                return -1;
            }
            String charAtStartIndex = text.substring(index, index + 1);
            if  (searchedCharacters.contains(charAtStartIndex)) {
                found = true;
            }
        }

        return index;
    }

    /** Letztes Wort im Text ermitteln. */
    public static String extractLastWord(String text) {
        int endIndex = findIndexOfLastWordChar(text);
        if (-1 == endIndex) {
            return "";
        }
        ++endIndex; // muss ja nachher hinter dem letzten Zeichen sein.

        int startIndex = endIndex;

        while (startIndex > 0) {
            --startIndex;
            String charAtStartIndex = text.substring(startIndex, startIndex + 1);
            if  (!MIXED_CHARS.contains(charAtStartIndex)) {
                ++startIndex;
                break;
            }
        }

        return text.substring(startIndex, endIndex);
    }

    private static int findIndexOfLastWordChar(String text) {
        return findIndexOfLastCharContained(text, MIXED_CHARS);
    }

    /** Alles ab dem letzten Großbuchstaben im Text ermitteln. */
    public static String extractFromLastUpperLetter(String text) {
        int startIndex = findIndexOfLastUppercaseChar(text);
        if (-1 == startIndex) {
            return "";
        }
        int endIndex = findIndexOfLastWordChar(text);
        if (-1 == endIndex) {
            return "";
        }

        return text.substring(startIndex, endIndex + 1);
    }

    private static int findIndexOfLastUppercaseChar(String text) {
        return findIndexOfLastCharContained(text, BIG_CHARS);
    }

    /** Gibt den Unterschied zwei einzeiliger Texte aus. Leer bei Gleichheit. */
    public static String lineDifference(String line1, String line2) {
        int length1 = line1.length();
        int length2 = line2.length();

        String additionalMessage = "";
        if (length1 < length2) {
            additionalMessage = "Die erste Zeile ist kürzer: " + length1 + " < " + length2 + ".";
        }
        else if (length1 > length2) {
            additionalMessage = "Die erste Zeile ist länger: " + length1 + " < " + length2 + ".";
        }

        int length = Math.min(length1, length2);

        for (int index = 0; index < length; ++index) {
            String char1 = line1.substring(index, index + 1);
            String char2 = line2.substring(index, index + 1);

            if (!char1.equals(char2)) {
                return "Am Index " + index + " unterscheiden sich die beiden Zeilen:\n"
                        + "\t" + "Zeichen der ersten Zeile : '" + char1 + "'\n"
                        + "\t" + "Zeichen der zweiten Zeile: '" + char2 + "'"
                        //+ "\t" + "Zeile 1                  : " + line1 + "\n"
                        //+ "\t" + "Zeile 2                  : " + line2 + "\n"
                        + (additionalMessage.length() > 0 ? "\n" : "")
                        + additionalMessage;
            }
        }

        if (length == 0 && length1 != length2) {
            return additionalMessage;
        }
        else {
            return "";
        }
    }

    /**
     * Schneidet die Eingabe auf die angegebene Länge zurecht, falls sie länger ist.
     *
     * @param input
     *            Zu bearbeitende Eingabe.
     * @param length
     *            Maximale Länge für die Ausgabe.
     */
    public static String firstChars(String input, int length) {
        if (length < 0) {
            throw new IllegalArgumentException(
                    "Die Länge darf nicht negativ sein!\n\tlength = " + length + "\n");
        }

        if (input.length() > length) {
            return input.substring(0, length);
        }
        else {
            return input;
        }
    }

    /** Entfernt vorn und hinten bestimmte Satzzeichen und Leerraum. */
    public static String removePunctuationAtFrontAndEnd(String input) {
        String output = input;

        output = output.strip();
        for (String punctuationMark : PUNCTUATION_MARKS) {
            output = Text.removeTextAtEndIfEndsWith(output, punctuationMark).strip();
            output = Text.removeTextAtStartIfStartsWith(output, punctuationMark).strip();
        }

        if (input.equals(output)) {
            return output;
        }
        else {
            return removePunctuationAtFrontAndEnd(output);
        }
    }

    /** Ermittelt die Anzahl an Leerzeichen, mit der der übergebene String beginnt. */
    public static int countNumberOfSpacesAtBegin(String input) {
        int numberOfSpacesAtFront = 0;
        boolean search = true;
        while (search) {
            if (numberOfSpacesAtFront >= input.length()) {
                search = false;
            }
            else {
                String nextChar = input.substring(numberOfSpacesAtFront, numberOfSpacesAtFront + 1);
                if (nextChar.equals(" ")) {
                    ++numberOfSpacesAtFront;
                }
                else {
                    search = false;
                }
            }
        }

        return numberOfSpacesAtFront;
    }

    /**
     * Umgibt Pipezeichen, die Zeichen, welche kein Leerraum sind, davor und dahinter haben, mit
     * Leerzeichen.
     */
    public static String surroundSeparatingPipesWithBlanks(String input) {
        return input.replaceAll("(\\S)\\|(\\S)", "$1 \\| $2");
    }

    /** Umgibt Pipezeichen mit Leerzeichen. */
    public static String surroundPipesWithBlanks(String input) {
        String output = input;
        output = output.replaceAll("^\\|", " \\|");
        output = output.replaceAll("\\|$", "\\| ");
        output = output.replaceAll("\\|(?=\\|)", "\\| ");
        output = output.replaceAll("(\\S)\\|", "$1 \\|");
        output = output.replaceAll("\\|(\\S)", "\\| $1");
        output = output.replaceAll("(\\S)\\|(\\S)", "$1 \\| $2");
        return output;
    }

    /**
     * Wandelt Backslashes in Pfade in doppelte Backslashes um, damit die Ausgabe in Java-Code in
     * doppelten Anführungszeichen gültig ist.
     */
    public static String backslashToCodeForm(String input) {
        return input.replace("\\", "\\\\");
    }

    /**
     * Versteckt Anführungszeichen in Strings durch ein vorgestelltes Backslash, so dass man sie in
     * Quellcode verwenden kann, z.B. bei der Erzeugung von automatisiert erstelltem Testcode.
     *
     * @param input
     *            Zu bearbeitende Eingabe.
     * @return Eingabe mit versteckten Anführungszeichen.
     */
    public static String hideQuotesForSourceCode(String input) {
        return input.replace("\"", "\\\"");
    }

    /**
     * Sucht im übergebenen Text die übergebene Nummer in beliebigen mit Leerzeichen aufgefüllten
     * Varianten. Dabei werden anfänglich enthaltene Leerzeichen entfernt.
     *
     * @param number
     *            Zu suchende Nummer.
     * @param text
     *            Text der die Nummer vielleicht enthält.
     * @return Liste mit den Varianten der im Text gefundenen Nummern.
     */
    public static List<String> searchNumberWithDifferentSpacesInText(String number, String text) {
        List<String> allNumberVariations = determineNumbersWithDifferentSpaces(number);
        List<String> numberVariationsInText = new ArrayList<>();

        for (String numberVariation : allNumberVariations) {
            if (text.contains(numberVariation)) {
                numberVariationsInText.add(numberVariation);
            }
        }

        return numberVariationsInText;
    }

    /**
     * Erzeugt aus der übergebenen Nummer Varianten aufgefüllt mit Leerzeichen. Je nach der Länge
     * der Nummer werden unterschiedliche Verfahren dazu angewendet. Im Zweifelsfall wird eine
     * Liste mit einem Element, der originalen Nummer, zurückgegeben.
     *
     * @param number
     *            Zu bearbeitende Nummer.
     * @return Liste mit allen Varianten.
     */
    public static List<String> determineNumbersWithDifferentSpaces(String number) {
        if (number.length() < MAXIMAL_NUMBER_OF_PARTS_FOR_SPACE_VARIATIONS) {
            return determineNumberWithAllDifferentSpaces(number);
        }
        else {
            return determineNumbersWithDifferentSpacesBetweenDigitBorders(number);
        }
    }

    /**
     * Erzeugt aus der übergebenen Nummer alle beliebigen mit Leerzeichen aufgefüllten Varianten.
     * Dabei werden anfänglich enthaltene Leerzeichen entfernt.
     *
     * Achtung, da die mit der Länge der Nummer exponentiell wächst (2 ^ length), nur für kurze
     * Nummern verwenden!
     *
     * @param number
     *            Zu bearbeitende Nummer.
     * @return Liste mit allen Varianten.
     */
    public static List<String> determineNumberWithAllDifferentSpaces(String number) {
        String numberWithoutSpaces = removeSpaces(number);
        List<String> numberParts = wordToLetters(numberWithoutSpaces);
        return generateAllDifferentMixesOfPartsBySpaces(numberParts);
    }

    private static List<String> generateAllDifferentMixesOfPartsBySpaces(List<String> numberParts) {
        List<String> numberVariations = new ArrayList<>();
        for (String numberLetter : numberParts) {
            if (numberVariations.isEmpty()) {
                numberVariations.add(numberLetter);
            }
            else {
                List<String> copied = CollectionsHelper.copyList(numberVariations);
                numberVariations.clear();
                for (String numberPart : copied) {
                    numberVariations.add(numberPart + numberLetter);
                    numberVariations.add(numberPart + " " + numberLetter);
                }
            }
        }

        CollectionsHelper.makeListDisjunct(numberVariations);
        return numberVariations;
    }

    /**
     * Erzeugt aus der übergebenen Nummer mit Leerzeichen aufgefüllten Varianten, wobei zwischen
     * in der originalen Nummer zusammenstehenden Ziffern keine eingestreut werden.
     * Dabei werden andere anfänglich enthaltene Leerzeichen entfernt.
     *
     * Bei zu vielen Teilen wird nur eine Liste mit der originalen Nummer zurück gegeben.
     *
     * @param number
     *            Zu bearbeitende Nummer.
     * @return Liste mit Varianten.
     */
    public static List<String> determineNumbersWithDifferentSpacesBetweenDigitBorders(
            String number) {
        List<String> numberParts = splitAtDigitBorders(number);
        if (numberParts.size() < MAXIMAL_NUMBER_OF_PARTS_FOR_SPACE_VARIATIONS) {
            return generateAllDifferentMixesOfPartsBySpaces(numberParts);
        }
        else {
            return CollectionsHelper.buildListFrom(number);
        }
    }

    /**
     * Unterteilt eine Nummer wie z.B."4 542 IN 1031/15" an den Grenzen zwischen den Zahlen.
     *
     * @param number
     *            zu unterteilende Nummer
     * @return Liste mit den Teilen (im Beispiel "4", "542", "IN", "1031", "/" und "15").
     */
    public static List<String> splitAtDigitBorders(String number) {
        String cleandNumber = trimAndCompactSpaces(number);
        List<String> numberParts = new ArrayList<>();

        boolean atBorder = true;
        for (int index = 0; index < cleandNumber.length(); ++index) {
            String letter = cleandNumber.substring(index, index + 1);
            if (numberParts.isEmpty()) {
                numberParts.add(letter);
                atBorder = !isDigit(letter);
            }
            else {
                boolean atBorderBefore = atBorder;
                atBorder = !isDigit(letter);
                if (letter.equals(" ")) {
                    // nichts zu tun, wir sind damit schon bei atBorder = true
                }
                else if (atBorder || atBorderBefore) {
                    numberParts.add(letter);
                }
                else {
                    String lastPart = numberParts.get(numberParts.size() - 1);
                    numberParts.set(numberParts.size() - 1, lastPart + letter);
                }
            }
        }

        return numberParts;
    }

    /**
     * Ermittelt den Index und die gefundene Phrase, der längsten im Text gefundenen Phrase der
     * übergebenen Phrasen.
     *
     * Wird keiner der übergebenen Begriffe im Text gefunden, so wird FoundSearch.NOT_FOUND
     * zurückgegeben.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     */
    public static FoundSearch findLongest(String text, List<String> searchPhrases) {
        FoundSearch found = FoundSearch.NOT_FOUND;

        for (String searchPhrase : searchPhrases) {
            int searchPhraseIndex = text.lastIndexOf(searchPhrase);
            if (searchPhraseIndex > -1) {
                if (!found.wasSuccessfull() ||
                        found.getSearch().length() < searchPhrase.length()) {
                    found = new FoundSearch(searchPhraseIndex, searchPhrase);
                }
            }
        }

        return found;
    }

    /**
     * Ermittelt die längste Phrase, mit der der Text beginnt. Beginnt er mit keiner, wird der
     * leere String zurückgegeben.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param startPhrases
     *            Phrasen mit denen der Text anfangen könnte.
     * @return Die längste Phrase, mit der Text beginnt oder der leere String, wenn er mit keiner
     *         beginnt.
     */
    public static String findLongestAtStart(String text, List<String> startPhrases) {
        String longestStartPhrase = "";

        for (String startPhrase : startPhrases) {
            if (text.startsWith(startPhrase)) {
                if (longestStartPhrase.length() < startPhrase.length()) {
                    longestStartPhrase = startPhrase;
                }
            }
        }

        return longestStartPhrase;
    }

    /**
     * Ermittelt ob der Text beginnt mit einer der Phrasen beginnt. Beginnt er mit keiner, wird der
     * false zurückgegeben.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param startPhrases
     *            Phrasen mit denen der Text anfangen könnte.
     * @return Wahrheitswert. Wahr genau dann, wenn der Text mit einer der Phrasen beginnt.
     */
    public static boolean startsWithAnyListMember(String text, List<String> startPhrases) {
        for (String startPhrase : startPhrases) {
            if (text.startsWith(startPhrase)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Ermittelt den größten Index (und den gefundenen Suchtext) einer der Suchtexte aus der
     * Liste. Die Reihenfolge der Liste ist hierbei unerheblich.
     *
     * Wird keiner der übergebenen Begriffe im Text gefunden, so wird FoundSearch.NOT_FOUND
     * zurückgegeben.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     */
    public static FoundSearch findLastListElementInText(String text, List<String> searchPhrases) {
        FoundSearch foundSearch = FoundSearch.NOT_FOUND;
        for (String searchPhrase : searchPhrases) {
            int searchPhraseIndex = text.lastIndexOf(searchPhrase);
            if (searchPhraseIndex > -1) {
                boolean useNewFound = false;
                if (foundSearch.wasSuccessfull()) {
                    if (searchPhraseIndex > foundSearch.getIndex()) {
                        useNewFound = true;
                    }
                }
                else {
                    useNewFound = true;
                }
                if (useNewFound) {
                    foundSearch = new FoundSearch(searchPhraseIndex, searchPhrase);
                }
            }
        }

        return foundSearch;
    }

    /**
     * Findet alle Fundstellen der Suchbegriffe im Text.                                <br><br><i>
     *
     * Achtung, es werden keine Treffer in anderen Treffern verhindert.                        </i>
     *
     * Wird keiner der übergebenen Begriffe im Text gefunden, so wird eine leere Liste
     * zurückgegeben.
     *
     * @param text
     *            Zu durchsuchender Text.
     * @param searchPhrases
     *            Liste der zu suchenden Begriffe.
     */
    public static List<FoundSearch> findAllSearchPhrasesInText(String text,
            List<String> searchPhrases) {
        List<FoundSearch> foundSearches = new ArrayList<>();

        for (String searchPhrase : searchPhrases) {
            int searchStart = 0;

            int searchPhraseIndex = text.indexOf(searchPhrase, searchStart);
            while (searchPhraseIndex > -1) {
                FoundSearch foundSearch = new FoundSearch(searchPhraseIndex, searchPhrase);
                foundSearches.add(foundSearch);

                searchStart = searchPhraseIndex + searchPhrase.length();
                // Das verhindert nur überlappende Treffer DIESES Suchbegriffs, aber nicht von
                // anderen.

                searchPhraseIndex = text.indexOf(searchPhrase, searchStart);
            }
        }

        return foundSearches;
    }

    /**
     * Entfernt ein optionales Komma am Anfang und zuvor und im Anschluss Leerzeichen vorn und
     * hinten.
     */
    public static String removeCommaAtStart(String input) {
        String output = input;
        output = output.strip();
        output = Text.removeTextAtFrontIfStartsWith(output, ",");
        return output.strip();
    }

    /**
     * Entfernt ein optionales Komma am Ende und zuvor und im Anschluss Leerzeichen vorn und
     * hinten.
     */
    public static String removeCommaAtEnd(String input) {
        String output = input;
        output = output.strip();
        output = Text.removeTextAtEndIfEndsWith(output, ",");
        return output.strip();
    }

    /**
     * Bricht den Kommentar über einer Java-Methode um, wenn dieser zu lang ist.
     *
     * @param comment
     *            Kommentar ohne die Java-Kommentarzeichen rundherum.
     * @return Liste mit den Ausgabezeilen.
     */
    public static List<String> createJavaMethodComment(String comment) {
        String mulitCommentarHead = "    /**";
        String mulitCommentarTail = "     */";
        String multiLineCommentarStart = "     * ";

        String singleLineCommentarStart = "    /** ";
        String singleLineCommentarEnd = " */";

        String cleanedComment = Text.stripWhitespace(comment);

        String singleLineComment = singleLineCommentarStart + cleanedComment
                + singleLineCommentarEnd;
        if (singleLineComment.length() < MAXIMAL_JAVA_LINE_LENGTH) {
            return CollectionsHelper.buildListFrom(singleLineComment);
        }
        else {
            List<String> commentLines = new ArrayList<>();
            commentLines.add(mulitCommentarHead);
            int lineLength = MAXIMAL_JAVA_LINE_LENGTH - 1 - multiLineCommentarStart.length();
            String multiLineComment = addLineBreaks(cleanedComment, lineLength);
            for (String line : splitByLineBreaks(multiLineComment)) {
                String commentLine = multiLineCommentarStart + line;
                commentLines.add(commentLine);
            }
            commentLines.add(mulitCommentarTail);
            return commentLines;
        }
    }

    /**
     * Bricht den Kommentar über einer Java-Klasse um, wenn dieser zu lang ist.
     *
     * Hier werden im Gegensatz zu createJavaMethodComment() kein öffnender und schließender
     * Kommentar angefügt, sondern nur die oben anzufügenden Zeilen erzeugt.
     *
     * @param comment
     *            Kommentar ohne die Java-Kommentarzeichen rundherum.
     * @return Liste mit den Ausgabezeilen.
     */
    public static List<String> createJavaHeadCommentLines(String comment) {
        return createJavaCommentLines(comment, 0);
    }

    /**
     * Bricht den Kommentar über einer Java-Methode um, wenn dieser zu lang ist.
     *
     * Hier werden im Gegensatz zu createJavaMethodComment() kein öffnender und schließender
     * Kommentar angefügt, sondern nur die oben anzufügenden Zeilen erzeugt.
     *
     * @param comment
     *            Kommentar ohne die Java-Kommentarzeichen rundherum.
     * @return Liste mit den Ausgabezeilen.
     */
    public static List<String> createJavaMethodCommentLines(String comment) {
        return createJavaCommentLines(comment, 4);
    }

    /**
     * Bricht den Kommentar in einer Java-Methode um, wenn dieser zu lang ist.
     *
     * Hier werden im Gegensatz zu createJavaMethodComment() kein öffnender und schließender
     * Kommentar angefügt, sondern nur die oben anzufügenden Zeilen erzeugt.
     *
     * @param comment
     *            Kommentar ohne die Java-Kommentarzeichen rundherum.
     * @return Liste mit den Ausgabezeilen.
     */
    public static List<String> createJavaInMethodCommentLines(String comment) {
        return createJavaCommentLines(comment, 8);
    }

    /**
     * Bricht einen Java-Kommentar, wenn dieser zu lang ist.
     *
     * Hier werden im Gegensatz zu createJavaMethodComment() kein öffnender und schließender
     * Kommentar angefügt, sondern nur die oben anzufügenden Zeilen erzeugt.
     *
     * @param comment
     *            Kommentar ohne die Java-Kommentarzeichen rundherum.
     * @param numberOfFrontSapces
     *            Anzahl der vorn eingefügten Leerzeichen, ergibt durch vier geteilt die
     *            Einrückungstiefe.
     * @return Liste mit den Ausgabezeilen.
     */
    public static List<String> createJavaCommentLines(String comment, int numberOfFrontSapces) {
        String commentarLineStart = multipleString(" ", numberOfFrontSapces) + " * ";

        String cleanedComment = Text.stripWhitespace(comment);

        String singleLineComment = commentarLineStart + cleanedComment;
        if (singleLineComment.length() < MAXIMAL_JAVA_LINE_LENGTH) {
            return CollectionsHelper.buildListFrom(singleLineComment);
        }
        else {
            List<String> commentLines = new ArrayList<>();
            int lineLength = MAXIMAL_JAVA_LINE_LENGTH - 1 - commentarLineStart.length();
            String multiLineComment = addLineBreaks(cleanedComment, lineLength);
            for (String line : splitByLineBreaks(multiLineComment)) {
                String commentLine = commentarLineStart + line;
                commentLines.add(commentLine);
            }
            return commentLines;
        }
    }

    /**
     * Bricht eine Java-Zeile passend um, falls sie zu lang ist.
     *
     * @param javaCommand
     *            Zu bearbeitende Java-Zeile.
     * @param initialIdentationDepth
     *            Einrücktiefe in Anzahl der Leerzeichen der Zeile.
     * @return Liste mit den Ausgabezeilen.
     */
    public static List<String> createJavaCommand(String javaCommand, int initialIdentationDepth) {
        String singleLineComment = multipleString(" ", initialIdentationDepth)
                + Text.stripWhitespace(javaCommand);

        if (singleLineComment.length() < MAXIMAL_JAVA_LINE_LENGTH) {
            return CollectionsHelper.buildListFrom(singleLineComment);
        }
        else {
            int lineLength = MAXIMAL_JAVA_LINE_LENGTH;
            int numberOfFrontSpaces = initialIdentationDepth + 8;
            String multiLines = addLineBreaks(singleLineComment, lineLength, numberOfFrontSpaces);
            return splitByLineBreaks(multiLines);
        }
    }

    /**
     * Ändert den Beginn des übergebenen Textes zu einer Genitiv-Form ("der Name ..." wird zu
     * "des Namens ...").
     */
    public static String changeBeginningToGenitiv(String input) {
        List<String> words = splitByWhitespace(input);
        if (words.size() < 2) {
            return input;
        }
        else {
            String firstWord = words.get(0);
            List<String> otherWords = words.subList(1, words.size());
            if (firstWord.equals("der")) {
                return changeMaleBeginningToGenitiv(otherWords);
            }
            else if (firstWord.equals("die")) {
                return changeFemaleBeginningToGenitiv(otherWords);
            }
            else if (firstWord.equals("das")) {
                return changeNeutralBeginningToGenitiv(otherWords);
            }
            else {
                return input;
            }
        }
    }

    private static String changeMaleBeginningToGenitiv(List<String> otherWords) {
        List<String> outputWords = new ArrayList<>();
        outputWords.add("des");

        String secondWord = otherWords.get(0);
        List<String> restOfWordsWords = otherWords.subList(1, otherWords.size());

        if (firstLetterIsCapitalLetter(secondWord)) {
            secondWord = maleNomenToGenitiv(secondWord);
            outputWords.add(secondWord);
        }
        else {
            secondWord = maleAdjectivToGenitiv(secondWord);
            outputWords.add(secondWord);

            if (!restOfWordsWords.isEmpty()) {
                String thirdWord = restOfWordsWords.remove(0);
                if (firstLetterIsCapitalLetter(thirdWord)) {
                    thirdWord = maleNomenToGenitiv(thirdWord);
                }
                outputWords.add(thirdWord);
            }
        }
        outputWords.addAll(restOfWordsWords);
        return Text.joinWithBlank(outputWords);
    }

    private static String changeFemaleBeginningToGenitiv(List<String> otherWords) {
        List<String> outputWords = new ArrayList<>();
        outputWords.add("der");

        String secondWord = otherWords.get(0);
        List<String> restOfWordsWords = otherWords.subList(1, otherWords.size());

        if (firstLetterIsCapitalLetter(secondWord)) {
            secondWord = femaleNomenToGenitiv(secondWord);
            outputWords.add(secondWord);
        }
        else {
            secondWord = femaleAdjectivToGenitiv(secondWord);
            outputWords.add(secondWord);

            if (!restOfWordsWords.isEmpty()) {
                String thirdWord = restOfWordsWords.remove(0);
                if (firstLetterIsCapitalLetter(thirdWord)) {
                    thirdWord = femaleNomenToGenitiv(thirdWord);
                }
                outputWords.add(thirdWord);
            }
        }
        outputWords.addAll(restOfWordsWords);
        return Text.joinWithBlank(outputWords);
    }

    private static String changeNeutralBeginningToGenitiv(List<String> otherWords) {
        List<String> outputWords = new ArrayList<>();
        outputWords.add("des");

        String secondWord = otherWords.get(0);
        List<String> restOfWordsWords = otherWords.subList(1, otherWords.size());

        if (firstLetterIsCapitalLetter(secondWord)) {
            secondWord = neutralNomenToGenitiv(secondWord);
            outputWords.add(secondWord);
        }
        else {
            secondWord = neutralAdjectivToGenitiv(secondWord);
            outputWords.add(secondWord);

            if (!restOfWordsWords.isEmpty()) {
                String thirdWord = restOfWordsWords.remove(0);
                if (firstLetterIsCapitalLetter(thirdWord)) {
                    thirdWord = neutralNomenToGenitiv(thirdWord);
                }
                outputWords.add(thirdWord);
            }
        }
        outputWords.addAll(restOfWordsWords);
        return Text.joinWithBlank(outputWords);
    }

    /** Macht aus der Grundform eines männlichen Nomens eine Genitivform. */
    public static String maleNomenToGenitiv(String nomen) {
        if (nomen.endsWith("n")) {
            return nomen + "es"; // (der) Mann -> (des) Mannes
        }
        else if (nomen.endsWith("e")) {
            return nomen + "ns"; // (der) Name -> (des) Namens
        }
        else if (nomen.endsWith("Ort")) {
            return nomen + "es"; // (der) HR-Ort -> (des) HR-Ortes
        }
        else {
            switch (nomen) {
                case "xxx":
                    return "xxx";
                default:
                    return nomen + "s";
            }
        }
    }

    /** Macht aus der Grundform eines männlichen Nomens eine Genitivform. */
    public static String femaleNomenToGenitiv(String nomen) {
        return nomen;
    }

    /** Macht aus der Grundform eines männlichen Nomens eine Genitivform. */
    public static String neutralNomenToGenitiv(String nomen) {
        return nomen + "es";
    }

    /** Macht aus der Grundform eines männlichen Adjektivs eine Genitivform. */
    private static String maleAdjectivToGenitiv(String adjectiv) {
        if (adjectiv.endsWith("e")) {
            return adjectiv + "n";
        }
        else {
            return adjectiv;
        }
    }

    /** Macht aus der Grundform eines männlichen Adjektivs eine Genitivform. */
    private static String femaleAdjectivToGenitiv(String adjectiv) {
        if (adjectiv.endsWith("e")) {
            return adjectiv + "n";
        }
        else {
            return adjectiv;
        }
    }

    /** Macht aus der Grundform eines männlichen Adjektivs eine Genitivform. */
    private static String neutralAdjectivToGenitiv(String adjectiv) {
        if (adjectiv.endsWith("e")) {
            return adjectiv + "n";
        }
        else {
            return adjectiv;
        }
    }

    /**
     * Ersetzt große und kleine Umlaute sowie das ß durch ausgeschriebene Versionen mit zwei
     * Buchstaben.
     */
    public final static String replaceUmlauts(String input) {
        String output = input;

        output = output.replace("Ä", "Ae");
        output = output.replace("Ö", "Oe");
        output = output.replace("Ü", "Ue");
        output = output.replace("ä", "ae");
        output = output.replace("ö", "oe");
        output = output.replace("ü", "ue");
        output = output.replace("ß", "ss");

        return output;
    }

    /** Erzeugt aus dem Teil eines Satzes einen passenden Namen für eine Java-Klasse. */
    public static String createJavaClassName(String input) {
        String output = createJavaVariableName(input);
        output = firstCharToUpperCase(output);
        return output;
    }

    /**
     * Erzeugt aus dem Teil eines Satzes einen passenden Namen für eine Java-Variable oder Methode.
     */
    public static String createJavaVariableName(String input) {
        StringBuilder output = new StringBuilder();

        boolean first = true;
        for (String part : createReplacedPartsForJava(input)) {
            if (first) {
                first = false;
                output.append(part);
            }
            else {
                output.append(toFirstUpperCase(part));
            }
        }

        String outputString = output.toString();
        String numbersAtFront = NumberString.findNaturalNumberAtStartString(outputString);
        outputString = outputString.substring(numbersAtFront.length());
        outputString = firstCharToLowerCase(outputString);

        return outputString;
    }

    /**
     * Erzeugt aus dem Teil eines Satzes einen passenden Namen für eine Java-Konstante (static
     * final).
     */
    public static String createJavaConstantName(String input) {
        StringBuilder output = new StringBuilder();

        boolean first = true;
        for (String part : createReplacedPartsForJava(input)) {
            if (first) {
                first = false;
            }
            else {
                output.append("_");
            }
            output.append(toUpperCase(part));
        }

        return output.toString();
    }

    /**
     * Eine [...] Regex-Menge mit allen Zeichen, die nicht in Dateinamen gehören, allerdings auch
     * mit Bindestrich und Unterstrich, um diese später wieder einheitlich zusammenzusetzen.
     */
    public static final String NOT_PART_OF_FILENAMES_REGEX_WITH_HYPHEN_AND_UNDERSCORE =
            "[-_!?.,#\"';:/\\\\()\\*´&$§+`<>³]";

    private static List<String> createReplacedPartsForJava(String input) {
        List<String> replacedParts = new ArrayList<>();
        String cleaned = input.replaceAll(NOT_PART_OF_FILENAMES_REGEX_WITH_HYPHEN_AND_UNDERSCORE,
                " ");
        cleaned = cleaned.replaceAll("(" + SMALL + ")(" + BIG + ")", "$1 $2");
        cleaned = stripWhitespace(cleaned);

        for (String part : splitByWhitespace(cleaned)) {
            String output = part;
            output = replaceUmlauts(output);
            output = toLowerCase(output);
            replacedParts.add(output);
        }

        return replacedParts;
    }

    /**
     * Wandelt den eingegebenen Text so ab, dass er als Teil eines Dateinamens verwendet werden
     * kann.
     *
     * Worte in CamelCase werden dabei auseinander gerückt, so wird aus "createReplaced" dann
     * "create_replaced".
     *
     * Leerzeichen am Anfang und am Ende werden entfernt.
     *
     * Die Ausgabe enthält keine Leerzeichen, sondern Unterstriche.
     */
    public static String prepareForFilename(String input) {
        String output = input;
        output = output.strip();
        output = output.replaceAll(NOT_PART_OF_FILENAMES_REGEX_WITH_HYPHEN_AND_UNDERSCORE, "_");
        output = output.replaceAll("(" + SMALL + ")(" + BIG + ")", "$1_$2");
        output = output.replaceAll("\\s+", "_");
        output = output.replaceAll("\\r", "_"); // sollte in \s sein, aber wer weiß
        output = output.replaceAll("\\n", "_"); // sollte in \s sein, aber wer weiß
        output = output.replaceAll("_+", "_");
        output = replaceUmlauts(output);
        output = toLowerCase(output);

        return output;
    }

    /**
     * Teilt ein Wort in Camel-Case in seine Bestandteile auf.
     *
     * @param camelCase
     *            Der übergebene Stirn in Camel-Case, z.B. "hrNummer" oder "SessionManager".
     * @return Liste mit den Bestandteilen, z.B. ("hr", "Nummer") oder ("Session", "Manager").
     */
    public static List<String> splitCamelCase(String camelCase) {
        List<String> parts = new ArrayList<>();
        String part = "";
        for (String letter : CollectionsHelper.splitStringIntoLetters(camelCase)) {
            if (BIG_CHARS.contains(letter)) {
                if (!part.isEmpty()) {
                    parts.add(part);
                }
                part = letter;
            }
            else if (SMALL_CHARS.contains(letter)) {
                part += letter;
            }
            else {
                throw new IllegalArgumentException(
                        "Die Eingabe '" + camelCase + "' liegt nicht im CamelCase vor.");
            }
        }
        if (!part.isEmpty()) {
            parts.add(part);
        }
        return parts;
    }

    /**
     * Ersetzt Camel-Case in eine auseinandergezogene kleingeschriebene Variante.
     *
     * So wird "hrNummer" zu "hr-nummer".
     *
     * @param camelCase
     *            Der übergebene Stirn in Camel-Case, z.B. "hrNummer" oder "SessionManager".
     * @param fill
     *            Wird zwischen die auseinandergezogenen Teile eingefügt, z.B. "-".
     * @return Umgewandelter String
     */
    public static String separateCamelCase(String camelCase, String fill) {
        StringBuilder result = new StringBuilder();

        boolean first = true;
        for (String part : splitCamelCase(camelCase)) {
            if (first) {
                first = false;
            }
            else {
                result.append(fill);
            }
            result.append(toLowerCase(part));
        }

        return result.toString();
    }

    private static final Pattern NO_BREAK_AFTER_TYPE = Pattern.compile(
            "[\\),] " + MIXED + "+$");

    /**
     * Bricht einen langen Java-Methodenkopf auf mehrere Zeilen auf und rückt passend ein. Dabei
     * werden Begriffe hinter Leerzeichen ohne Komma und öffnende Klammer nicht zum Umbrechen
     * gewählt.
     *
     * @param code
     *            Der Methodenkopf als Einzeiler.
     * @return Liste mit den Zeilen des umgebrochenen Methodenkopfes.
     */
    public static List<String> breakLongJavaMethodStart(String code) {
        String broken = Text.addLineBreaks(code, 100, 3 * 4);
        List<String> brokenLines = Text.splitByLineBreaks(broken);
        for (int index = 0; index < brokenLines.size() - 1; ++index) {
            String brokenLine = brokenLines.get(index);
            Matcher matcher = NO_BREAK_AFTER_TYPE.matcher(brokenLine);
            if (matcher.find()) {
                String end = matcher.group();
                end = end.substring(1); // ohne öffnende Klammer oder Komma.
                brokenLine = Text.removeTextAtEndIfEndsWith(brokenLine, end);
                String nextBrokenLine = brokenLines.get(index + 1);
                int nonBlankIndex = Text.findIndexOfFirstWordLetter(nextBrokenLine);
                if (nonBlankIndex > -1) {
                    String front = nextBrokenLine.substring(0, nonBlankIndex);
                    String rear = nextBrokenLine.substring(nonBlankIndex);
                    nextBrokenLine = front + end.strip() + " " + rear;
                    brokenLines.set(index, brokenLine);
                    brokenLines.set(index + 1, nextBrokenLine);
                }
            }
        }
        return brokenLines;
    }

    /** Prüft ob die Eingabe auf einen großen oder kleinen Buchstaben endet. */
    public static boolean endsWithLetter(String input) {
        return endsWithBigLetter(input) || endsWithSmallLetter(input);
    }

    /** Prüft ob die Eingabe auf einen großen Buchstaben endet. */
    public static boolean endsWithBigLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }
        else {
            String lastCharacter = input.substring(input.length() - 1, input.length());
            return BIG_CHARS.contains(lastCharacter);
        }
    }

    /** Prüft ob die Eingabe auf einen kleinen Buchstaben endet. */
    public static boolean endsWithSmallLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }
        else {
            String lastCharacter = input.substring(input.length() - 1, input.length());
            return SMALL_CHARS.contains(lastCharacter);
        }
    }

    /** Prüft ob die Eingabe auf einen großen oder kleinen Buchstaben beginnt. */
    public static boolean startsWithLetter(String input) {
        return startsWithBigLetter(input) || startsWithSmallLetter(input);
    }

    /** Prüft ob die Eingabe auf einen großen Buchstaben beginnt. */
    public static boolean startsWithBigLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }
        else {
            String firstCharacter = input.substring(0, 1);
            return BIG_CHARS.contains(firstCharacter);
        }
    }

    /** Prüft ob die Eingabe auf einen kleinen Buchstaben beginnt. */
    public static boolean startsWithSmallLetter(String input) {
        if (input.isEmpty()) {
            return false;
        }
        else {
            String firstCharacter = input.substring(0, 1);
            return SMALL_CHARS.contains(firstCharacter);
        }
    }

    /** Prüft, ob der Text auf den übergebenen Regex endet. */
    public static boolean endsWithRegex(String text, String regex) {
        Pattern pattern = Pattern.compile(regex + "$");
        Matcher matcher = pattern.matcher(text);
        return matcher.find();
    }

    /**
     * Ersetzt alle Vorkommen des Suchtextes im übergebenen Text durch die Ersetzung, falls der
     * Suchtext kein Anfang eines Wortes ist.
     *
     * @param text
     *            Zu bearbeitender Text.
     * @param search
     *            Begriff der im Text gesucht wird.
     * @param replacement
     *            Der Suchbegriff wird hierdurch ersetzt.
     * @return Überarbeiteter Text.
     */
    public static String replacePhrasesInText(String text, String search, String replacement) {
        if (search.isBlank()) {
            return text;
        }

        final int searchLength = search.length();
        int searchIndex = 0;
        boolean searching = true;
        String replacedText = text;
        while (searching) {
            int index = replacedText.indexOf(search, searchIndex);
            if (index == -1) {
                searching = false;
            }
            else {
                boolean replaceAtIndex = false;
                int indexAfter = index + searchLength;
                if (replacedText.length() == indexAfter) { // ganz hinten im Text
                    replaceAtIndex = true;
                    searching = false;
                }
                else {
                    String charAfterMatch = replacedText.substring(indexAfter, indexAfter + 1);
                    if (!MIXED_CHARS.contains(charAfterMatch)) {
                        replaceAtIndex = true;
                    }
                }

                if (replaceAtIndex) {
                    replacedText = replaceInText(replacedText, replacement, index, indexAfter);
                    searchIndex = index + replacement.length();
                }
                else {
                    searchIndex = indexAfter;
                }
            }
        }

        return replacedText;
    }

    private static final Pattern CONTAINES_ONLY_BIG_LETTERS_PATTERN = Pattern.compile(
            BIG + "+");

    /** Überprüft, ob der gegebene String nur aus Großbuchstaben besteht. */
    public static boolean containsOnlyBigLetters(String input) {
        Matcher matcher = CONTAINES_ONLY_BIG_LETTERS_PATTERN.matcher(input);
        return matcher.matches();
    }

    private static final Pattern CONTAINES_ONLY_BIG_LETTERS_AND_HYPHENS_PATTERN = Pattern.compile(
            BIG + "+(?:-" + BIG + "+)*");

    /**
     * Überprüft, ob der gegebene String nur aus Großbuchstaben und Bindestrichen besteht, wobei
     * der Bindestrich nicht als erstes oder als letztes stehen darf und keine zwei Bindestriche
     * nacheinander vorkommen dürfen.
     */
    public static boolean containsOnlyBigLettersAndHyphens(String input) {
        Matcher matcher = CONTAINES_ONLY_BIG_LETTERS_AND_HYPHENS_PATTERN.matcher(input);
        return matcher.matches();
    }

    /**
     * Erzeugt eine umgebrochene Definition einer langen Java-String Variablen.
     *
     * Hierbei werden 8 Leerzeichen vor "String " verwendet und die maximale Zeilenlänge beträgt
     * 100.
     *
     * @param variableName
     *            Name der Variablen.
     * @param variableContent
     *            Inhalt der Variablen, hier werden Anführungszeichen escaped.
     * @return Mehrzeilige Definition.
     */
    public static List<String> createLongJavaStringDefinition(String variableName,
            String variableContent) {
        int indentation = 8;
        int maxLength = 100;
        return createLongJavaStringDefinition(variableName, variableContent, indentation, maxLength);
    }

    /**
     * Erzeugt eine umgebrochene Definition einer langen Java-String Variablen.
     *
     * @param variableName
     *            Name der Variablen.
     * @param variableContent
     *            Inhalt der Variablen, hier werden Anführungszeichen escaped.
     * @param indentation
     *            Anzahl der Leerzeichen vor "String ".
     * @param maxLength
     *            Die maximale Länge der Zeilen.
     * @return Mehrzeilige Definition.
     */
    public static List<String> createLongJavaStringDefinition(String variableName,
            String variableContent, int indentation, int maxLength) {
        List<String> lines = new ArrayList<>();
        String firstLine = multipleString(" ", indentation) + "String " + variableName
                + " = \"\"";
        lines.add(firstLine);

        int numberOfFrontSpaces = indentation + 8;
        int numberOfAdditionalCharacters = 5; // vorn: '+ "' und hinten '"', beim letzten auch ';'
        String text = escapeDoubleQuotes(variableContent);
        int lineLength = maxLength - numberOfFrontSpaces - numberOfAdditionalCharacters;
        String contentLinesAsString = addLineBreaksKeepSpacesAtBreaks(text, lineLength);
        List<String> contentLines = splitByLineBreaks(contentLinesAsString);
        for (int index = 0; index < contentLines.size(); ++index) {
            String contentLine = contentLines.get(index);
            String line = multipleString(" ", numberOfFrontSpaces) + "+ \""
                    + contentLine + "\"";
            if (index == contentLines.size() - 1) {
                line += ";";
            }
            lines.add(line);
        }

        return lines;
    }

    private static final List<String> ROUND_BRACES = CollectionsHelper.buildListFrom("(", ")");

    /** Gibt an, ob der übergebene Index aus dem Text innerhalb einer Klammer liegt. */
    public static boolean isInsideBrace(String text, int index) {
        String before = text.substring(0, index);
        String after = text.substring(index);
        FoundSearch lastBraceBefore = findLast(before, ROUND_BRACES);
        FoundSearch firstBraceAfter = findFirstPosition(after, ROUND_BRACES);

        if (lastBraceBefore.wasSuccessfull() && firstBraceAfter.wasSuccessfull()) {
            String braceBefore = lastBraceBefore.getSearch();
            String braceAfter = firstBraceAfter.getSearch();
            return "(".equals(braceBefore) && ")".equals(braceAfter);
        }
        else {
            return false;
        }
    }

    /**
     * Kürzt einen Java-Klassennamen mit vollem Paket zum eigentlichen Namen der Klasse (den Teil
     * hinter dem Punkt. So wird "de.duehl.basics.text.Text" zu "Text".
     */
    public static String shortenJavaClassName(String className) {
        int lastDotIndex = className.lastIndexOf(".");
        if (lastDotIndex > -1) {
            return className.substring(lastDotIndex + 1);
        }
        else {
            return className;
        }
    }

    /**
     * Ermittelt zu einer Klasse den gekürzten Java-Klassennamen (ohne das volle Paket).
     * So zur Klasse "de.duehl.basics.text.Text" die Rückgabe "Text" erzeugt.
     */
    public static String getShortenedJavaClassName(Class<?> clazz) {
        String className = clazz.getName();
        return shortenJavaClassName(className);
    }

    /**
     * Teilt den übergebenen Text am ersten Vorkommen einer der Suchbegriffe.
     *
     * Ist kein Suchbegriff enthalten, wird eine Ausnahme geworfen.
     *
     * @param text
     *            Der zu teilende Text.
     * @param searchWords
     *            Die Suchbegriffe.
     * @return Das Paar mit dem vorderen und hinteren Teil des Textes, der hintere Teil beginnt mit
     *         dem zuerst gefunden Suchbegriff.
     */
    public static Pair<String> splitByFirstOccurence(String text, String ... searchWords) {
        List<String> searchWordsList = CollectionsHelper.arrayToList(searchWords);
        return splitByFirstOccurence(text, searchWordsList);
    }

    /**
     * Teilt den übergebenen Text am ersten Vorkommen einer der Suchbegriffe.
     *
     * Ist kein Suchbegriff enthalten, wird eine Ausnahme geworfen.
     *
     * @param text
     *            Der zu teilende Text.
     * @param searchWords
     *            Die Suchbegriffe.
     * @return Das Paar mit dem vorderen und hinteren Teil des Textes, der hintere Teil beginnt mit
     *         dem zuerst gefunden Suchbegriff.
     */
    public static Pair<String> splitByFirstOccurence(String text, List<String> searchWords) {
        FoundSearch firstPosition = Text.findFirstPosition(text, searchWords);
        if (firstPosition.wasSuccessfull()) {
            int index = firstPosition.getIndex();
            String first = text.substring(0, index);
            String second = text.substring(index);
            return new Pair<>(first, second);
        }
        else {
            throw new RuntimeException("Keiner der Suchbegriffe wurde im Text gefunden.\n"
                    + "\t" + "Text = '" + text + "'\n"
                    + "\t" + "Suchbegriffe:\n"
                    + CollectionsHelper.listListNice(searchWords));
        }
    }

    /**
     * Teilt den übergebenen Text nach dem ersten Vorkommen einer der Suchbegriffe.
     *
     * Ist kein Suchbegriff enthalten, wird eine Ausnahme geworfen.
     *
     * @param text
     *            Der zu teilende Text.
     * @param searchWords
     *            Die Suchbegriffe.
     * @return Das Paar mit dem vorderen und hinteren Teil des Textes, der vordere Teil endet auf
     *         den zuerst gefunden Suchbegriff.
     */
    public static Pair<String> splitAfterFirstOccurence(String text, String ... searchWords) {
        List<String> searchWordsList = CollectionsHelper.arrayToList(searchWords);
        return splitAfterFirstOccurence(text, searchWordsList);
    }

    /**
     * Teilt den übergebenen Text nach dem ersten Vorkommen einer der Suchbegriffe.
     *
     * Ist kein Suchbegriff enthalten, wird eine Ausnahme geworfen.
     *
     * @param text
     *            Der zu teilende Text.
     * @param searchWords
     *            Die Suchbegriffe.
     * @return Das Paar mit dem vorderen und hinteren Teil des Textes, der vordere Teil endet auf
     *         den zuerst gefunden Suchbegriff.
     */
    public static Pair<String> splitAfterFirstOccurence(String text, List<String> searchWords) {
        FoundSearch firstPosition = Text.findFirstPosition(text, searchWords);
        if (firstPosition.wasSuccessfull()) {
            int endIndex = firstPosition.getEnd();
            String first = text.substring(0, endIndex);
            String second = text.substring(endIndex);
            return new Pair<>(first, second);
        }
        else {
            throw new RuntimeException("Keiner der Suchbegriffe wurde im Text gefunden.\n"
                    + "\t" + "Text = '" + text + "'\n"
                    + "\t" + "Suchbegriffe:\n"
                    + CollectionsHelper.listListNice(searchWords));
        }
    }

    /**
     * Teilt den übergebenen Text am ersten Vorkommen einer der Suchbegriffe.
     * Hierbei wird der zuerst gefundene nicht mit zurückgegeben.
     *
     * Ist kein Suchbegriff enthalten, wird eine Ausnahme geworfen.
     *
     * @param text
     *            Der zu teilende Text.
     * @param searchWords
     *            Die Suchbegriffe.
     * @return Das Paar mit dem vorderen und hinteren Teil des Textes, beide Teile enthalten nicht
     *         den zuerst gefunden Suchbegriff.
     */
    public static Pair<String> splitByFirstOccurenceWithoutSearch(String text,
            String ... searchWords) {
        List<String> searchWordsList = CollectionsHelper.arrayToList(searchWords);
        return splitByFirstOccurenceWithoutSearch(text, searchWordsList);
    }

    /**
     * Teilt den übergebenen Text am ersten Vorkommen einer der Suchbegriffe.
     * Hierbei wird der zuerst gefundene nicht mit zurückgegeben.
     *
     * Ist kein Suchbegriff enthalten, wird eine Ausnahme geworfen.
     *
     * @param text
     *            Der zu teilende Text.
     * @param searchWords
     *            Die Suchbegriffe.
     * @return Das Paar mit dem vorderen und hinteren Teil des Textes, beide Teile enthalten nicht
     *         den zuerst gefunden Suchbegriff.
     */
    public static Pair<String> splitByFirstOccurenceWithoutSearch(String text,
            List<String> searchWords) {
        FoundSearch firstPosition = Text.findFirstPosition(text, searchWords);
        if (firstPosition.wasSuccessfull()) {
            int index = firstPosition.getIndex();
            int endIndex = firstPosition.getEnd();
            String first = text.substring(0, index);
            String second = text.substring(endIndex);
            return new Pair<>(first, second);
        }
        else {
            throw new RuntimeException("Keiner der Suchbegriffe wurde im Text gefunden.\n"
                    + "\t" + "Text = '" + text + "'\n"
                    + "\t" + "Suchbegriffe:\n"
                    + CollectionsHelper.listListNice(searchWords));
        }
    }

    /**
     * Prüft ob der übergebene Text aus einer Wiederholung des übergebenen Teils besteht.
     *
     * @param text
     *            Zu überprüfender Text.
     * @param part
     *            Möglicher Teil des Textes.
     * @return Wahrheitswert, true genau dann, wenn der übergebene Text aus einer Wiederholung des
     *         übergebenen Teils besteht.
     */
    public static boolean isMultipleString(String text, String part) {
        if (part.isEmpty()) {
            return text.isEmpty();
        }

        if (!text.startsWith(part) || !text.endsWith(part)) { // Beschleunigung
            return false;
        }

        String test = "";

        while (text.startsWith(test) && text.length() > test.length()) {
            test += part;
            if (text.equals(test)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Ersetzt das erste Vorkommen des Suchbegriffs im übergebenen Text durch den übergebenen
     * Ersatz.
     *
     * @param text
     *            Der zu verändernde Text.
     * @param search
     *            Die zu ersetzende Phrase.
     * @param replacement
     *            Hierdurch soll search ersetzt werden.
     * @return Der veränderte Text.
     */
    public static String replaceFirstMatch(String text, String search, String replacement) {
        int index = text.indexOf(search);
        if (index == -1) {
            return text;
        }
        else {
            String front = text.substring(0, index);
            int endIndex = index + search.length();
            String rear = text.substring(endIndex);
            return front + replacement + rear;
        }
    }

    /**
     * Gibt eine Liste mit den einzelnen Zeichen der Zeichenkette zurück. Aus "Hallo" wird also
     * ("H", "a", "l", "l", "o").
     */
    public static List<String> textToCharactersList(String text) {
        List<String> characters = new ArrayList<>();

        for (int index = 0; index < text.length(); ++index) {
            String character = text.substring(index, index + 1);
            characters.add(character);
        }

        return characters;
    }

    /**
     * Gibt eine Liste mit den einzelnen Zeichen der Zeichenkette für komplexe Utf8-Zeichen zurück.
     * Aus "Hallo" wird also ("H", "a", "l", "l", "o").
     */
    public static List<String> textToCharactersListForComplexUtf8(String text) {
        List<String> characters = new ArrayList<>();

        text.codePoints().forEach(cp -> {
            characters.add(new String(Character.toChars(cp)));
        });

//        for (int index = 0; index < text.length(); ++index) {
//            String character = text.substring(index, index + 1);
//            characters.add(character);
//        }

        return characters;
    }

    /**
     * Hängt die beiden Zeichenketten so zusammen, dass mindestens ein Leerzeichen dazwischen
     * steht, aber auch nicht mehr als diese, es sei denn, sie enthalten vorn und hinten
     * entsprechend zusammen mehr als ein Leerzeichen.
     */
    public static String concatenate(String first, String second) {
        if (first.isEmpty()) {
            return second;
        }
        else if (second.isEmpty()) {
            return first;
        }
        else {
            String lastCharacter = first.substring(first.length() - 1, first.length());
            String firstCharacter = second.substring(0, 1);
            if (lastCharacter.equals(" ") || firstCharacter.equals(" ")) {
                return first + second;
            }
            else {
                return first + " " + second;
            }
        }
    }

    /**
     * Hängt die übergebenen Zeichenketten so zusammen, dass jeweils mindestens ein Leerzeichen
     * dazwischen steht, aber auch nicht mehr als diese, es sei denn, sie enthalten vorn und hinten
     * entsprechend zusammen mehr als ein Leerzeichen.
     */
    public static String concatenate(List<String> parts) {
        String concatenated = "";
        for (String part : parts) {
            concatenated = concatenate(concatenated, part);
        }
        return concatenated;
    }

    /**
     * Ersetzt bestimmte Zeichen aus dem UTF-8-Zeichensatz, um Texte im Zeichensatz ISO_8859_1
     * abspeichern zu können.
     */
    public static String replaceSpecialUtf8Chars(String text) {
        String correctedText = text;

        correctedText = correctedText.replace("„", "\"");
        correctedText = correctedText.replace("“", "\"");
        correctedText = correctedText.replace("”", "\"");
        correctedText = correctedText.replace("–", "-");
        correctedText = correctedText.replace("•", "-");
        correctedText = correctedText.replace("’", "'");
        correctedText = correctedText.replace("‘", "'");
        correctedText = correctedText.replace("‚", ",");

        return correctedText;
    }

    /**
     * Prüft ob die übergebenen Worte in alphabetischer Reihenfolge vorliegen.
     *
     * @param words
     *            Die zu prüfenden Worte.
     * @return Wahrheitswert.
     */
    public static boolean isInAlphabeticalOrder(String ... names) {
        List<String> testList1 = CollectionsHelper.arrayToList(names);
        List<String> testList2 = CollectionsHelper.arrayToList(names);

        Collections.sort(testList2);

        return testList1.equals(testList2);
    }

    /**
     * Prüft ob die beiden Eingaben gleich sind, wenn man umhüllende eckige oder runde Klammern
     * entfernt. Gleiche Strings sind hier immer gleich.
     *
     * @param input1
     *            Die erste Eingabe (z.B. "sicher").
     * @param input2
     *            Die zweite Eingabe (z.B. "(sicher)").
     * @return Wahrheitswert, true, wenn die beiden Eingaben ohne die Klammern gleich sind.
     */
    public static boolean equalsIgnoringRoundOrSquareBrackets(String input1, String input2) {
        String compare1 = removeRoundOrQuareBraces(input1);
        String compare2 = removeRoundOrQuareBraces(input2);
        return compare1.equals(compare2);
    }

    private static String removeRoundOrQuareBraces(String input) {
        if (input.startsWith("(") && input.endsWith(")")) {
            return input.substring(1, input.length() - 1);
        }
        else if (input.startsWith("[") && input.endsWith("]")) {
            return input.substring(1, input.length() - 1);
        }
        else {
            return input;
        }
    }

    /**
     * Vergleicht zwei Strings auf natürlichere Weise als es text1.compareTo(text2) tut, denn da
     * kommt dann 'Z' vor 'a' und behandelt Umlaute richtig.
     */
    public static int compareStringsBetter(String text1, String text2) {
        String loweredText1 = toLowerCase(text1);
        String loweredText2 = toLowerCase(text2);
        loweredText1 = replaceSmallUmlautsAndSz(loweredText1);
        loweredText2 = replaceSmallUmlautsAndSz(loweredText2);
        int loweredCompareValue = loweredText1.compareTo(loweredText2);
        if (loweredCompareValue == 0) {
            return text1.compareTo(text2);
        }
        else {
            return loweredCompareValue;
        }
    }

    private static String replaceSmallUmlautsAndSz(String input) {
        String output = input;
        output = output.replace("ä", "ae");
        output = output.replace("ö", "oe");
        output = output.replace("ü", "ue");
        output = output.replace("ß", "ss");
        return output;
    }

    /**
     * Sucht nach dem Suchbegriff als ganzes Wort.
     *
     * @param text
     *            Der zu durchsuchende Text.
     * @param search
     *            Der Suchbegriff.
     * @return Wahrheitswert.
     */
    public static boolean containsAsWholeWord(String text, String search) {
        int startIndex = 0;
        boolean searching = true;
        boolean result = false;

        while (searching) {
            int index = text.indexOf(search, startIndex);
            if (index == -1) {
                result =  false;
                searching = false;
            }
            else if (containsAsWholeWordAtIndex(text, search, index)) {
                result =  true;
                searching = false;
            }
            else {
                startIndex = index + search.length();
            }
        }

        return result;
    }

    /**
     * Prüft ob der Suchbegriff am übergebenen Index als ganzes Wort vorliegt.
     *
     * @param text
     *            Der zu durchsuchende Text.
     * @param search
     *            Der Suchbegriff.
     * @param index
     *            Ein Index an dem der Suchbegriff gefunden wurde.
     * @return Wahrheitswert.
     */
    private static boolean containsAsWholeWordAtIndex(String text, String search, int index) {
        String before = text.substring(0, index);
        String after = text.substring(index + search.length());

        if (before.isEmpty()) {
            //
        }
        else {
            String lastCharacter = before.substring(before.length() - 1);
            if (MIXED_CHARS.contains(lastCharacter)) {
                return false;
            }
        }

        if (after.isEmpty()) {
            //
        }
        else {
            String firstCharacter = after.substring(0, 1);
            if (MIXED_CHARS.contains(firstCharacter)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Ermittelt den Index der ersten Fundstelle des Suchbegriffs als als ganzes Wort.
     *
     * Wird der Suchbegriff nicht gefunden, wird -1 zurückgegeben.
     *
     * @param text
     *            Der zu durchsuchende Text.
     * @param search
     *            Der Suchbegriff.
     * @return Der Index der ersten Fundstelle als ganzes Wort oder -1, wenn er nicht als ganzes
     *         Wort gefunden wurde.
     */
    public static int indexOfAsWholeWord(String text, String search) {
        return indexOfAsWholeWord(text, search, 0);
    }

    /**
     * Ermittelt den Index der ersten Fundstelle ab dem Startindex des Suchbegriffs als als ganzes
     * Wort.
     *
     * Wird der Suchbegriff ab dem Startindex nicht gefunden, wird -1 zurückgegeben.
     *
     * @param text
     *            Der zu durchsuchende Text.
     * @param search
     *            Der Suchbegriff.
     * @param startIndex
     *            Der Index ab dem der Text nach dem Suchbegriff durchsucht wird.
     * @return Der Index der ersten Fundstelle ab dem Startindex als ganzes Wort oder -1, wenn er
     *         nicht als ganzes Wort gefunden wurde.
     */
    public static int indexOfAsWholeWord(String text, String search, int startIndex) {
        int indexToSearchFrom = startIndex;
        boolean searching = true;
        int result = -1;

        while (searching) {
            int index = text.indexOf(search, indexToSearchFrom);
            if (index == -1) {
                result =  -1;
                searching = false;
            }
            else if (containsAsWholeWordAtIndex(text, search, index)) {
                result =  index;
                searching = false;
            }
            else {
                indexToSearchFrom = index + search.length();
            }
        }

        return result;
    }

    /**
     * Ersetzt im übergebenen Text alle (als ganzes Wort gefundenen) zu versteckenden Worte durch
     * eine Reihe von "#" der gleichen Länge.
     *
     * @param text
     *            Der zu verändernde Text.
     * @param wordsToHide
     *            Die zu versteckenden Worte.
     * @return Der veränderte Text.
     */
    public static String hideWholeWordsInText(String text, List<String> wordsToHide) {
        String changedText = text;

        for (String wordToHide : wordsToHide) {
            boolean replacing = true;
            while (replacing) {
                int index = indexOfAsWholeWord(changedText, wordToHide);
                if (index == -1) {
                    replacing = false;
                }
                else {
                    String before = changedText.substring(0, index);
                    String after = changedText.substring(index + wordToHide.length());
                    int middleLength = wordToHide.length();
                    String middle = Text.multipleString("#", middleLength);
                    int oldLength = changedText.length();
                    changedText = before + middle + after;
                    int newLength = changedText.length();
                    if (oldLength != newLength) {
                        throw new RuntimeException("Fehler bei der Ersetzung von ganzen Worten in "
                                + "Texten:\n"
                                + "\t" + "text        = '" + text + "'\n"
                                + "\t" + "changedText = '" + changedText + "'\n"
                                + "\t" + "wordToHide  = '" + wordToHide + "'\n"
                                + "\t" + "oldLength   = " + oldLength + "\n"
                                + "\t" + "newLength   = " + newLength + "\n"
                                );
                    }
                }
            }
        }

        return changedText;
    }

    /**
     * Findet Texte in "Anführungszeichen", die sich vorn und hinten unterscheiden, also z.B.
     * „Baum“, ‚Haus‘ (Klammer), [Klammer], {Klammer}, «sowas» und ‹sowas›.
     *
     * Aber auch 「japanisch」.
     *
     * @param text
     *            Der zu durchsuchende Text.
     * @param startQuote
     *            Das Anführungszeichen am Anfang.
     * @param endQute
     *            Das Anführungszeichen am Ende.
     * @return Die Liste mit den gefundenen Texten, die in den übergebenen Anführungszeichen
     *         stehen. Findet man keine Anführungszeichen im Text, sind es unterschiedlich viele
     *         öffnende und schließende Anführungszeichen oder stimmt deren Reihenfolge nicht, wird
     *         die leere Liste zurückgegeben. Das ist auch der Fall, wenn die gleichen
     *         Anführungszeichen übergeben werden.
     */
    public static List<String> findTextsBetweenDifferentStartAndEndQuotes(String text,
            String startQuote, String endQute) {
        List<Integer> openingQuoteIndices = Text.findAllPositions(startQuote, text);
        List<Integer> closingQuoteIndices = Text.findAllPositions(endQute, text);

        if (openingQuoteIndices.size() != closingQuoteIndices.size()
                || openingQuoteIndices.isEmpty()) {
            return new ArrayList<>();
        }

        /*
         * Fehler sind:
         *   - n. öffnendes >= n. schließendes
         *   - n. öffnendes <= n-1. schließendes
         */
        for (int index = 0; index < openingQuoteIndices.size(); ++index) {
            int indexOfOpeningQuote = openingQuoteIndices.get(index);
            int indexOfClosingQuote = closingQuoteIndices.get(index);
            if (indexOfOpeningQuote >= indexOfClosingQuote) {
                return new ArrayList<>();
            }
            if (index > 0) {
                int indexOfClosingQuoteBefore = closingQuoteIndices.get(index - 1);
                if (indexOfOpeningQuote <= indexOfClosingQuoteBefore) {
                    return new ArrayList<>();
                }
            }
        }

        List<String> quotedTexts = new ArrayList<>();

        for (int index = 0; index < openingQuoteIndices.size(); ++index) {
            int indexOfOpeningQuote = openingQuoteIndices.get(index);
            int indexOfClosingQuote = closingQuoteIndices.get(index);
            String quotedText = text.substring(indexOfOpeningQuote + 1, indexOfClosingQuote);
            quotedTexts.add(quotedText);
        }

        return quotedTexts;
    }

    /**
     * Findet Texte in japanischen Anführungszeichen der Art 「DING」.
     *
     * @param text
     *            Der zu durchsuchende Text.
     * @return Die Liste mit den gefundenen Texten, die in den übergebenen Anführungszeichen
     *         stehen. Findet man keine Anführungszeichen im Text, sind es unterschiedlich viele
     *         öffnende und schließende Anführungszeichen oder stimmt deren Reihenfolge nicht, wird
     *         die leere Liste zurückgegeben.
     */
    public static List<String> findTextsBetweenJapaneseQuotes(String text) {
        return findTextsBetweenDifferentStartAndEndQuotes(text, "「", "」");
    }

    /**
     * Gibt den String ohne die führenden Leerzeichen zurück. Hat er keine führenden Leerzeichen
     * oder ist leer, wird er unverändert zurückgegeben.
     */
    public static String removeSpacesAtStart(String text) {
        String rest = text;

        while (!rest.isEmpty() && rest.startsWith(" ")) {
            rest = rest.substring(1);
        }

        return rest;
    }

    /**
     * Wandelt Tabs und Umbrüche in Leerzeichen und entfernt dann Leerzeichen zu Beginn und am Ende
     * und macht aus allen mehrfachen Leerzeichen ein einzelnes.
     *
     * @param input
     *            Zu bearbeitender Text.
     * @return Bearbeiteter Text.
     */
    public static String trimAndCompactSpacesWithLineBreakAndTab(String input) {
        return trimAndCompactSpaces(lineBreaksToSpace(tabToSpace(input)));
    }

    /**
     * Gibt alle Worte des Textes zurück.
     *
     * @param text
     *            Der Text in dem gesucht wird.
     */
    public static List<String> getAllWords(String text) {
        List<String> words = new ArrayList<>();
        StringBuilder wordBuilder = new StringBuilder(); // sinnlose Initialisierung für den Compiler
        List<String> letters = CollectionsHelper.splitStringIntoLetters(text);

        boolean inWord = false;
        for (String letter : letters) {
            if (MIXED_CHARS_LIST.contains(letter)) {
                if (!inWord) {
                    wordBuilder = new StringBuilder();
                    inWord = true;
                }
                wordBuilder.append(letter);
            }
            else {
                if (inWord) {
                    String word = wordBuilder.toString();
                    if (!word.isBlank()) {
                        words.add(word);
                    }
                    inWord = false;
                }
            }
        }

        if (inWord) {
            String word = wordBuilder.toString();
            if (!word.isBlank()) {
                words.add(word);
            }
        }

        return words;
    }

    /** Gibt an, ob der übergebene Text in Kleinbuchstaben geschrieben ist. */
    public static boolean isInLoweredCase(String test) {
        String loweredInput = toLowerCase(test);
        return test.equals(loweredInput);
    }

    /** Stellt alle Worte so dar, dass der erste Buchstabe groß und alle anderen klein sind. */
    public static String toAllWordsFirstCharUppercaseRestLowercase(String text) {
        List<String> words = splitByWhitespace(text);
        List<String> correctWords = new ArrayList<>();
        for (String word : words) {
            String correctWord = toFirstUpperCase(word);
            correctWords.add(correctWord);
        }
        return joinWithBlank(correctWords);
    }

}
