1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.IO; 5 using System.Text; 6 7 namespace CsvFile 8 { 9 ///10 /// Determines how empty lines are interpreted when reading CSV files. 11 /// These values do not affect empty lines that occur within quoted fields 12 /// or empty lines that appear at the end of the input file. 13 /// 14 public enum EmptyLineBehavior 15 { 16 ///17 /// Empty lines are interpreted as a line with zero columns. 18 /// 19 NoColumns, 20 ///21 /// Empty lines are interpreted as a line with a single empty column. 22 /// 23 EmptyColumn, 24 ///25 /// Empty lines are skipped over as though they did not exist. 26 /// 27 Ignore, 28 ///29 /// An empty line is interpreted as the end of the input file. 30 /// 31 EndOfFile, 32 } 33 34 ///35 /// Common base class for CSV reader and writer classes. 36 /// 37 public abstract class CsvFileCommon 38 { 39 ///40 /// These are special characters in CSV files. If a column contains any 41 /// of these characters, the entire column is wrapped in double quotes. 42 /// 43 protected char[] SpecialChars = new char[] { ',', '"', '\r', '\n' }; 44 45 // Indexes into SpecialChars for characters with specific meaning 46 private const int DelimiterIndex = 0; 47 private const int QuoteIndex = 1; 48 49 ///50 /// Gets/sets the character used for column delimiters. 51 /// 52 public char Delimiter 53 { 54 get { return SpecialChars[DelimiterIndex]; } 55 set { SpecialChars[DelimiterIndex] = value; } 56 } 57 58 ///59 /// Gets/sets the character used for column quotes. 60 /// 61 public char Quote 62 { 63 get { return SpecialChars[QuoteIndex]; } 64 set { SpecialChars[QuoteIndex] = value; } 65 } 66 } 67 68 ///69 /// Class for reading from comma-separated-value (CSV) files 70 /// 71 public class CsvFileReader : CsvFileCommon, IDisposable 72 { 73 // Private members 74 private StreamReader Reader; 75 private string CurrLine; 76 private int CurrPos; 77 private EmptyLineBehavior EmptyLineBehavior; 78 79 ///80 /// Initializes a new instance of the CsvFileReader class for the 81 /// specified stream. 82 /// 83 /// The stream to read from 84 /// Determines how empty lines are handled 85 public CsvFileReader(Stream stream, 86 EmptyLineBehavior emptyLineBehavior = EmptyLineBehavior.NoColumns) 87 { 88 Reader = new StreamReader(stream); 89 EmptyLineBehavior = emptyLineBehavior; 90 } 91 92 ///93 /// Initializes a new instance of the CsvFileReader class for the 94 /// specified file path. 95 /// 96 /// The name of the CSV file to read from 97 /// Determines how empty lines are handled 98 public CsvFileReader(string path, 99 EmptyLineBehavior emptyLineBehavior = EmptyLineBehavior.NoColumns)100 {101 Reader = new StreamReader(path);102 EmptyLineBehavior = emptyLineBehavior;103 }104 105 ///106 /// Reads a row of columns from the current CSV file. Returns false if no107 /// more data could be read because the end of the file was reached.108 /// 109 /// Collection to hold the columns read110 public bool ReadRow(Listcolumns)111 {112 // Verify required argument113 if (columns == null)114 throw new ArgumentNullException("columns");115 116 ReadNextLine:117 // Read next line from the file118 CurrLine = Reader.ReadLine();119 CurrPos = 0;120 // Test for end of file121 if (CurrLine == null)122 return false;123 // Test for empty line124 if (CurrLine.Length == 0)125 {126 switch (EmptyLineBehavior)127 {128 case EmptyLineBehavior.NoColumns:129 columns.Clear();130 return true;131 case EmptyLineBehavior.Ignore:132 goto ReadNextLine;133 case EmptyLineBehavior.EndOfFile:134 return false;135 }136 }137 138 // Parse line139 string column;140 int numColumns = 0;141 while (true)142 {143 // Read next column144 if (CurrPos < CurrLine.Length && CurrLine[CurrPos] == Quote)145 column = ReadQuotedColumn();146 else147 column = ReadUnquotedColumn();148 // Add column to list149 if (numColumns < columns.Count)150 columns[numColumns] = column;151 else152 columns.Add(column);153 numColumns++;154 // Break if we reached the end of the line155 if (CurrLine == null || CurrPos == CurrLine.Length)156 break;157 // Otherwise skip delimiter158 Debug.Assert(CurrLine[CurrPos] == Delimiter);159 CurrPos++;160 }161 // Remove any unused columns from collection162 if (numColumns < columns.Count)163 columns.RemoveRange(numColumns, columns.Count - numColumns);164 // Indicate success165 return true;166 }167 168 /// 169 /// Reads a quoted column by reading from the current line until a170 /// closing quote is found or the end of the file is reached. On return,171 /// the current position points to the delimiter or the end of the last172 /// line in the file. Note: CurrLine may be set to null on return.173 /// 174 private string ReadQuotedColumn()175 {176 // Skip opening quote character177 Debug.Assert(CurrPos < CurrLine.Length && CurrLine[CurrPos] == Quote);178 CurrPos++;179 180 // Parse column181 StringBuilder builder = new StringBuilder();182 while (true)183 {184 while (CurrPos == CurrLine.Length)185 {186 // End of line so attempt to read the next line187 CurrLine = Reader.ReadLine();188 CurrPos = 0;189 // Done if we reached the end of the file190 if (CurrLine == null)191 return builder.ToString();192 // Otherwise, treat as a multi-line field193 builder.Append(Environment.NewLine);194 }195 196 // Test for quote character197 if (CurrLine[CurrPos] == Quote)198 {199 // If two quotes, skip first and treat second as literal200 int nextPos = (CurrPos + 1);201 if (nextPos < CurrLine.Length && CurrLine[nextPos] == Quote)202 CurrPos++;203 else204 break; // Single quote ends quoted sequence205 }206 // Add current character to the column207 builder.Append(CurrLine[CurrPos++]);208 }209 210 if (CurrPos < CurrLine.Length)211 {212 // Consume closing quote213 Debug.Assert(CurrLine[CurrPos] == Quote);214 CurrPos++;215 // Append any additional characters appearing before next delimiter216 builder.Append(ReadUnquotedColumn());217 }218 // Return column value219 return builder.ToString();220 }221 222 ///223 /// Reads an unquoted column by reading from the current line until a224 /// delimiter is found or the end of the line is reached. On return, the225 /// current position points to the delimiter or the end of the current226 /// line.227 /// 228 private string ReadUnquotedColumn()229 {230 int startPos = CurrPos;231 CurrPos = CurrLine.IndexOf(Delimiter, CurrPos);232 if (CurrPos == -1)233 CurrPos = CurrLine.Length;234 if (CurrPos > startPos)235 return CurrLine.Substring(startPos, CurrPos - startPos);236 return String.Empty;237 }238 239 // Propagate Dispose to StreamReader240 public void Dispose()241 {242 Reader.Dispose();243 }244 }245 246 ///247 /// Class for writing to comma-separated-value (CSV) files.248 /// 249 public class CsvFileWriter : CsvFileCommon, IDisposable250 {251 // Private members252 private StreamWriter Writer;253 private string OneQuote = null;254 private string TwoQuotes = null;255 private string QuotedFormat = null;256 257 ///258 /// Initializes a new instance of the CsvFileWriter class for the259 /// specified stream.260 /// 261 /// The stream to write to262 public CsvFileWriter(Stream stream)263 {264 Writer = new StreamWriter(stream);265 }266 267 ///268 /// Initializes a new instance of the CsvFileWriter class for the269 /// specified file path.270 /// 271 /// The name of the CSV file to write to272 public CsvFileWriter(string path)273 {274 Writer = new StreamWriter(path);275 }276 277 ///278 /// Writes a row of columns to the current CSV file.279 /// 280 /// The list of columns to write281 public void WriteRow(Listcolumns)282 {283 // Verify required argument284 if (columns == null)285 throw new ArgumentNullException("columns");286 287 // Ensure we're using current quote character288 if (OneQuote == null || OneQuote[0] != Quote)289 {290 OneQuote = String.Format("{0}", Quote);291 TwoQuotes = String.Format("{0}{0}", Quote);292 QuotedFormat = String.Format("{0}{ {0}}{0}", Quote);293 }294 295 // Write each column296 for (int i = 0; i < columns.Count; i++)297 {298 // Add delimiter if this isn't the first column299 if (i > 0)300 Writer.Write(Delimiter);301 // Write this column302 if (columns[i].IndexOfAny(SpecialChars) == -1)303 Writer.Write(columns[i]);304 else305 Writer.Write(QuotedFormat, columns[i].Replace(OneQuote, TwoQuotes));306 }307 Writer.WriteLine();308 }309 310 // Propagate Dispose to StreamWriter311 public void Dispose()312 {313 Writer.Dispose();314 }315 }316 }