IO namespace

This commit is contained in:
Matt Nadareski
2020-08-01 22:46:28 -07:00
parent 73a8c663a6
commit 41d3d0c848
16 changed files with 41 additions and 50 deletions

View File

@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.Library.Data;
namespace SabreTools.Library.IO
{
public class ClrMameProReader : IDisposable
{
/// <summary>
/// Internal stream reader for inputting
/// </summary>
private StreamReader sr;
/// <summary>
/// Get if at end of stream
/// </summary>
public bool EndOfStream
{
get
{
return sr?.EndOfStream ?? true;
}
}
/// <summary>
/// Contents of the currently read line as an internal item
/// </summary>
public Dictionary<string, string> Internal { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Current internal item name
/// </summary>
public string InternalName { get; private set; } = null;
/// <summary>
/// Get if we should be making DosCenter exceptions
/// </summary>
public bool DosCenter { get; set; } = false;
/// <summary>
/// Current row type
/// </summary>
public CmpRowType RowType { get; private set; } = CmpRowType.None;
/// <summary>
/// Contents of the currently read line as a standalone item
/// </summary>
public KeyValuePair<string, string>? Standalone { get; private set; } = null;
/// <summary>
/// Current top-level being read
/// </summary>
public string TopLevel { get; private set; } = string.Empty;
/// <summary>
/// Constructor for opening a write from a file
/// </summary>
public ClrMameProReader(string filename)
{
sr = new StreamReader(filename);
DosCenter = true;
}
/// <summary>
/// Constructor for opening a write from a stream and encoding
/// </summary>
public ClrMameProReader(Stream stream, Encoding encoding)
{
sr = new StreamReader(stream, encoding);
DosCenter = true;
}
/// <summary>
/// Read the next line in the file
/// </summary>
public bool ReadNextLine()
{
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
return false;
string line = sr.ReadLine().Trim();
ProcessLine(line);
return true;
}
/// <summary>
/// Process the current line and extract out values
/// </summary>
private void ProcessLine(string line)
{
// Standalone (special case for DC dats)
if (line.StartsWith("Name:"))
{
string temp = line.Substring("Name:".Length).Trim();
line = $"Name: {temp}";
}
// Comment
if (line.StartsWith("#"))
{
Internal = null;
InternalName = null;
RowType = CmpRowType.Comment;
Standalone = null;
}
// Top-level
else if (Regex.IsMatch(line, Constants.HeaderPatternCMP))
{
GroupCollection gc = Regex.Match(line, Constants.HeaderPatternCMP).Groups;
string normalizedValue = gc[1].Value.ToLowerInvariant();
Internal = null;
InternalName = null;
RowType = CmpRowType.TopLevel;
Standalone = null;
TopLevel = normalizedValue;
}
// Internal
else if (Regex.IsMatch(line, Constants.InternalPatternCMP))
{
GroupCollection gc = Regex.Match(line, Constants.InternalPatternCMP).Groups;
string normalizedValue = gc[1].Value.ToLowerInvariant();
string[] linegc = SplitLineAsCMP(gc[2].Value);
Internal = new Dictionary<string, string>();
for (int i = 0; i < linegc.Length; i++)
{
string key = linegc[i].Replace("\"", string.Empty);
if (string.IsNullOrWhiteSpace(key))
continue;
string value = string.Empty;
// Special case for DC-style dats, only a few known fields
if (DosCenter)
{
// If we have a name
if (key == "name")
{
while (++i < linegc.Length && linegc[i] != "size" && linegc[i] != "date" && linegc[i] != "crc")
{
value += $"{linegc[i]}";
}
value = value.Trim();
i--;
}
// If we have a date (split into 2 parts)
else if (key == "date")
{
value = $"{linegc[++i].Replace("\"", string.Empty)} {linegc[++i].Replace("\"", string.Empty)}";
}
// Default case
else
{
value = linegc[++i].Replace("\"", string.Empty);
}
}
else
{
// Special cases for standalone statuses
if (key == "baddump" || key == "good" || key == "nodump" || key == "verified")
{
value = key;
key = "status";
}
// Special case for standalone sample
else if (normalizedValue == "sample")
{
value = key;
key = "name";
}
// Default case
else
{
value = linegc[++i].Replace("\"", string.Empty);
}
}
Internal[key] = value;
RowType = CmpRowType.Internal;
Standalone = null;
}
InternalName = normalizedValue;
}
// Standalone
else if (Regex.IsMatch(line, Constants.ItemPatternCMP))
{
GroupCollection gc = Regex.Match(line, Constants.ItemPatternCMP).Groups;
string itemval = gc[2].Value.Replace("\"", string.Empty);
Internal = null;
InternalName = null;
RowType = CmpRowType.Standalone;
Standalone = new KeyValuePair<string, string>(gc[1].Value, itemval);
}
// End section
else if (Regex.IsMatch(line, Constants.EndPatternCMP))
{
Internal = null;
InternalName = null;
RowType = CmpRowType.EndTopLevel;
Standalone = null;
TopLevel = null;
}
// Invalid (usually whitespace)
else
{
Internal = null;
InternalName = null;
RowType = CmpRowType.None;
Standalone = null;
}
}
/// <summary>
/// Split a line as if it were a CMP rom line
/// </summary>
/// <param name="s">Line to split</param>
/// <returns>Line split</returns>
/// <remarks>Uses code from http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes</remarks>
private string[] SplitLineAsCMP(string s)
{
// Get the opening and closing brace locations
int openParenLoc = s.IndexOf('(');
int closeParenLoc = s.LastIndexOf(')');
// Now remove anything outside of those braces, including the braces
s = s.Substring(openParenLoc + 1, closeParenLoc - openParenLoc - 1);
s = s.Trim();
// Now we get each string, divided up as cleanly as possible
string[] matches = Regex
.Matches(s, Constants.InternalPatternAttributesCMP)
.Cast<Match>()
.Select(m => m.Groups[0].Value)
.ToArray();
return matches;
}
/// <summary>
/// Dispose of the underlying reader
/// </summary>
public void Dispose()
{
sr.Dispose();
}
}
}

View File

@@ -0,0 +1,486 @@
using System;
using System.IO;
using System.Text;
namespace SabreTools.Library.IO
{
/// <summary>
/// ClrMamePro writer patterned heavily off of XmlTextWriter
/// </summary>
/// <see cref="https://referencesource.microsoft.com/#System.Xml/System/Xml/Core/XmlTextWriter.cs"/>
public class ClrMameProWriter : IDisposable
{
/// <summary>
/// State machine state for use in the table
/// </summary>
private enum State
{
Start,
Prolog,
Element,
Attribute,
Content,
AttrOnly,
Epilog,
Error,
Closed,
}
/// <summary>
/// Potential token types
/// </summary>
private enum Token
{
None,
Standalone,
StartElement,
EndElement,
LongEndElement,
StartAttribute,
EndAttribute,
Content,
}
/// <summary>
/// Tag information for the stack
/// </summary>
private struct TagInfo
{
public string Name;
public bool Mixed;
public void Init()
{
Name = null;
Mixed = false;
}
}
/// <summary>
/// Internal stream writer
/// </summary>
private StreamWriter sw;
/// <summary>
/// Stack for tracking current node
/// </summary>
private TagInfo[] stack;
/// <summary>
/// Pointer to current top element in the stack
/// </summary>
private int top;
/// <summary>
/// State table for determining the state machine
/// </summary>
private readonly State[] stateTable = {
// State.Start State.Prolog State.Element State.Attribute State.Content State.AttrOnly State.Epilog
//
/* Token.None */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.Standalone */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.StartElement */ State.Element, State.Element, State.Element, State.Element, State.Element, State.Error, State.Element,
/* Token.EndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.LongEndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.StartAttribute */ State.AttrOnly, State.Error, State.Attribute, State.Attribute, State.Error, State.Error, State.Error,
/* Token.EndAttribute */ State.Error, State.Error, State.Error, State.Element, State.Error, State.Epilog, State.Error,
/* Token.Content */ State.Content, State.Content, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
};
/// <summary>
/// Current state in the machine
/// </summary>
private State currentState;
/// <summary>
/// Last seen token
/// </summary>
private Token lastToken;
/// <summary>
/// Get if quotes should surround attribute values
/// </summary>
public bool Quotes { get; set; }
/// <summary>
/// Constructor for opening a write from a file
/// </summary>
public ClrMameProWriter(string filename)
{
sw = new StreamWriter(filename);
Quotes = true;
// Element stack
stack = new TagInfo[10];
top = 0;
stack[top].Init();
}
/// <summary>
/// Constructor for opening a write from a stream and encoding
/// </summary>
public ClrMameProWriter(Stream stream, Encoding encoding)
{
sw = new StreamWriter(stream, encoding);
Quotes = true;
// Element stack
stack = new TagInfo[10];
top = 0;
stack[top].Init();
}
/// <summary>
/// Write the start of an element node
/// </summary>
public void WriteStartElement(string name)
{
try
{
AutoComplete(Token.StartElement);
PushStack();
stack[top].Name = name;
sw.Write(name);
sw.Write(" (");
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write the end of an element node
/// </summary>
public void WriteEndElement()
{
InternalWriteEndElement(false);
}
/// <summary>
/// Write the end of a mixed element node
/// </summary>
public void WriteFullEndElement()
{
InternalWriteEndElement(true);
}
/// <summary>
/// Write a complete element with content
/// </summary>
public void WriteElementString(string name, string value)
{
WriteStartElement(name);
WriteString(value);
WriteEndElement();
}
/// <summary>
/// Write the start of an attribute node
/// </summary>
public void WriteStartAttribute(string name)
{
try
{
AutoComplete(Token.StartAttribute);
sw.Write(name);
sw.Write(" ");
if (Quotes)
sw.Write("\"");
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write the end of an attribute node
/// </summary>
public void WriteEndAttribute()
{
try
{
AutoComplete(Token.EndAttribute);
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write a complete attribute with content
/// </summary>
public void WriteAttributeString(string name, string value)
{
WriteStartAttribute(name);
WriteString(value);
WriteEndAttribute();
}
/// <summary>
/// Write a standalone attribute
/// </summary>
public void WriteStandalone(string name, string value, bool? quoteOverride = null)
{
try
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException();
AutoComplete(Token.Standalone);
sw.Write(name);
sw.Write(" ");
if ((quoteOverride == null && Quotes)
|| (quoteOverride == true))
{
sw.Write("\"");
}
sw.Write(value);
if ((quoteOverride == null && Quotes)
|| (quoteOverride == true))
{
sw.Write("\"");
}
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write a string content value
/// </summary>
public void WriteString(string value)
{
try
{
if (!string.IsNullOrEmpty(value))
{
AutoComplete(Token.Content);
sw.Write(value);
}
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Close the writer
/// </summary>
public void Close()
{
try
{
AutoCompleteAll();
}
catch
{
// Don't fail at this step
}
finally
{
currentState = State.Closed;
sw.Close();
}
}
/// <summary>
/// Close and dispose
/// </summary>
public void Dispose()
{
Close();
sw.Dispose();
}
/// <summary>
/// Flush the base TextWriter
/// </summary>
public void Flush()
{
sw.Flush();
}
/// <summary>
/// Prepare for the next token to be written
/// </summary>
private void AutoComplete(Token token)
{
// Handle the error cases
if (currentState == State.Closed)
throw new InvalidOperationException();
else if (currentState == State.Error)
throw new InvalidOperationException();
State newState = stateTable[(int)token * 7 + (int)currentState];
if (newState == State.Error)
throw new InvalidOperationException();
// TODO: Figure out how to get attributes on their own lines ONLY if an element contains both attributes and elements
switch (token)
{
case Token.StartElement:
case Token.Standalone:
if (currentState == State.Attribute)
{
WriteEndAttributeQuote();
WriteEndStartTag(false);
}
else if (currentState == State.Element)
{
WriteEndStartTag(false);
}
if (currentState != State.Start)
Indent(false);
break;
case Token.EndElement:
case Token.LongEndElement:
if (currentState == State.Attribute)
WriteEndAttributeQuote();
if (currentState == State.Content)
token = Token.LongEndElement;
else
WriteEndStartTag(token == Token.EndElement);
break;
case Token.StartAttribute:
if (currentState == State.Attribute)
{
WriteEndAttributeQuote();
sw.Write(' ');
}
else if (currentState == State.Element)
{
sw.Write(' ');
}
break;
case Token.EndAttribute:
WriteEndAttributeQuote();
break;
case Token.Content:
if (currentState == State.Element && lastToken != Token.Content)
WriteEndStartTag(false);
if (newState == State.Content)
stack[top].Mixed = true;
break;
default:
throw new InvalidOperationException();
}
currentState = newState;
lastToken = token;
}
/// <summary>
/// Autocomplete all open element nodes
/// </summary>
private void AutoCompleteAll()
{
while (top > 0)
{
WriteEndElement();
}
}
/// <summary>
/// Internal helper to write the end of an element
/// </summary>
private void InternalWriteEndElement(bool longFormat)
{
try
{
if (top <= 0)
throw new InvalidOperationException();
AutoComplete(longFormat ? Token.LongEndElement : Token.EndElement);
if (this.lastToken == Token.LongEndElement)
{
Indent(true);
sw.Write(')');
}
top--;
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Internal helper to write the end of a tag
/// </summary>
private void WriteEndStartTag(bool empty)
{
if (empty)
sw.Write(" )");
}
/// <summary>
/// Internal helper to write the end of an attribute
/// </summary>
private void WriteEndAttributeQuote()
{
if (Quotes)
sw.Write("\"");
}
/// <summary>
/// Internal helper to indent a node, if necessary
/// </summary>
private void Indent(bool beforeEndElement)
{
if (top == 0)
{
sw.WriteLine();
}
else if (!stack[top].Mixed)
{
sw.WriteLine();
int i = beforeEndElement ? top - 1 : top;
for (; i > 0; i--)
{
sw.Write('\t');
}
}
}
/// <summary>
/// Move up one element in the stack
/// </summary>
private void PushStack()
{
if (top == stack.Length - 1)
{
TagInfo[] na = new TagInfo[stack.Length + 10];
if (top > 0) Array.Copy(stack, na, top + 1);
stack = na;
}
top++; // Move up stack
stack[top].Init();
}
}
}

View File

@@ -0,0 +1,27 @@
namespace SabreTools.Library.IO
{
/// <summary>
/// Different types of CMP rows being parsed
/// </summary>
public enum CmpRowType
{
None,
TopLevel,
Standalone,
Internal,
Comment,
EndTopLevel,
}
/// <summary>
/// Different types of INI rows being parsed
/// </summary>
public enum IniRowType
{
None,
SectionHeader,
KeyValue,
Comment,
Invalid,
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.Library.IO
{
public class IniReader : IDisposable
{
/// <summary>
/// Internal stream reader for inputting
/// </summary>
private StreamReader sr;
/// <summary>
/// Get if at end of stream
/// </summary>
public bool EndOfStream
{
get
{
return sr?.EndOfStream ?? true;
}
}
/// <summary>
/// Contents of the currently read line as a key value pair
/// </summary>
public KeyValuePair<string, string>? KeyValuePair { get; private set; } = null;
/// <summary>
/// Contents of the currently read line
/// </summary>
public string Line { get; private set; } = string.Empty;
/// <summary>
/// Current row type
/// </summary>
public IniRowType RowType { get; private set; } = IniRowType.None;
/// <summary>
/// Current section being read
/// </summary>
public string Section { get; private set; } = string.Empty;
/// <summary>
/// Validate that rows are in key=value format
/// </summary>
public bool ValidateRows { get; set; } = true;
/// <summary>
/// Constructor for reading from a file
/// </summary>
public IniReader(string filename)
{
sr = new StreamReader(filename);
}
/// <summary>
/// Constructor for reading from a stream
/// </summary>
public IniReader(Stream stream, Encoding encoding)
{
sr = new StreamReader(stream, encoding);
}
/// <summary>
/// Read the next line in the INI file
/// </summary>
public bool ReadNextLine()
{
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
return false;
Line = sr.ReadLine().Trim();
ProcessLine();
return true;
}
/// <summary>
/// Process the current line and extract out values
/// </summary>
private void ProcessLine()
{
// Comment
if (Line.StartsWith(";"))
{
KeyValuePair = null;
RowType = IniRowType.Comment;
}
// Section
else if (Line.StartsWith("[") && Line.EndsWith("]"))
{
KeyValuePair = null;
RowType = IniRowType.SectionHeader;
Section = Line.TrimStart('[').TrimEnd(']');
}
// KeyValuePair
else if (Line.Contains("="))
{
// Split the line by '=' for key-value pairs
string[] data = Line.Split('=');
// If the value field contains an '=', we need to put them back in
string key = data[0].Trim();
string value = string.Join("=", data.Skip(1)).Trim();
KeyValuePair = new KeyValuePair<string, string>(key, value);
RowType = IniRowType.KeyValue;
}
// Empty
else if (string.IsNullOrEmpty(Line))
{
KeyValuePair = null;
Line = string.Empty;
RowType = IniRowType.None;
}
// Invalid
else
{
KeyValuePair = null;
RowType = IniRowType.Invalid;
if (ValidateRows)
throw new InvalidDataException($"Invalid INI row found, cannot continue: {Line}");
}
}
/// <summary>
/// Dispose of the underlying reader
/// </summary>
public void Dispose()
{
sr.Dispose();
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Text;
namespace SabreTools.Library.IO
{
public class IniWriter : IDisposable
{
/// <summary>
/// Internal stream writer for outputting
/// </summary>
private StreamWriter sw;
/// <summary>
/// Constructor for writing to a file
/// </summary>
public IniWriter(string filename)
{
sw = new StreamWriter(filename);
}
/// <summary>
/// Consturctor for writing to a stream
/// </summary>
public IniWriter(Stream stream, Encoding encoding)
{
sw = new StreamWriter(stream, encoding);
}
/// <summary>
/// Write a section tag
/// </summary>
public void WriteSection(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException(nameof(value));
sw.WriteLine($"[{value.TrimStart('[').TrimEnd(']')}]");
}
/// <summary>
/// Write a key value pair
/// </summary>
public void WriteKeyValuePair(string key, string value)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException(nameof(key));
if (value == null)
value = string.Empty;
sw.WriteLine($"{key}={value}");
}
/// <summary>
/// Write a comment
/// </summary>
public void WriteComment(string value)
{
if (value == null)
value = string.Empty;
sw.WriteLine($";{value}");
}
/// <summary>
/// Write a generic string
/// </summary>
public void WriteString(string value)
{
if (value == null)
value = string.Empty;
sw.Write(value);
}
/// <summary>
/// Write a newline
/// </summary>
public void WriteLine()
{
sw.WriteLine();
}
/// <summary>
/// Flush the underlying writer
/// </summary>
public void Flush()
{
sw.Flush();
}
/// <summary>
/// Dispose of the underlying writer
/// </summary>
public void Dispose()
{
sw.Dispose();
}
}
}

View File

@@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace SabreTools.Library.IO
{
public class SeparatedValueReader : IDisposable
{
/// <summary>
/// Internal stream reader for inputting
/// </summary>
private StreamReader sr;
/// <summary>
/// Internal value to say how many fields should be written
/// </summary>
private int fields = -1;
/// <summary>
/// Get if at end of stream
/// </summary>
public bool EndOfStream
{
get
{
return sr?.EndOfStream ?? true;
}
}
/// <summary>
/// Assume the first row is a header
/// </summary>
public bool Header { get; set; } = true;
/// <summary>
/// Header row values
/// </summary>
public List<string> HeaderValues { get; set; } = null;
/// <summary>
/// Get the current line values
/// </summary>
public List<string> Line { get; private set; } = null;
/// <summary>
/// Get the current line number
/// </summary>
public long LineNumber { get; private set; } = -1;
/// <summary>
/// Assume that values are wrapped in quotes
/// </summary>
public bool Quotes { get; set; } = true;
/// <summary>
/// Set what character should be used as a separator
/// </summary>
public char Separator { get; set; } = ',';
/// <summary>
/// Set if field count should be verified from the first row
/// </summary>
public bool VerifyFieldCount { get; set; } = true;
/// <summary>
/// Constructor for reading from a file
/// </summary>
public SeparatedValueReader(string filename)
{
sr = new StreamReader(filename);
}
/// <summary>
/// Constructor for reading from a stream
/// </summary>
public SeparatedValueReader(Stream stream, Encoding encoding)
{
sr = new StreamReader(stream, encoding);
}
/// <summary>
/// Read the header line
/// </summary>
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();
}
/// <summary>
/// Read the next line in the separated value file
/// </summary>
public bool ReadNextLine()
{
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
return false;
string fullLine = sr.ReadLine();
LineNumber++;
// 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<string>();
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;
}
/// <summary>
/// Get the value for the current line for the current key
/// </summary>
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];
}
/// <summary>
/// Get the value for the current line for the current index
/// </summary>
public string GetValue(int index)
{
if (Line.Count() < index)
throw new ArgumentException($"Current line doesn't have index {index}");
return Line[index];
}
/// <summary>
/// Dispose of the underlying reader
/// </summary>
public void Dispose()
{
sr.Dispose();
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.IO;
using System.Text;
namespace SabreTools.Library.IO
{
public class SeparatedValueWriter : IDisposable
{
/// <summary>
/// Internal stream writer for outputting
/// </summary>
private StreamWriter sw;
/// <summary>
/// Internal value if we've written a header before
/// </summary>
private bool header = false;
/// <summary>
/// Internal value if we've written our first line before
/// </summary>
private bool firstRow = false;
/// <summary>
/// Internal value to say how many fields should be written
/// </summary>
private int fields = -1;
/// <summary>
/// Set if values should be wrapped in quotes
/// </summary>
public bool Quotes { get; set; } = true;
/// <summary>
/// Set what character should be used as a separator
/// </summary>
public char Separator { get; set; } = ',';
/// <summary>
/// Set if field count should be verified from the first row
/// </summary>
public bool VerifyFieldCount { get; set; } = true;
/// <summary>
/// Constructor for writing to a file
/// </summary>
public SeparatedValueWriter(string filename)
{
sw = new StreamWriter(filename);
}
/// <summary>
/// Consturctor for writing to a stream
/// </summary>
public SeparatedValueWriter(Stream stream, Encoding encoding)
{
sw = new StreamWriter(stream, encoding);
}
/// <summary>
/// Write a header row
/// </summary>
public void WriteHeader(string[] headers)
{
// If we haven't written anything out, we can write headers
if (!header && !firstRow)
WriteValues(headers);
header = true;
}
/// <summary>
/// Write a value row
/// </summary>
public void WriteValues(object[] values, bool newline = true)
{
// If the writer can't be used, we error
if (sw == null || !sw.BaseStream.CanWrite)
throw new ArgumentException(nameof(sw));
// If the separator character is invalid, we error
if (Separator == default(char))
throw new ArgumentException(nameof(Separator));
// If we have the first row, set the bool and the field count
if (!firstRow)
{
firstRow = true;
if (VerifyFieldCount && fields == -1)
fields = values.Length;
}
// Get the number of fields to write out
int fieldCount = values.Length;
if (VerifyFieldCount)
fieldCount = Math.Min(fieldCount, fields);
// Iterate over the fields, writing out each
bool firstField = true;
for (int i = 0; i < fieldCount; i++)
{
var value = values[i];
if (!firstField)
sw.Write(Separator);
if (Quotes)
sw.Write("\"");
sw.Write(value?.ToString() ?? string.Empty);
if (Quotes)
sw.Write("\"");
firstField = false;
}
// If we need to pad out the number of fields, add empties
if (VerifyFieldCount && values.Length < fields)
{
for (int i = 0; i < fields - values.Length; i++)
{
sw.Write(Separator);
if (Quotes)
sw.Write("\"\"");
}
}
// Add a newline, if needed
if (newline)
sw.WriteLine();
}
/// <summary>
/// Write a generic string
/// </summary>
public void WriteString(string value)
{
if (string.IsNullOrEmpty(value))
return;
sw.Write(value);
}
/// <summary>
/// Write a newline
/// </summary>
public void WriteLine()
{
sw.WriteLine();
}
/// <summary>
/// Flush the underlying writer
/// </summary>
public void Flush()
{
sw.Flush();
}
/// <summary>
/// Dispose of the underlying writer
/// </summary>
public void Dispose()
{
sw.Dispose();
}
}
}