StringTokenizer per .NET Core
Dividere una stringa in token è un argomento più complesso di quanto String.Split () voglia farci credere. Esistono almeno tre criteri comuni in base ai quali una stringa potrebbe essere interpretata e suddivisa in token.
Criterio 1: equivalente a String.Split ()
Non c'è molto da menzionare su questa politica. Data una stringa se un delimitatore d, suddividi sin segmenti delimitati da d. Lo svantaggio principale qui è che se il delimitatore fa parte di almeno uno dei token, ricostruire i token desiderati potrebbe essere costoso.
Criterio 2: sfuggire ai caratteri speciali
Un carattere viene dichiarato come carattere di escape e (comunemente la barra rovesciata \) e il carattere che lo segue perde il suo significato speciale. Una stringa di token potrebbe quindi essere simile a questa:
token_1 token_2 very\ long \ token
che sarebbe equivalente a
{ "token_1", "token_2", "very long token" }
Criterio 3: inserire i gettoni tra virgolette
Questo approccio è ad esempio utilizzato nei file CSV generati in MSExcel. Tutto ciò che è compreso tra virgolette è considerato come un segno. Se le virgolette "fanno parte del token, vengono raddoppiate "". Una stringa di token potrebbe quindi essere simile a questa:
token_1,token_2,"token2,5"
che sarebbe equivalente a
{ "token_1", "token_2", "token2,5" }
Codice
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace Pillepalle1.ConsoleTelegramBot.Model.Misc
{
public sealed class StringTokenizer
{
private string _sourceString = null; // Provided data to split
#region Constructors
/// <summary>
/// Creates a new StringTokenizer
/// </summary>
/// <param name="dataInput">Data to be split into tokens</param>
public StringTokenizer(string dataInput)
{
_sourceString = dataInput ?? string.Empty;
}
#endregion
#region Interface
/// <summary>
/// Access tokens by index
/// </summary>
public string this[int index]
{
get
{
if (index >= this.Count)
{
return String.Empty;
}
return _Tokens[index];
}
}
/// <summary>
/// How many tokens does the command consist of
/// </summary>
public int Count
{
get
{
return _Tokens.Count;
}
}
/// <summary>
/// Which strategy is used to split the string into tokens
/// </summary>
public StringTokenizerStrategy Strategy
{
get
{
return _strategy;
}
set
{
if (value != _strategy)
{
_strategy = value;
_tokens = null;
}
}
}
private StringTokenizerStrategy _strategy = StringTokenizerStrategy.Split;
/// <summary>
/// Character used to delimit tokens
/// </summary>
public char Delimiter
{
get
{
return _delimiter;
}
set
{
if (value != _delimiter)
{
_delimiter = value;
_tokens = null;
}
}
}
private char _delimiter = ' ';
/// <summary>
/// Character used to escape the following character
/// </summary>
public char Escape
{
get
{
return _escape;
}
set
{
if (value != _escape)
{
_escape = value;
if (Strategy == StringTokenizerStrategy.Escaping)
{
_tokens = null;
}
}
}
}
private char _escape = '\\';
/// <summary>
/// Character used to surround tokens
/// </summary>
public char Quotes
{
get
{
return _quotes;
}
set
{
if (value != _quotes)
{
_quotes = value;
if (Strategy == StringTokenizerStrategy.Quotation)
{
_tokens = null;
}
}
}
}
private char _quotes = '"';
#endregion
#region Predefined Regex
private Regex Whitespaces
{
get
{
return new Regex("\\s+");
}
}
#endregion
#region Implementation Details
/// <summary>
/// Formats and splits the tokens by delimiter allowing to add delimiters by quoting
/// </summary>
private List<string> _SplitRespectingQuotation()
{
string data = _sourceString;
// Doing some basic transformations
data = Whitespaces.Replace(data, " ");
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Initialisation
List<string> l = new List<string>();
char[] record = data.ToCharArray();
StringBuilder property = new StringBuilder();
char c;
bool quoting = false;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Scan character by character
for (int i = 0; i < record.Length; i++)
{
c = record[i];
// Quotation-Character: Single -> Quote; Double -> Append
if (c == Quotes)
{
if (i == record.Length - 1)
{
quoting = !quoting;
}
else if (Quotes == record[1 + i])
{
property.Append(c);
i++;
}
else
{
quoting = !quoting;
}
}
// Delimiter: Escaping -> Append; Otherwise append
else if (c == Delimiter)
{
if (quoting)
{
property.Append(c);
}
else
{
l.Add(property.ToString());
property.Clear();
}
}
// Any other character: Append
else
{
property.Append(c);
}
}
l.Add(property.ToString()); // Add last token
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Checking consistency
if (quoting) throw new FormatException(); // All open quotation marks closed
return l;
}
/// <summary>
/// Splits the string by declaring one character as escape
/// </summary>
private List<string> _SplitRespectingEscapes()
{
string data = _sourceString;
// Doing some basic transformations
data = Whitespaces.Replace(data, " ");
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Initialisation
List<string> l = new List<string>();
char[] record = data.ToCharArray();
StringBuilder property = new StringBuilder();
char c;
bool escaping = false;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Scan character by character
for (int i = 0; i < record.Length; i++)
{
c = record[i];
if (escaping)
{
property.Append(c);
escaping = false;
continue;
}
if (c == Escape)
{
escaping = true;
}
else if (c == Delimiter)
{
l.Add(property.ToString());
property.Clear();
}
else
{
property.Append(c);
}
}
return l;
}
/// <summary>
/// Splits the string by calling a simple String.Split
/// </summary>
private List<string> _SplitPlain()
{
return new List<string>(Whitespaces.Replace(_sourceString, " ").Split(Delimiter));
}
/// <summary>
/// Backer for tokens
/// </summary>
private List<string> _Tokens
{
get
{
if (null == _tokens)
{
switch (Strategy)
{
case (StringTokenizerStrategy.Quotation): _tokens = _SplitRespectingQuotation(); break;
case (StringTokenizerStrategy.Escaping): _tokens = _SplitRespectingEscapes(); break;
default: _tokens = _SplitPlain(); break;
}
}
return _tokens;
}
}
private List<string> _tokens = null;
#endregion
}
public enum StringTokenizerStrategy
{
Split,
Quotation,
Escaping
}
}
Risposte
Non sono così sicuro che questo appartenga a una classe, almeno non a una sola!
Fai un passo indietro e guarda cosa unisce e cosa separa ogni "strategia". Devono tutti trasformare una stringa di input in un elenco di token basato su un delimitatore di variabile. Tuttavia, ci sono proprietà che vengono utilizzate solo da una delle tre opzioni e la maggior parte della logica di divisione è unica per la sua strategia.
Suggerimento 1: tre funzioni "autonome".
Dovresti davvero metterli in una classe statica o fare qualcosa di speciale con delegati / lambda, ma alla fine c'è poco da guadagnare dall'avere una grande classe.
public static IList<string> SplitRespectingQuotation(string sourceString, char delimiter = ' ', char quote = '"') { ... }
public static IList<string> SplitRespectingEscapes(string sourceString, char delimiter = ' ', char escape = '\') { ... }
public static IList<string> SplitPlain(string sourceString, char delimiter = ' ') { ... }
Se desideri che l'output comunichi i parametri di input, puoi creare una classe molto più leggera che lo faccia. Le sue proprietà sarebbero readonly; se è necessario modificarli e ricalcolarli, è sufficiente chiamare di nuovo la funzione. Dopotutto, è essenzialmente quello che stai facendo all'interno della tua classe attuale!
Un altro vantaggio: se e quando proponi una nuova strategia per la divisione, puoi semplicemente creare una nuova funzione senza influenzare le altre. Sono tutti testabili, modificabili e cancellabili in modo indipendente.
Suggerimento 2: tre classi concrete che estendono una classe base astratta.
Mi piace quello che hai fatto con la _Tokensproprietà: ti consente di posticipare il calcolo fino a quando non ne hai davvero bisogno, il che è utile nei casi in cui non lo farai. Inoltre, un caso d'uso che supporta (che non è supportato dalle funzioni "autonome") è cambiare ad esempio il carattere di escape e avere il risultato automaticamente "invalidato".
Per mantenere quel comportamento, potresti inserire gli elementi comuni in una classe base astratta, come la seguente:
public abstract class StringTokenizer
{
public string SourceString { get; }
public StringTokenizer(string dataInput)
{
SourceString = dataInput ?? string.Empty;
}
public string this[int index] => index >= this.Count ? String.Empty : Tokens[index];
public int Count => Tokens.Count;
public char Delimiter
{
get { return _delimiter; }
set
{
if (value != _delimiter)
{
_delimiter = value;
InvalidateResult();
}
}
}
private char _delimiter = ' ';
public IEnumerable<string> Tokens
{
get
{
if (_tokens is null)
{
_tokens = ComputeTokens();
}
return _tokens;
}
}
private List<string> _tokens = null;
protected abstract List<string> ComputeTokens();
protected void InvalidateResult()
{
_tokens = null;
}
}
Cambiamenti notevoli:
- La logica di divisione effettiva è assente. Ogni strategia fornirà la propria.
- Le proprietà specifiche della strategia sono assenti. Non è necessaria una strategia basata sull'escape per avere una proprietà per un carattere di citazione e viceversa.
- Invece di impostare direttamente
_tokens = null, le proprietà dovrebbero chiamareInvalidateResult. Questo permette_tokensdi fareprivateche mantenga la logica contenuta nella classe base. Tokensè pubblico ed è un fileIEnumerable. Ciò consente ai consumatori di utilizzareforeach, ma scoraggia la modifica diretta.
Una classe base ora ha esattamente un compito: implementare ComputeTokens. Se ha bisogno di creare proprietà per farlo, può farlo, in base alla propria logica specifica della strategia. Se queste proprietà devono invalidare i token calcolati in precedenza quando cambiano, possono chiamare InvalidateResult.
Ecco un esempio approssimativo di come sarebbe una sottoclasse di strategia:
public sealed class EscapeStringTokenizer : StringTokenizer
{
public EscapeStringTokenizer (string dataInput) : base(dataInput) { }
public char Escape
{
get { return _escape; }
set
{
if (value != _escape)
{
_escape = value;
InvalidateResult();
}
}
}
protected override List<string> ComputeTokens()
{
// Actual logic omitted
}
}
Altre osservazioni
- Consenti di specificare il delimitatore, ma condensa sempre gli spazi. Se divido
"a,a and b,b"con un delimitatore di",", mi aspetto di tornare{"a", "a and b", "b"}indietro, ma in realtà otterrei{"a", "a and b", "b"}. - Se il delimitatore, ecc., Può essere letto pubblicamente, perché non esporre anche la stringa di origine? Vedi la
SourceStringmia classe astratta sopra. - Trovo che le funzioni di accesso alle proprietà con corpo di espressione (relativamente nuove) siano migliori a tutto tondo per le proprietà semplici. Vedi
Countnella mia classe astratta sopra. - Non penso sia possibile assegnare accidentalmente
nulla una variabile come condizione di un'istruzione if. Questo perchéx = nullrestituisce lo stesso tipo dix, che deve essere abool(e quindi non annullabile) per essere una condizione valida. Se vuoi ancora evitarex == null, puoi direx is null. - Come accennato da altri, non dovresti anteporre alle proprietà
_. Non è lì per distinguere tra pubblico e privato, ma tra variabili locali e campi di classe. Personalmente, però, non lo uso nemmeno_in quel caso, ma preferisco invecethis.se necessario. Ma nel complesso dovrai essere flessibile al riguardo e assicurarti di seguire qualsiasi modello già stabilito in un team o progetto esistente. - Inoltre, come altri hanno già detto, usare
varquando si dichiarano le variabili quando possibile. Qualsiasi buon IDE sarà in grado di dirti il tipo quando passi il mouse sulla variabile e il suo nome dovrebbe dirti a cosa serve anche senza il tipo. - In quella nota, evita nomi come
cel.iva bene perché è idiomatica come variabile di ciclo / indice, ma le altre richiedono un contesto aggiuntivo per essere comprese. I caratteri del codice sorgente sono economici, quindi paga per un po 'di leggibilità extra usandocurrentCharefinishedTokens. - Non è necessario tradurre la fonte
stringin unchar[]; puoi già accedere ai caratteri in unstringindice.
Non dovresti avere
Whitespacescome proprietà di sola ricezione ma comereadonlycampo privato e dovresti avere quella regex compilata perché la usi abbastanza spesso.L'utilizzo regionè considerato un modello antitetico
Usa il carattere di sottolineatura come prefisso solo per i campi privati. Non usarli per metodi o proprietà.
Se il tipo di una variabile è chiaro dal lato destro di un compito, dovresti usare al
varposto del tipo concreto.Il codice sta facendo molto anche se
_sourceStringpotrebbe esserestring.Emptyperché l'argomento ctor passatodataInputpotrebbe esserenullostring.Empty. Preferirei lanciare un'eccezione nel filector.Invece di assegnare una variabile a un'altra e quindi manipolare la variabile risultante, potresti semplicemente farlo su una riga come ad es
string data = Whitespaces.Replace(_sourceString, " ");invece di
string data = _sourceString; // Doing some basic transformations data = Whitespaces.Replace(data, " ");Se hai solo bisogno di accedere a singoli elementi di un array e non hai bisogno di guardare avanti, dovresti preferire un
foreachover aforloop.
Un
lnome di una sola lettera mi sembra brutto.Penso che dovresti aggiungere un messaggio all'eccezione che descrive il motivo dell'errore.
Per impostazione predefinita, rimuovi tutti gli spazi bianchi dai dati. Ma potrebbero essere necessari all'interno dei token. È possibile creare un'opzione aggiuntiva per specificarlo.
Grazie a tutti per l'ottimo feedback. Ho adottato la maggior parte delle modifiche al mio codice che viene ospitato come FOS suhttps://github.com/pillepalle1/dotnet-pillepalle1 dove riceverà ulteriore manutenzione.
Per ora, ho impacchettato la logica di suddivisione in tre metodi di estensione statica. Inoltre ho costruito wrapper come suggerito da therubberduck per mantenere facoltativamente il comfort dell'invalidazione automatica del token
Suggerimenti che ho implementato
Denominazione delle variabili I nomi delle variabili come
lsono stati sostituiti da nomi più descrittiviSono stati aggiunti messaggi di eccezione
La modifica del contenuto del token è stata completamente rimossa dai metodi di estensione e resa facoltativamente disponibile nei wrapper
Le regioni sono state completamente rimosse
Usare var quando ragionevole / possibile
Loops Preferendo
foreachsuforloop e l'iterazione di lasourceString, invece di convertirlo inchar[]primaInputstring Lanciare
ArgumentNullExceptioninvece di convertirenullinString.EmptyDivisione CSV secondo RFC4180
Avrei adottato più modifiche ma alcuni suggerimenti (ad esempio riguardo alle Whitespacesproprietà del corpo di espressione) sono diventati obsoleti nella nuova implementazione.
Suggerimenti che non ho implementato
- La sottolineatura di denominazione per tutto ciò che è privato / protetto mi sembra più ragionevole della semplice distinzione tra variabili membri e locali poiché quando si implementano strutture di dati robuste per la concorrenza (che sono diventate una cosa importante da quando
Taskssono state implementate) è incredibilmente prezioso vedere a prima vista se un metodo esegue i controlli di concorrenza (pubblico) o meno (privato).
Codice
Metodi di tokenizzatori statici
using System;
using System.Text;
using System.Collections.Immutable;
namespace pillepalle1.Text
{
public static class StringTokenizer
{
private static FormatException _nonQuotedTokenMayNotContainQuotes =
new FormatException("[RFC4180] If fields are not enclosed with double quotes, then double quotes may not appear inside the fields.");
private static FormatException _quotesMustBeEscapedException =
new FormatException("[RFC4180] If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote.");
private static FormatException _tokenNotFullyEnclosed =
new FormatException("[RFC4180] \"Each field may or may not be enclosed in double quotes\". However, for the final field the closing quotes are missing.");
/// <summary>
/// <para>
/// Formats and splits the tokens by delimiter allowing to add delimiters by quoting
/// similar to https://tools.ietf.org/html/rfc4180
/// </para>
///
/// <para>
/// Each field may or may not be enclosed in double quotes (however some programs, such as
/// Microsoft Excel, do not use double quotes at all). If fields are not enclosed with
/// double quotes, then double quotes may not appear inside the fields.
/// </para>
///
/// <para>
/// Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in
/// double-quotes.
/// </para>
///
/// <para>
/// If double-quotes are used to enclose fields, then a double-quote appearing inside a
/// field must be escaped by preceding it with another double quote.
/// </para>
///
/// <para>
/// The ABNF defines
///
/// [field = (escaped / non-escaped)] ||
/// [non-escaped = *TEXTDATA] ||
/// [TEXTDATA = %x20-21 / %x23-2B / %x2D-7E]
///
/// specifically forbidding to include quotes in non-escaped fields, hardening the *SHOULD*
/// requirement above.
/// </para>
/// </summary>
public static ImmutableList<string> SplitRespectingQuotation(this string sourceString, char delimiter = ' ', char quotes = '"')
{
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Initialisation
var tokenList = ImmutableList<string>.Empty;
var tokenBuilder = new StringBuilder();
var expectingDelimiterOrQuotes = false; // Next char must be Delimiter or Quotes
var hasReadTokenChar = false; // We are not between tokens (=> No quoting)
var isQuoting = false;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Scan character by character
foreach (char c in sourceString)
{
if (expectingDelimiterOrQuotes)
{
expectingDelimiterOrQuotes = false;
if (c == delimiter)
{
isQuoting = false;
}
else if (c == quotes)
{
tokenBuilder.Append(c);
hasReadTokenChar = true;
continue;
}
else
{
throw _quotesMustBeEscapedException;
}
}
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
if (c == quotes)
{
if (isQuoting)
{
expectingDelimiterOrQuotes = true;
}
else
{
if (hasReadTokenChar)
{
throw _nonQuotedTokenMayNotContainQuotes;
}
isQuoting = true;
}
}
else if (c == delimiter)
{
if (isQuoting)
{
tokenBuilder.Append(c);
hasReadTokenChar = true;
}
else
{
tokenList = tokenList.Add(tokenBuilder.ToString());
tokenBuilder.Clear();
hasReadTokenChar = false;
}
}
// Any other character is just being appended to
else
{
tokenBuilder.Append(c);
hasReadTokenChar = true;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Tidy up open flags and checking consistency
tokenList = tokenList.Add(tokenBuilder.ToString());
if (isQuoting && !expectingDelimiterOrQuotes)
{
throw _tokenNotFullyEnclosed;
}
return tokenList;
}
/// <summary>
/// Splits the string by declaring one character as escape
/// </summary>
public static ImmutableList<string> SplitRespectingEscapes(this string sourceString, char delimiter = ' ', char escapeChar = '\\')
{
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Initialisation
var tokenList = ImmutableList<string>.Empty;
var tokenBuilder = new StringBuilder();
var escapeNext = false;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Scan character by character
foreach (char c in sourceString)
{
if (escapeNext)
{
tokenBuilder.Append(c);
escapeNext = false;
continue;
}
if (c == escapeChar)
{
escapeNext = true;
}
else if (c == delimiter)
{
tokenList = tokenList.Add(tokenBuilder.ToString());
tokenBuilder.Clear();
}
else
{
tokenBuilder.Append(c);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Tidy up open flags and checking consistency
tokenList = tokenList.Add(tokenBuilder.ToString());
if (escapeNext) throw new FormatException(); // Expecting additional char
return tokenList;
}
/// <summary>
/// Splits the string by calling a simple String.Split
/// </summary>
public static ImmutableList<string> SplitPlain(this string sourceString, char delimiter = ' ')
{
return ImmutableList<string>.Empty.AddRange(sourceString.Split(delimiter));
}
}
}
Classe base wrapper astratta
using System;
using System.Collections.Immutable;
namespace pillepalle1.Text
{
public abstract class AStringTokenizer
{
public AStringTokenizer()
{
}
public AStringTokenizer(string sourceString)
{
SourceString = sourceString;
}
/// <summary>
/// String that is supposed to be split in tokens
/// </summary>
public string SourceString
{
get
{
return _sourceString;
}
set
{
if (null == value)
{
throw new ArgumentNullException("Cannot split null in tokens");
}
else if (_sourceString.Equals(value))
{
// nop
}
else
{
_sourceString = value;
_InvalidateTokens();
}
}
}
private string _sourceString = String.Empty;
/// <summary>
/// Character indicating how the source string is supposed to be split
/// </summary>
public char Delimiter
{
get
{
return _delimiter;
}
set
{
if (value != _delimiter)
{
_delimiter = value;
_InvalidateTokens();
}
}
}
private char _delimiter = ' ';
/// <summary>
/// Flag indicating whether whitespaces should be removed from start and end of each token
/// </summary>
public bool TrimTokens
{
get
{
return _trimTokens;
}
set
{
if (value != _trimTokens)
{
_trimTokens = value;
_InvalidateTokens();
}
}
}
private bool _trimTokens = false;
/// <summary>
/// Result of tokenization
/// </summary>
public ImmutableList<string> Tokens
{
get
{
if (null == _tokens)
{
_tokens = Tokenize();
if (TrimTokens)
{
_tokens = _TrimTokens(_tokens);
}
}
return _tokens;
}
}
private ImmutableList<string> _tokens = null;
/// <summary>
/// Split SourceString into tokens
/// </summary>
protected abstract ImmutableList<string> Tokenize();
/// <summary>
/// Trims whitespaces from tokens
/// </summary>
/// <param name="candidates">List of tokens</param>
private ImmutableList<string> _TrimTokens(ImmutableList<string> candidates)
{
var trimmedTokens = ImmutableList<string>.Empty;
foreach (var token in candidates)
{
trimmedTokens = trimmedTokens.Add(token.Trim());
}
return trimmedTokens;
}
/// <summary>
/// Invalidate and recompute tokens if necessary
/// </summary>
protected void _InvalidateTokens()
{
_tokens = null;
}
}
}
Wrapper per la semplice tokenizzazione
using System.Collections.Immutable;
namespace pillepalle1.Text
{
public class PlainStringTokenizer : AStringTokenizer
{
protected override ImmutableList<string> Tokenize()
{
return SourceString.SplitPlain(Delimiter);
}
}
}
Wrapper per la tokenizzazione delle quotazioni
using System.Collections.Immutable;
namespace pillepalle1.Text
{
public class QuotationStringTokenizer : AStringTokenizer
{
/// <summary>
/// Indicates which character is used to encapsulate tokens
/// </summary>
public char Quotes
{
get
{
return _quotes;
}
set
{
if (value != _quotes)
{
_quotes = value;
_InvalidateTokens();
}
}
}
private char _quotes = '"';
protected override ImmutableList<string> Tokenize()
{
return SourceString.SplitRespectingQuotation(Delimiter, Quotes);
}
}
}
Wrapper per la tokenizzazione della fuga
using System.Collections.Immutable;
namespace pillepalle1.Text
{
public class EscapedStringTokenizer : AStringTokenizer
{
/// <summary>
/// Indicates which character is used to escape characters
/// </summary>
public char Escape
{
get
{
return _escape;
}
set
{
if (value != _escape)
{
_escape = value;
_InvalidateTokens();
}
}
}
private char _escape = '"';
protected override ImmutableList<string> Tokenize()
{
return SourceString.SplitRespectingEscapes(Delimiter, Escape);
}
}
}