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<string> lines) : this(lines, null) { }
public LineEnumerator(IReadOnlyList<string> lines, IReadOnlyList<int> lineNumbers)
{
Lines = lines;
Index = -1;
LineNumbers = lineNumbers;
}
public IReadOnlyList<string> Lines { get; }
public IReadOnlyList<int> LineNumbers { get; }
public int Index { get; private set; }
public bool IsIndexInRange => Index > IndexOutOfRange && Index < (LineNumbers?.Count ?? Lines.Count);
public int LineNumber
{
get
{
if (!IsIndexInRange) { return IndexOutOfRange; }
return LineNumbers?[Index] ?? Index;
}
}
public string Current => IsIndexInRange ? Lines[LineNumber] : null;
public CharEnumerator Chars => IsIndexInRange ? new CharEnumerator(Current) : null;
public bool MoveNext() => ++Index < (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 => Index >= Index0 && Counter <= Length;
public char Current => IsIndexInRange ? Line[Index] : '\0';
public bool MoveNext()
{
if (!IsIndexInRange)
{
Index = Index0;
Counter++;
return true;
}
Index++;
Counter++;
return (Counter <= 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