165 lines
5.3 KiB
C#
165 lines
5.3 KiB
C#
// This code is in the public domain. I can't remember where I found it on the Web, but it
|
|
// didn't come with any license.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace CSVReader {
|
|
|
|
/// <summary>
|
|
/// A data-reader style interface for reading CSV files.
|
|
/// </summary>
|
|
public class CSVReader : IDisposable {
|
|
|
|
#region Private variables
|
|
|
|
private Stream stream;
|
|
private StreamReader reader;
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Create a new reader for the given stream.
|
|
/// </summary>
|
|
/// <param name="s">The stream to read the CSV from.</param>
|
|
public CSVReader(Stream s) : this(s, null) { }
|
|
|
|
/// <summary>
|
|
/// Create a new reader for the given stream and encoding.
|
|
/// </summary>
|
|
/// <param name="s">The stream to read the CSV from.</param>
|
|
/// <param name="enc">The encoding used.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new reader for the given text file path.
|
|
/// </summary>
|
|
/// <param name="filename">The name of the file to be read.</param>
|
|
public CSVReader(string filename) : this(filename, null) { }
|
|
|
|
/// <summary>
|
|
/// Creates a new reader for the given text file path and encoding.
|
|
/// </summary>
|
|
/// <param name="filename">The name of the file to be read.</param>
|
|
/// <param name="enc">The encoding used.</param>
|
|
public CSVReader(string filename, Encoding enc)
|
|
: this(new FileStream(filename, FileMode.Open), enc) { }
|
|
|
|
/// <summary>
|
|
/// Returns the fields for the next row of CSV data (or null if at eof)
|
|
/// </summary>
|
|
/// <returns>A string array of fields or null if at the end of file.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the CSVReader. The underlying stream is closed.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Exception class for CSVReader exceptions.
|
|
/// </summary>
|
|
public class CSVReaderException : ApplicationException {
|
|
|
|
/// <summary>
|
|
/// Constructs a new exception object with the given message.
|
|
/// </summary>
|
|
/// <param name="message">The exception message.</param>
|
|
public CSVReaderException(string message) : base(message) { }
|
|
}
|
|
}
|