diff --git a/SabreTools.Library/Readers/SeparatedValueReader.cs b/SabreTools.Library/Readers/SeparatedValueReader.cs
index 249878b6..a512ef0f 100644
--- a/SabreTools.Library/Readers/SeparatedValueReader.cs
+++ b/SabreTools.Library/Readers/SeparatedValueReader.cs
@@ -1,12 +1,182 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
-using System.Threading.Tasks;
+using System.Text.RegularExpressions;
namespace SabreTools.Library.Readers
{
- public class SeparatedValueReader
+ public class SeparatedValueReader : IDisposable
{
+ ///
+ /// Internal stream reader for inputting
+ ///
+ private StreamReader sr;
+
+ ///
+ /// Internal value to say how many fields should be written
+ ///
+ private int fields = -1;
+
+ ///
+ /// Get if at end of stream
+ ///
+ public bool EndOfStream
+ {
+ get
+ {
+ return sr?.EndOfStream ?? true;
+ }
+ }
+
+ ///
+ /// 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;
+
+ ///
+ /// Constructor for reading from a file
+ ///
+ public SeparatedValueReader(string filename)
+ {
+ sr = new StreamReader(filename);
+ }
+
+ ///
+ /// Constructor for reading from a stream
+ ///
+ public SeparatedValueReader(Stream stream, Encoding encoding)
+ {
+ sr = new StreamReader(stream, encoding);
+ }
+
+ ///
+ /// Read the header line
+ ///
+ public bool ReadHeader()
+ {
+ if (!Header)
+ throw new InvalidOperationException("No header line expected");
+
+ if (HeaderValues != 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
+ ///
+ public bool ReadNextLine()
+ {
+ if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
+ return false;
+
+ string fullLine = sr.ReadLine();
+
+ // 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.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
+ {
+ Line = fullLine.Split(Separator).Select(f => f.Trim()).ToList();
+ }
+
+ // If we don't have a header yet and are expecting one, read this as the header
+ if (Header && HeaderValues == null)
+ {
+ HeaderValues = Line;
+ fields = HeaderValues.Count;
+ }
+
+ // If we're verifying field counts and the numbers are off, error out
+ if (VerifyFieldCount && fields != -1 && Line.Count != fields)
+ throw new InvalidDataException($"Invalid row found, cannot continue: {fullLine}");
+
+ return true;
+ }
+
+ ///
+ /// Get the value for the current line for the current key
+ ///
+ public string GetValue(string key)
+ {
+ // No header means no key-based indexing
+ if (!Header)
+ throw new ArgumentException("No header expected so no keys can be used");
+
+ // If we don't have the key, return null;
+ if (!HeaderValues.Contains(key))
+ return null;
+
+ int index = HeaderValues.IndexOf(key);
+ if (Line.Count() < index)
+ throw new ArgumentException($"Current line doesn't have index {index}");
+
+ return Line[index];
+ }
+
+ ///
+ /// Get the value for the current line for the current index
+ ///
+ public string GetValue(int index)
+ {
+ if (Line.Count() < index)
+ throw new ArgumentException($"Current line doesn't have index {index}");
+
+ return Line[index];
+ }
+
+ ///
+ /// Dispose of the underlying reader
+ ///
+ public void Dispose()
+ {
+ sr.Dispose();
+ }
}
}