.NET Core 용 StringTokenizer

Sep 18 2020

토큰에서 문자열을 분할하는 것은 String.Split () 이 우리를 믿게 만드는 것보다 더 복잡한 주제 입니다. 문자열이 토큰으로 해석되고 분할 될 수있는 공통 정책이 3 개 이상 있습니다.

정책 1 : String.Split ()과 동일

이 정책에 대해 언급 할 내용이 많지 않습니다. 문자열을 감안할 s와 구분 기호 d, 휴식 s에 의해 구분 된 세그먼트로를 d. 여기서 주요 단점은 구분 기호가 토큰 중 하나 이상의 일부인 경우 원하는 토큰을 재구성하는 데 비용이 많이들 수 있다는 것입니다.

정책 2 : 특수 문자 이스케이프

문자는 이스케이프 문자 e (일반적으로 백 슬래시 \)로 선언되어 그 뒤에 오는 문자가 특별한 의미를 잃게됩니다. 토큰 문자열은 다음과 같습니다.

token_1 token_2 very\ long \ token

이는

{ "token_1", "token_2", "very long token" }

정책 3 : 따옴표 안에 토큰 배치

이 접근 방식은 예를 들어 MSExcel에서 생성 된 CSV 파일에 사용됩니다. 따옴표 사이의 모든 것은 토큰으로 간주됩니다. 따옴표 "가 토큰의 일부인 경우 두 배가 ""됩니다. 토큰 문자열은 다음과 같습니다.

token_1,token_2,"token2,5"

이는

{ "token_1", "token_2", "token2,5" }

암호

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
    }
}

답변

7 TheRubberDuck Sep 19 2020 at 01:42

나는 이것이 클래스에 속하는지 확신하지 못합니다-적어도 하나는 아닙니다!

한 걸음 물러서서 무엇이 통합되고 각 "전략"을 구분하는지 살펴보십시오. 그들은 모두 입력 문자열을 변수 구분 기호를 기반으로 토큰 목록으로 변환해야합니다. 그러나 세 가지 옵션 중 하나에서만 사용되는 속성이 있으며 분할 논리의 대부분은 해당 전략에 고유합니다.

제안 1 : 세 가지 "독립형"기능.

정말로 그것들을 정적 클래스에 넣거나 델리게이트 / 람다로 특별한 일을해야하지만, 궁극적으로 하나의 큰 클래스를 갖는 것으로부터 얻을 것이 거의 없습니다.

  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 = ' ') { ... }

출력이 입력 매개 변수를 전달하도록하려면 그렇게하는 훨씬 더 가벼운 클래스를 만들 수 있습니다. 그 속성은 다음과 같습니다 readonly. 변경하고 다시 계산해야하는 경우 함수를 다시 호출하면됩니다. 결국, 그것은 본질적으로 현재 수업에서하고있는 일입니다!

또 다른 장점 : 분할을위한 새로운 전략이 떠오르면 다른 기능에 영향을주지 않고 새로운 기능을 생성 할 수 있습니다. 모두 독립적으로 테스트, 편집 및 삭제할 수 있습니다.

제안 2 : 추상 기본 클래스를 확장하는 세 가지 구체적인 클래스.

나는 당신이 _Tokens속성으로 했던 일을 좋아 합니다. 그것은 당신이 정말로 필요로 할 때까지 계산을 연기 할 수있게하여 당신이 원하지 않는 경우에 도움이됩니다. 또한 지원하는 사용 사례 ( "독립 실행 형"기능에서 지원되지 않음)는 예를 들어 이스케이프 문자를 변경하고 결과가 자동으로 "무효화"되도록하는 것입니다.

이러한 동작을 유지하기 위해 다음과 같이 공통 요소를 추상 기본 클래스로 가져올 수 있습니다.

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;
  }
}

주목할만한 변화 :

  1. 실제 분할 논리가 없습니다. 각 전략은 자체적으로 제공됩니다.
  2. 전략 별 속성이 없습니다. 이스케이프 기반 전략이 따옴표 문자에 대한 속성을 가질 필요가 없으며 그 반대의 경우도 마찬가지입니다.
  3. 직접 설정하는 대신 _tokens = null속성을 호출해야합니다 InvalidateResult. 이것은 기본 클래스에 포함 된 논리를 유지하는 것을 허용 _tokens합니다 private.
  4. Tokens은 (는) 공개이며 IEnumerable. 이를 통해 소비자는을 사용할 수 foreach있지만 직접 수정하는 것은 권장하지 않습니다.

이제 기본 클래스에는 정확히 하나의 작업이 ComputeTokens있습니다. 이를 위해 속성을 만들어야하는 경우 자체 전략 별 논리를 기반으로 할 수 있습니다. 이러한 속성이 변경 될 때 이전에 계산 된 토큰을 무효화해야하는 경우 InvalidateResult.

다음은 전략 하위 클래스의 대략적인 예입니다.

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
  }
}

기타 관찰

  1. 구분 기호를 지정할 수 있지만 항상 공백을 압축합니다. "a,a and b,b"구분 기호로 분할 ","하면 {"a", "a and b", "b"}다시 돌아올 것으로 예상 하지만 실제로는 얻을 수 {"a", "a and b", "b"}있습니다.
  2. 구분 기호 등을 공개적으로 읽을 수 있다면 소스 문자열도 노출하지 않는 이유는 무엇입니까? SourceString위의 추상 클래스를 참조하십시오 .
  3. 나는 (상대적으로 새로운) 표현 본문 속성 접근자가 단순한 속성에 대해 더 나은 것을 발견했습니다. Count위의 추상 클래스를 참조하십시오 .
  4. 실수로 nullif 문의 조건으로 변수에 할당 하는 것이 가능하지 않다고 생각합니다 . 는 유효한 조건이 되려면 이어야합니다 (따라서 nullable이 아님)와 x = null동일한 유형으로 평가 되기 때문 입니다. 그래도 피하고 싶다면라고 말할 수 있습니다 .xboolx == nullx is null
  5. 다른 사람들이 언급했듯이 속성 앞에 _. 공개와 비공개를 구분하는 것이 아니라 지역 변수와 클래스 필드를 구분합니다. 그러나 개인적으로 나는 _그 경우 에도 사용하지 않고 대신 this.필요한 경우 선호 합니다. 그러나 전반적으로 이에 대해 유연하게 대처하고 기존 팀이나 프로젝트에서 이미 확립 된 패턴을 따라야합니다.
  6. 또한 다른 사람들이 언급했듯이 var가능하면 변수를 선언 할 때 사용하십시오. 좋은 IDE는 변수 위로 마우스를 가져 가면 유형을 알려줄 수 있으며 그 이름은 유형이 없어도 해당 유형을 알려줍니다.
  7. 그 메모에서 피 이름은 좋아 c하고 l. i루프 / 인덱스 변수로서 관용적이기 때문에 괜찮지 만 다른 것들은 이해하기 위해 추가 컨텍스트가 필요합니다. 소스 코드 문자는 저렴하므로 currentChar및 을 사용하여 가독성을 높이려면 비용을 지불하십시오 finishedTokens.
  8. 소스를 string로 번역 할 필요가 없습니다 char[]. 이미 string인덱스로 문자에 액세스 할 수 있습니다 .
9 Heslacher Sep 18 2020 at 16:53
  • 당신은하지 말았어야 Whitespaces대한 GET 전용 속성으로 만 비공개로 readonly필드와 당신은 당신이 매우 자주 사용하고 있기 때문에 그 정규식 컴파일해야한다.

  • 사용 region은 antypattern으로 간주됩니다.

  • 개인 필드에만 밑줄 접두사를 사용하십시오. 메서드 나 속성에 사용하지 마십시오.

  • 할당의 오른쪽에서 변수 유형이 명확 var하면 구체적인 유형 대신 사용해야 합니다.

  • 전달 된 ctor-argument 가 또는 _sourceString일 수 string.Empty있기 때문에 코드가 많이 수행되고 dataInput있습니다 . .NET Framework에서 예외를 던지고 싶습니다 .nullstring.Emptyctor

  • 변수를 다른 변수에 할당 한 다음 결과 변수를 조작하는 대신 다음과 같이 한 줄에서 수행 할 수 있습니다.

    string data = Whitespaces.Replace(_sourceString, " ");  
    

    대신에

    string data = _sourceString;
    
    // Doing some basic transformations
    data = Whitespaces.Replace(data, " ");  
    
  • 배열의 단일 항목에만 액세스해야하고 앞을 볼 필요가없는 foreach경우 for루프 보다 우선해야합니다 .

5 AlexanderPetrov Sep 18 2020 at 19:51
  • 한 글자로 된 l이름은 나 에게 좋지 않은 것 같습니다.

  • 오류의 원인을 설명하는 메시지를 예외에 추가해야한다고 생각합니다.

  • 기본적으로 데이터에서 모든 공백을 제거합니다. 그러나 토큰 내부에 필요할 수 있습니다. 이를 지정하는 추가 옵션을 만들 수 있습니다.

1 Benj Sep 20 2020 at 00:13

훌륭한 피드백을 주신 모든 분들께 감사드립니다. FOS로 호스팅되는 내 코드에 대한 대부분의 변경 사항을 채택했습니다.https://github.com/pillepalle1/dotnet-pillepalle1 추가 유지 보수를 받게됩니다.

지금은 분할 논리를 세 가지 정적 확장 메서드로 압축했습니다. 또한 자동 토큰 무효화의 편의를 선택적으로 유지하기 위해 therubberduck 에서 제안한 래퍼를 만들었습니다.

내가 구현 한 제안

  • 변수 이름 지정l 보다 설명적인 이름으로 대체 된 것과 같은 변수 이름

  • 예외 메시지 가 추가되었습니다.

  • 토큰 콘텐츠 수정 확장 메서드에서 완전히 제거되었으며 래퍼에서 선택적으로 사용할 수 있습니다.

  • 지역 이 완전히 제거되었습니다.

  • 합리적이고 / 가능할 때마다 var 사용

  • 루프 선호 foreach이상 for루프 이상 반복 sourceString대신으로 변환 char[]첫번째

  • 입력 문자열ArgumentNullException 로 변환 null하는 대신 던지기String.Empty

  • RFC4180에 따른 CSV 분할

더 많은 변경 사항을 채택했지만 일부 제안 (예 : 관련 Whitespaces및 표현 본문 속성)은 새로운 구현에서 쓸모 없게되었습니다.

구현하지 않은 제안

  • 개인 / 보호 된 모든 항목에 대한 밑줄 이름 지정은 멤버 및 로컬 변수를 구분하는 것보다 더 합리적으로 보입니다 Tasks. 메서드는 동시성 검사를 수행하거나 (공개) 수행하지 않습니다 (개인).

암호

정적 토크 나이저 방법

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));
        }
    }
}

추상 래퍼 기본 클래스

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;
        }
    }
}

일반 토큰 화를위한 래퍼

using System.Collections.Immutable;

namespace pillepalle1.Text
{
    public class PlainStringTokenizer : AStringTokenizer
    {
        protected override ImmutableList<string> Tokenize()
        {
            return SourceString.SplitPlain(Delimiter);
        }
    }
}

견적 토큰 화를위한 래퍼

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);
        }
    }
}

이스케이프 토큰 화를위한 래퍼

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);
        }
    }
}