StringTokenizer pour .NET Core
Fractionner une chaîne en jetons est un sujet plus complexe que String.Split () veut nous faire croire. Il existe au moins trois stratégies communes selon lesquelles une chaîne peut être interprétée et divisée en jetons.
Stratégie 1: équivalent à String.Split ()
Il n'y a pas grand-chose à mentionner sur cette politique. Étant donné une chaîne set un délimiteur d, divisez sen segments délimités par d. Le principal inconvénient ici est que si le délimiteur fait partie d'au moins un des jetons, la reconstruction des jetons souhaités peut être coûteuse.
Politique 2: échapper aux caractères spéciaux
Un caractère est déclaré comme caractère d'échappement e (généralement la barre oblique inverse \), ce qui fait que le caractère qui le suit perd sa signification particulière. Une chaîne de jeton pourrait alors ressembler à ceci:
token_1 token_2 very\ long \ token
ce qui équivaudrait à
{ "token_1", "token_2", "very long token" }
Politique 3: placer les jetons entre guillemets
Cette approche est par exemple utilisée dans les fichiers CSV générés dans MSExcel. Tout ce qui est entre guillemets est considéré comme un jeton. Si les guillemets "font partie du jeton, ils sont doublés "". Une chaîne de jeton pourrait alors ressembler à ceci:
token_1,token_2,"token2,5"
ce qui équivaudrait à
{ "token_1", "token_2", "token2,5" }
Code
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
}
}
Réponses
Je ne suis pas sûr que cela appartienne à une classe - du moins pas une seule!
Prenez du recul et regardez ce qui unit et ce qui sépare chaque «stratégie». Ils ont tous besoin de transformer une chaîne d'entrée en une liste de jetons basée sur un délimiteur de variable. Cependant, certaines propriétés ne sont utilisées que par l'une des trois options et la majorité de la logique de fractionnement est propre à sa stratégie.
Suggestion 1: Trois fonctions «autonomes».
Il faudrait vraiment les mettre dans une classe statique ou faire quelque chose de spécial avec les délégués / lambdas, mais en fin de compte, il y a peu à gagner à avoir une seule 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 = ' ') { ... }
Si vous souhaitez que la sortie communique les paramètres d'entrée, vous pouvez créer une classe beaucoup plus légère qui le fasse. Ses propriétés seraient readonly; si vous avez besoin de les modifier et de recalculer, appelez simplement la fonction à nouveau. Après tout, c'est essentiellement ce que vous faites à l'intérieur de votre classe actuelle!
Un autre avantage: si et quand vous proposez une nouvelle stratégie de fractionnement, vous pouvez simplement créer une nouvelle fonction sans affecter les autres. Ils sont tous testables, modifiables et supprimables indépendamment.
Suggestion 2: Trois classes concrètes qui étendent une classe de base abstraite.
J'aime ce que vous avez fait avec la _Tokenspropriété: cela vous permet de différer le calcul jusqu'à ce que vous en ayez vraiment besoin, ce qui est utile dans les cas où vous n'en avez pas. De plus, un cas d'utilisation qu'il prend en charge (qui n'est pas supporté par les fonctions "autonomes") est de changer par exemple le caractère d'échappement et de faire automatiquement "invalider" le résultat.
Pour conserver ce comportement, vous pouvez extraire les éléments communs dans une classe de base abstraite, comme suit:
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;
}
}
Changements notables:
- La logique de division réelle est absente. Chaque stratégie fournira la sienne.
- Les propriétés spécifiques à la stratégie sont absentes. Il n'est pas nécessaire qu'une stratégie basée sur l'évasion ait une propriété pour un caractère de citation, et vice-versa.
- Au lieu de définir directement
_tokens = null, les propriétés doivent appelerInvalidateResult. Cela permet_tokensd'être faitprivatequi garde la logique contenue dans la classe de base. Tokensest public, et est unIEnumerable. Cela permet aux consommateurs d'utiliserforeach, mais décourage la modification directe.
Une classe de base a maintenant exactement un travail: implémenter ComputeTokens. S'il a besoin de créer des propriétés pour ce faire, il peut le faire, selon sa propre logique propre à la stratégie. Si ces propriétés doivent invalider les jetons précédemment calculés lors de leur modification, elles peuvent appeler InvalidateResult.
Voici un exemple approximatif de ce à quoi ressemblerait une sous-classe de stratégie:
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
}
}
Autres observations
- Vous autorisez la spécification du délimiteur, mais vous condensez toujours les espaces. Si je me sépare
"a,a and b,b"avec un délimiteur de",", je m'attendrais à{"a", "a and b", "b"}revenir - mais j'obtiendrais réellement{"a", "a and b", "b"}. - Si le délimiteur, etc., peut être lu publiquement, pourquoi ne pas exposer également la chaîne source? Voir
SourceStringma classe abstraite ci-dessus. - Je trouve que les accesseurs de propriété à expression (relativement nouveaux) sont meilleurs pour les propriétés simples. Voir
Countdans ma classe abstraite ci-dessus. - Je ne pense pas qu'il soit possible d'assigner accidentellement
nullà une variable comme condition d'une instruction if. En effet, il estx = nullévalué comme étant du même type quex, qui doit être abool(et donc non nullable) pour être une condition valide. Si vous souhaitez toujours éviterx == null, vous pouvez direx is null. - Comme mentionné par d'autres, vous ne devez pas préfixer les propriétés avec
_. Ce n'est pas là pour faire la différence entre public et privé, mais entre les variables locales et les champs de classe. Personnellement, cependant, je n'utilise même pas_dans ce cas, mais préfère plutôtthis.si nécessaire. Mais dans l'ensemble, vous devrez faire preuve de souplesse à ce sujet et vous assurer de suivre tout modèle déjà établi dans une équipe ou un projet existant. - De plus, comme d'autres l'ont mentionné, utilisez
varlorsque cela est possible lors de la déclaration de variables. Tout bon IDE sera en mesure de vous indiquer le type lorsque vous survolez la variable, et son nom devrait vous dire à quoi il sert, même sans le type. - Sur cette note, évitez les noms comme
cetl.ic'est bien car c'est idiomatique en tant que variable de boucle / index, mais les autres nécessitent un contexte supplémentaire pour être compris. Les caractères du code source sont bon marché, alors payez pour une lisibilité supplémentaire en utilisantcurrentCharetfinishedTokens. - Vous n'avez pas besoin de traduire la source
stringen unchar[]; vous pouvez déjà accéder aux caractères dans unstringindex.
Vous ne devriez pas avoir
Whitespacescomme propriété get-only mais commereadonlychamp privé et vous devriez avoir cette regex compilée parce que vous l'utilisez assez souvent.L'utilisation regionest considérée comme un anti-modèle
Utilisez le préfixe de soulignement uniquement pour les champs privés. Ne les utilisez pas pour des méthodes ou des propriétés.
Si le type d'une variable est clair du côté droit d'une affectation, vous devez utiliser à la
varplace du type concret.Le code fait beaucoup mais
_sourceStringpeut-êtrestring.Emptyparce que l'argument ctor passédataInputpeut êtrenulloustring.Empty. Je préférerais lancer une exception dans lector.Au lieu d'assigner une variable à une autre puis de manipuler la variable résultante, vous pouvez simplement le faire sur une ligne comme par exemple
string data = Whitespaces.Replace(_sourceString, " ");au lieu de
string data = _sourceString; // Doing some basic transformations data = Whitespaces.Replace(data, " ");Si vous n'avez besoin d'accéder qu'à des éléments uniques d'un tableau et que vous n'avez pas besoin de regarder en avant, vous devriez préférer
foreachuneforboucle.
Un
lnom d' une seule lettre me semble mauvais.Je pense que vous devriez ajouter un message à l'exception qui décrit la raison de l'erreur.
Par défaut, vous supprimez tous les espaces des données. Mais ils peuvent être nécessaires à l'intérieur des jetons. Vous pouvez créer une option supplémentaire pour le spécifier.
Merci à tous pour les excellents commentaires. J'ai adopté la plupart des changements apportés à mon code qui est hébergé en tant que FOS surhttps://github.com/pillepalle1/dotnet-pillepalle1 où il recevra un entretien supplémentaire.
Pour l'instant, j'ai regroupé la logique de fractionnement en trois méthodes d'extension statiques. De plus, j'ai construit des wrappers comme suggéré par therubberduck pour éventuellement conserver le confort de l'invalidation automatique des jetons
Suggestions que j'ai implémentées
Dénomination des variables Noms de variables tels que
lremplacés par des noms plus descriptifsDes messages d'exception ont été ajoutés
Modification du contenu du jeton A été complètement supprimé des méthodes d'extension et rendu disponible en option dans les wrappers
Les régions ont été complètement supprimées
Utiliser var chaque fois que cela est raisonnable / possible
Boucles Préférant
foreachlesforboucles et itérer sur lesourceStringplutôt que de le convertir enchar[]premierInputstring Lancer
ArgumentNullExceptionau lieu de convertirnullenString.EmptyDivision CSV selon RFC4180
J'aurais adopté plus de changements mais certaines suggestions (c'est-à-dire concernant Whitespaceset l'expression des propriétés corporelles) sont devenues obsolètes dans la nouvelle implémentation.
Suggestions que je n'ai pas mises en œuvre
- La dénomination de soulignement pour tout ce qui est privé / protégé me semble plus raisonnable que la simple distinction entre les variables membres et locales, car lors de la mise en œuvre de structures de données robustes en simultanéité (qui sont devenues une grande chose depuis
Tasksleur mise en œuvre), il est incroyablement précieux de voir au premier coup d'œil si un méthode effectue des contrôles de concurrence (public) ou non (privé).
Code
Méthodes de jeton statique
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 de base de wrapper abstraite
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 pour la tokenisation simple
using System.Collections.Immutable;
namespace pillepalle1.Text
{
public class PlainStringTokenizer : AStringTokenizer
{
protected override ImmutableList<string> Tokenize()
{
return SourceString.SplitPlain(Delimiter);
}
}
}
Wrapper pour la tokenisation de devis
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 pour la tokenisation d'échappement
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);
}
}
}