using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace SabreTools.Text.SeparatedValue
{
public class Reader : IDisposable
{
#region Fields
///
/// Get if at end of stream
///
public bool EndOfStream => _reader.EndOfStream;
///
/// Contents of the current line, unprocessed
///
public string? CurrentLine { get; private set; } = string.Empty;
///
/// Get the current line number
///
public long LineNumber { get; private set; } = 0;
///
/// Assume the first row is a header
///
public bool Header { get; set; } = true;
///
/// Header row values
///
public List? HeaderValues { get; set; } = null;
///
/// Get the current line values
///
public List? Line { get; private set; } = null;
///
/// Assume that values are wrapped in quotes
///
public bool Quotes { get; set; } = true;
///
/// Set what character should be used as a separator
///
public char Separator { get; set; } = ',';
///
/// Set if field count should be verified from the first row
///
public bool VerifyFieldCount { get; set; } = true;
#endregion
#region Private Properties
///
/// Internal stream reader
///
private readonly StreamReader _reader;
///
/// How many fields should be written
///
private int _fieldCount = -1;
#endregion
#region Constructors
///
/// Constructor for reading from a file
///
public Reader(string filename)
{
_reader = new StreamReader(filename);
}
///
/// Constructor for reading from a stream
///
public Reader(Stream stream, Encoding encoding)
{
_reader = new StreamReader(stream, encoding);
}
///
/// Constructor for reading from a stream reader
///
public Reader(StreamReader streamReader)
{
_reader = streamReader;
}
#endregion
///
/// Read the header line
///
///
///
/// Thrown if either:
/// - A header line is requested when is false.
/// - A header line has already been read when is true.
///
public bool ReadHeader()
{
if (!Header)
throw new InvalidOperationException("No header line expected");
if (HeaderValues is not null)
throw new InvalidOperationException("No more than 1 header row in a file allowed");
return ReadNextLine();
}
///
/// Read the next line in the separated value file
///
///
/// Thrown if an malformed line is encountered during processing
/// and is true.
///
public bool ReadNextLine()
{
if (_reader.BaseStream is null)
return false;
if (!_reader.BaseStream.CanRead || _reader.EndOfStream)
return false;
string? fullLine = _reader.ReadLine();
CurrentLine = fullLine;
LineNumber++;
if (fullLine is null)
return false;
// If we have quotes, we need to split specially
if (Quotes)
{
// https://stackoverflow.com/questions/3776458/split-a-comma-separated-string-with-both-quoted-and-unquoted-strings
var lineSplitRegex = new Regex($"(?:^|{Separator})(\"(?:[^\"]+|\"\")*\"|[^{Separator}]*)");
var temp = new List();
foreach (Match? match in lineSplitRegex.Matches(fullLine))
{
string? curr = match?.Value;
if (curr is null)
continue;
if (curr.Length == 0)
temp.Add("");
// Trim separator, whitespace, quotes, inter-quote whitespace
curr = curr.TrimStart(Separator).Trim().Trim('\"').Trim();
temp.Add(curr);
}
Line = temp;
}
// Otherwise, just split on the delimiter
else
{
var lineArr = fullLine.Split(Separator);
lineArr = Array.ConvertAll(lineArr, f => f.Trim());
Line = [.. lineArr];
}
// If we don't have a header yet and are expecting one, read this as the header
if (Header && HeaderValues is null)
{
HeaderValues = Line;
_fieldCount = HeaderValues.Count;
}
// If we're verifying field counts and the numbers are off, error out
if (VerifyFieldCount && _fieldCount != -1 && Line.Count != _fieldCount)
throw new InvalidDataException($"Invalid row found, cannot continue: {fullLine}");
return true;
}
///
/// Get the value for the current line for the current key
///
/// Case-sensitive key based on header values
/// Value associated with the key, null if the key doesn't exist
///
/// Thrown if any required properties are missing.
///
public string? GetValue(string key)
{
// No header means no key-based indexing
if (!Header)
throw new InvalidDataException("No header expected so no keys can be used");
// If we don't have the key, return null
if (HeaderValues is null)
throw new InvalidDataException($"Current line doesn't have key {key}");
if (!HeaderValues.Contains(key))
return null;
int index = HeaderValues.IndexOf(key);
return GetValue(index);
}
///
/// Get the value for the current line for the current index
///
/// Index into the current line
/// Value associated with the index
///
/// Thrown if is greater than the line count.
///
///
/// Thrown if any required properties are missing.
///
public string GetValue(int index)
{
if (Line is null)
throw new InvalidDataException($"Current line doesn't have index {index}");
if (Line.Count < index)
throw new ArgumentOutOfRangeException($"Current line doesn't have index {index}");
return Line[index];
}
#region IDisposable Implementation
///
/// Dispose of the underlying reader
///
public void Dispose()
{
_reader.Dispose();
}
#endregion
}
}