Enumerating text (ranges) vertically and horizontally I'm writing a parser and I need to iterate through text vertically (down to bottom) and horizontally (left to right).

This alone would be easy but some text parts need to be rescanned by another parser and for this I need to reiterate over random lines or a text range.

I wrote two enumerators.

One for enumerating all lines or only some of them:

internal class LineEnumerator
{
    private const int IndexOutOfRange = -1;

    public LineEnumerator(IReadOnlyList&lt;string&gt; lines) : this(lines, null) { }

    public LineEnumerator(IReadOnlyList&lt;string&gt; lines, IReadOnlyList&lt;int&gt; lineNumbers)
    {
        Lines = lines;
        Index = -1;
        LineNumbers = lineNumbers;
    }

    public IReadOnlyList&lt;string&gt; Lines { get; }
    public IReadOnlyList&lt;int&gt; LineNumbers { get; }
    public int Index { get; private set; }
    public bool IsIndexInRange =&gt; Index &gt; IndexOutOfRange &amp;&amp; Index &lt; (LineNumbers?.Count ?? Lines.Count);
    public int LineNumber
    {
        get
        {
            if (!IsIndexInRange) { return IndexOutOfRange; }
            return LineNumbers?[Index] ?? Index;
        }
    }
    public string Current =&gt; IsIndexInRange ? Lines[LineNumber] : null;

    public CharEnumerator Chars =&gt; IsIndexInRange ? new CharEnumerator(Current) : null;

    public bool MoveNext() =&gt; ++Index &lt; (LineNumbers?.Count ?? Lines.Count);
}


and another one for enumerating chars or only a particular range:

internal class CharEnumerator
{
    private const int IndexOutOfRange = -1;

    public CharEnumerator(string line) : this(line, 0, line.Length) { }

    public CharEnumerator(string line, int index0, int length)
    {
        Line = line;
        Index0 = index0;
        Length = length;
        Index = IndexOutOfRange;
    }

    public string Line { get; }
    public int Index0 { get; }
    public int Length { get; }
    public int Counter { get; private set; }
    public int Index { get; private set; }
    public bool IsIndexInRange =&gt; Index &gt;= Index0 &amp;&amp; Counter &lt;= Length;
    public char Current =&gt; IsIndexInRange ? Line[Index] : '\0';

    public bool MoveNext()
    {
        if (!IsIndexInRange)
        {
            Index = Index0;
            Counter++;
            return true;
        }
        Index++;
        Counter++;
        return (Counter &lt;= Length);
    }
}




They shouldn't just enumerate the text but also keep track of where they are so that I can throw exceptions with exact location if something goes wrong thus the line enumerator needs to work with real indexes even for a range. The same applies to the char enumerator.

I feel like the enumerators violate SRP (single responsibility principle) and I should create two more enumerators for enumerating a range of lines or a range of characters. Is this necessary or would you leave it like that?



Examples

var lines = new[] { "foo", "bar", "baz", "qux" };
var lineEnumerator = new LineEnumerator(lines);
while (lineEnumerator.MoveNext())
{
    Console.WriteLine(lineEnumerator.Current);
}



  foo bar baz qux


Console.WriteLine();
var lineRangeEnumerator = new LineEnumerator(lines, new[] { 1, 3 });
while (lineRangeEnumerator.MoveNext())
{
    Console.WriteLine(lineRangeEnumerator.Current);
}



  bar qux


Console.WriteLine();
var line = "foo bar baz";
var charEnumerator = new CharEnumerator(line);
while (charEnumerator.MoveNext())
{
    Console.WriteLine(charEnumerator.Current);
}



  f o o   b a r   b a z


Console.WriteLine();
var charRangeEnumerator = new CharEnumerator(line, 4, 3);
while (charRangeEnumerator.MoveNext())
{
    Console.WriteLine(charRangeEnumerator.Current);
}



  b a r
I would go with standard IEnumerator and IEnumerable, so it could be consumed as:

    static void Main(string[] args)
    {
        var template = new Template("foo", "bar", "baz", "qux");

        foreach (var row in template)
            Console.WriteLine(row.Text);

        Console.WriteLine();
        foreach (var row in template.Select(1, 3))
            Console.WriteLine(row.Text);

        Console.WriteLine();
        foreach (var row in template.Select(0))
            foreach (var column in row)
                Console.WriteLine(column.Text);

        Console.WriteLine();
        foreach (var row in template.Select(0))
            foreach (var column in row.Range(0,3))
                Console.WriteLine(column.Text);
    }


Where Select allows to subset by indecies provided, while Range is working in terms of start and count. Row and Column objects have Index (current selection) and Indecies (allowed to be selected) properties:

public class Template : IEnumerable&lt;Row&gt;
{
    public Template(params string[] lines)
    {
        Lines = lines;
    }

    public IEnumerable&lt;Row&gt; Range(int start, int count) =&gt; Select(Enumerable.Range(start, count));
    public IEnumerable&lt;Row&gt; Select(params int[] indecies) =&gt; Select(indecies as IEnumerable&lt;int&gt;);
    public IEnumerable&lt;Row&gt; Select(IEnumerable&lt;int&gt; indecies)
    {
        foreach (var index in indecies)
            yield return new Row(Lines.ElementAtOrDefault(index), index, indecies);
    }

    public IEnumerator&lt;Row&gt; GetEnumerator() =&gt; Range(0, Lines.Length).GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator();
    string[] Lines { get; }
}


And:

public struct Row : IEnumerable&lt;Column&gt;
{
    public Row(string text, int index, IEnumerable&lt;int&gt; indecies)
    {
        Text = text;
        Index = index;
        Indecies = indecies;
    }

    public string Text { get; }
    public int Index { get; }
    public IEnumerable&lt;int&gt; Indecies { get; }

    public IEnumerable&lt;Column&gt; Range(int start, int count) =&gt; Select(Enumerable.Range(start, count));
    public IEnumerable&lt;Column&gt; Select(params int[] indecies) =&gt; Select(indecies as IEnumerable&lt;int&gt;);
    public IEnumerable&lt;Column&gt; Select(IEnumerable&lt;int&gt; indecies)
    {
        foreach (var index in indecies)
            yield return new Column(Text.ElementAtOrDefault(index), index, indecies);
    }

    public IEnumerator&lt;Column&gt; GetEnumerator() =&gt; Range(0, Text.Length).GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator();
}


And:

public struct Column
{
    public Column(char text, int index, IEnumerable&lt;int&gt; indecies)
    {
        Text = text;
        Index = index;
        Indecies = indecies;
    }

    public char Text { get; }
    public int Index { get; }
    public IEnumerable&lt;int&gt; Indecies { get; }
}