Splitting a string on delimiter outside of quotes while returning item-by-item One of the more recent functions I've needed for personal and work purposes is to split a string on a delimiter, but while ignoring the delimiter when inside a quote. Just as well, if within a quote then two adjacent double-quotes ("") should be treated as a double-quote escape, and as such should be output directly into the resultant string.


  Input:

"\"Doe, \"\"John\"\" Johnathan\", \"Doe, James\"".QuotedSplit(",")

  
  Output:

"Doe, \"\"John\"\" Johnathan", "Doe, James"



This actually posed a larger challenge than I originally anticipated, but in the end it was resolved pretty swiftly.

Obviously I want comments on all facets, but especially the idea of the performance of the method, and anything related to the yield return nature of it. I want it to be as lazy as possible, after all that's what good framework developers do, but I also want it to be robust.

The code is actually surprisingly long, but I think that's my fault for not abstracting it to a class, and I really do not want to abstract this to a class.

The method is as follows:

public static IEnumerable&lt;string&gt; QuotedSplit(this string s, string delim)
{
    var esb = new ExtendedStringBuilder(s.Length);

    bool inQuote = false;
    for (int i = 0; i &lt; s.Length; i++)
    {
        if (s[i] == '\"')
        {
            if (i &lt; s.Length - 1 &amp;&amp; s[i + 1] == '\"')
            {
                esb += s[i++];
                esb += s[i];
            }
            else
            {
                inQuote = !inQuote;
                esb += s[i];
            }
        }
        else if (!inQuote)
        {
            if (s[i] == delim[0])
            {
                var split = true;
                for (int j = 0; j &lt; delim.Length; j++)
                {
                    if (s.Length - 1 &lt;= i + j)
                    {
                        split = false;
                        break;
                    }

                    if (s[i + j] != delim[j])
                    {
                        split = false;
                        break;
                    }
                }

                if (split)
                {
                    yield return esb;
                    esb.Length = 0;
                    i += delim.Length - 1;
                    split = true;
                    continue;
                }
            }

            esb += s[i];
        }
        else if (inQuote)
        {
            esb += s[i];
        }
    }

    if (esb.EndsWith(delim))
    {
        esb.Length -= delim.Length;
    }

    yield return esb;
}


The unique advantage of this method is that it can take a string as the delimiter, and will lazily evaluate it, without creating a weird method structure (like string.Split does).

I haven't noticed any bugs with it, though I guess it's possible that there are some. I've used it in the production environment it's part of for a while with no issues.
You can simplify your logic a bit.  Once in a quote you don't need to check for anything until you either at the end of the string or find another quote. 

I also didn't use the ExtendedStringBuilder since I don't have that code and just used the StringBuilder class

From limited testing it returns the same as your function and from using Stopwatch class building a string of 10,000,000 random char it's a bit faster but nothing that couldn't be attributed to noise.  

First I would create another method to just read ahead.  It makes it easier to see if we equal the delimiter passed in 

private static IEnumerable&lt;char&gt; ReadNext(string str, int currentPosition, int count)
{
    for (var i = 0; i &lt; count; i++)
    {
        if (currentPosition + i &gt;= str.Length)
        {
            yield break;
        }
        else
        {
            yield return str[currentPosition + i];
        }
    }
}


I switch the for loop into a while so I can mess with the counter.  While you can change a counter in a for loop I generally frown on that as a developer I would not normally expect that.  With a while loop I would

public static IEnumerable&lt;string&gt; QuotedSplit2(string s, string delim)
{
    const char quote = '\"';

    var sb = new StringBuilder(s.Length);
    var counter = 0;
    while (counter &lt; s.Length)
    {
        // if starts with delmiter if so read ahead to see if matches
        if (delim[0] == s[counter] &amp;&amp; 
            delim.SequenceEqual(ReadNext(s, counter, delim.Length)))
        {
            yield return sb.ToString();
            sb.Clear();
            counter = counter + delim.Length; // Move the counter past the delimiter 
        }
        // if we hit a quote read until we hit another quote or end of string
        else if (s[counter] == quote)
        {
            sb.Append(s[counter++]);
            while (counter &lt; s.Length &amp;&amp; s[counter] != quote)
            {
                sb.Append(s[counter++]);
            }
            // if not end of string then we hit a quote add the quote
            if (counter &lt; s.Length)
            {
                sb.Append(s[counter++]);
            }
        }
        else
        {
            sb.Append(s[counter++]);
        }
    }

    if (sb.Length &gt; 0)
    {
        yield return sb.ToString();
    }
}


SequenceEqual isn't known to be fast but unless you have a large string you using as a delimiter I wouldn't worry about it.  If you do, then it might be worth changing the IEnumerable&lt;char&gt; into a string using String.Concat and comparing the string from String.Concat directly to the delim variable.  

Test

static void Main(string[] args)
{
    var random = new Random();
    var str = String.Concat(Enumerable.Range(1, 10000000).Select(_ =&gt; (char) random.Next(32, 122)));
    var timer = new Stopwatch();
    timer.Start();
    var result2 = QuotedSplit2(str, "1\\").ToArray();
    timer.Stop();
    Console.WriteLine(timer.ElapsedMilliseconds);
    timer.Reset();

    timer.Start();
    var result = QuotedSplit(str, "1\\").ToArray();
    timer.Stop();
    Console.WriteLine(timer.ElapsedMilliseconds);
    timer.Reset();

    Console.WriteLine(result2.SequenceEqual(result));
    Console.WriteLine(result2.Length);

    Console.ReadLine();
}