using System;
using System.Collections;
using System.IO;
using System.Text;
namespace JouniHeikniemi.Tools.Text {
///
/// A data-reader style interface for reading CSV files.
///
public class CSVReader : IDisposable {
#region Private variables
private Stream stream;
private StreamReader reader;
#endregion
///
/// Create a new reader for the given stream.
///
/// The stream to read the CSV from.
public CSVReader(Stream s) : this(s, null) { }
///
/// Create a new reader for the given stream and encoding.
///
/// The stream to read the CSV from.
/// The encoding used.
public CSVReader(Stream s, Encoding enc) {
this.stream = s;
if (!s.CanRead) {
throw new CSVReaderException("Could not read the given CSV stream!");
}
reader = (enc != null) ? new StreamReader(s, enc) : new StreamReader(s);
}
///
/// Creates a new reader for the given text file path.
///
/// The name of the file to be read.
public CSVReader(string filename) : this(filename, null) { }
///
/// Creates a new reader for the given text file path and encoding.
///
/// The name of the file to be read.
/// The encoding used.
public CSVReader(string filename, Encoding enc)
: this(new FileStream(filename, FileMode.Open), enc) { }
///
/// Returns the fields for the next row of CSV data (or null if at eof)
///
/// A string array of fields or null if at the end of file.
public string[] GetCSVLine() {
string data = reader.ReadLine();
if (data == null) return null;
if (data.Length == 0) return new string[0];
ArrayList result = new ArrayList();
ParseCSVFields(result, data);
return (string[])result.ToArray(typeof(string));
}
// Parses the CSV fields and pushes the fields into the result arraylist
private void ParseCSVFields(ArrayList result, string data) {
int pos = -1;
while (pos < data.Length)
result.Add(ParseCSVField(data, ref pos));
}
// Parses the field at the given position of the data, modified pos to match
// the first unparsed position and returns the parsed field
private string ParseCSVField(string data, ref int startSeparatorPosition) {
if (startSeparatorPosition == data.Length-1) {
startSeparatorPosition++;
// The last field is empty
return "";
}
int fromPos = startSeparatorPosition + 1;
// Determine if this is a quoted field
if (data[fromPos] == '"') {
// If we're at the end of the string, let's consider this a field that
// only contains the quote
if (fromPos == data.Length-1) {
fromPos++;
return "\"";
}
// Otherwise, return a string of appropriate length with double quotes collapsed
// Note that FSQ returns data.Length if no single quote was found
int nextSingleQuote = FindSingleQuote(data, fromPos+1);
startSeparatorPosition = nextSingleQuote+1;
return data.Substring(fromPos+1, nextSingleQuote-fromPos-1).Replace("\"\"", "\"");
}
// The field ends in the next comma or EOL
int nextComma = data.IndexOf(',', fromPos);
if (nextComma == -1) {
startSeparatorPosition = data.Length;
return data.Substring(fromPos);
}
else {
startSeparatorPosition = nextComma;
return data.Substring(fromPos, nextComma-fromPos);
}
}
// Returns the index of the next single quote mark in the string
// (starting from startFrom)
private int FindSingleQuote(string data, int startFrom) {
int i = startFrom-1;
while (++i < data.Length)
if (data[i] == '"') {
// If this is a double quote, bypass the chars
if (i < data.Length-1 && data[i+1] == '"') {
i++;
continue;
}
else
return i;
}
// If no quote found, return the end value of i (data.Length)
return i;
}
///
/// Disposes the CSVReader. The underlying stream is closed.
///
public void Dispose() {
// Closing the reader closes the underlying stream, too
if (reader != null) reader.Close();
else if (stream != null)
stream.Close(); // In case we failed before the reader was constructed
GC.SuppressFinalize(this);
}
}
///
/// Exception class for CSVReader exceptions.
///
public class CSVReaderException : ApplicationException {
///
/// Constructs a new exception object with the given message.
///
/// The exception message.
public CSVReaderException(string message) : base(message) { }
}
}
|