~/home of geeks

Listen und Separieren

· 434 Wörter · 3 Minute(n) Lesedauer

Vor einiger Zeit wollte ich String-Werte in einer Collection zu einem String zusammenfügen und baute ein eigenes, CSV ähnliches Format.

Mein eigentliches Vorhaben war es, alle Strings mit einem Separator, in diesem Fall das Semikolon, zu konkatenieren. CSV wollte ich nicht benutzen, da bei CSV die einzelnen Werte durch Anführungszeichen umschlossen werden. Ich wollte aus einem einfachen ["A", "B", "C"] ein "A;B;C" machen. Eigentlich ein trivialer Fall, wäre da nicht die Möglichkeit, dass der Separator selber ebenfalls in den Strings enthalten ist, also ["A;B", "C", "D"]. In so einem Fall hilft das Escapen des Separators durch ein Escape-Zeichen. Ich nahm das klassische Backslash. Damit wurde ["A;B", "C", "D"] zu ["A\\;B;C;D"]. Das Ganze geht natürlich nicht, ohne dass man das Escape-Zeichen selber ebenfalls escapt, um den Fall “A\;B” als Eingabe ebenfalls abzusichern.

/**
 * Einfaches Zusammenführen mit Escaping.
 *
 * @param inputs
 * @return
 */
public String serialize(Collection<String> inputs) {
    final StringBuilder output = new StringBuilder();
    boolean first = true;
    for (final String value : inputs) {
        if (StringUtils.isNotBlank(value)) {
            if (!first) {
                output.append(";");
            }
            for (final char c : value.toCharArray()) {
                if (c == '\\\\') {
                    output.append("\\\\\\\\");
                } else if (c == ';') {
                    output.append("\\\\;");
                } else {
                    output.append(c);
                }
            }
            first = false;
        }
    }

    return output.toString();
}

Das Parsen der serialisierten Strings ist recht einfach. In unserem Fall wird die Eingabe von Links nach Rechts verarbeitet, dabei hat die bereits verarbeitete Seite Priorität (Linksreduktion). Und für den Fall des Backslashes benötigen wir ein Zeichen weiter rechts (das Lookahead), um die Entscheidung zu treffen, ob wir ein Escape haben oder nicht. Für ein solches Vorgehen benötigt man einen LL(1) Parser.

/**
 * Einfaches Parsen. Das Format ist eine Semikolon separierte Liste.
 * Escapezeichen ist ein Backslash.
 *
 * @param in
 * @return
 */
public List<String> deserialize(String in) {
    final List<String> outputs = new ArrayList<String>();
    int i = 0;
    final StringBuilder buffer = new StringBuilder();
    while (i < in.length()) {
        if (in.charAt(i) == '\\\\') {
            // Fälle für Backslash:
            // 1. Es ist das letzte Zeichen -> Backslash wird übernommen
            // 2. Nächstes Zeichen ist ";" oder Backslash: Erst das Nächste wird übernommen (escape)
            // 3. Das Backslash wird übernommen
            if (i == in.length() - 1) {
                // Fall 1
                buffer.append(in.charAt(i));
                i++;
            } else {
                // Lookahead
                i++;
                if (i < in.length() && in.charAt(i) == ';' || in.charAt(i) == '\\\\') {
                    // Fall 2 (escape)
                    buffer.append(in.charAt(i));
                    i++;
                } else {
                    / Fall 3
                    buffer.append('\\\\');
                }
            }
        } else if (in.charAt(i) == ';') {
            // End of Token
            outputs.add(buffer.toString());
            // Reset Buffer
            buffer.setLength(0);
            i++;
        } else {
            buffer.append(in.charAt(i));
            i++;
        }
    }
    outputs.add(buffer.toString());

    return outputs;
}