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 /// <summary> 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 /// </summary> 14 public enum EmptyLineBehavior 15 { 16 /// <summary> 17 /// Empty lines are interpreted as a line with zero columns. 18 /// </summary> 19 NoColumns, 20 /// <summary> 21 /// Empty lines are interpreted as a line with a single empty column. 22 /// </summary> 23 EmptyColumn, 24 /// <summary> 25 /// Empty lines are skipped over as though they did not exist. 26 /// </summary> 27 Ignore, 28 /// <summary> 29 /// An empty line is interpreted as the end of the input file. 30 /// </summary> 31 EndOfFile, 32 } 33 34 /// <summary> 35 /// Common base class for CSV reader and writer classes. 36 /// </summary> 37 public abstract class CsvFileCommon 38 { 39 /// <summary> 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 /// </summary> 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 /// <summary> 50 /// Gets/sets the character used for column delimiters. 51 /// </summary> 52 public char Delimiter 53 { 54 get { return SpecialChars[DelimiterIndex]; } 55 set { SpecialChars[DelimiterIndex] = value; } 56 } 57 58 /// <summary> 59 /// Gets/sets the character used for column quotes. 60 /// </summary> 61 public char Quote 62 { 63 get { return SpecialChars[QuoteIndex]; } 64 set { SpecialChars[QuoteIndex] = value; } 65 } 66 } 67 68 /// <summary> 69 /// Class for reading from comma-separated-value (CSV) files 70 /// </summary> 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 /// <summary> 80 /// Initializes a new instance of the CsvFileReader class for the 81 /// specified stream. 82 /// </summary> 83 /// <param name="stream">The stream to read from</param> 84 /// <param name="emptyLineBehavior">Determines how empty lines are handled</param> 85 public CsvFileReader(Stream stream, 86 EmptyLineBehavior emptyLineBehavior = EmptyLineBehavior.NoColumns) 87 { 88 Reader = new StreamReader(stream); 89 EmptyLineBehavior = emptyLineBehavior; 90 } 91 92 /// <summary> 93 /// Initializes a new instance of the CsvFileReader class for the 94 /// specified file path. 95 /// </summary> 96 /// <param name="path">The name of the CSV file to read from</param> 97 /// <param name="emptyLineBehavior">Determines how empty lines are handled</param> 98 public CsvFileReader(string path, 99 EmptyLineBehavior emptyLineBehavior = EmptyLineBehavior.NoColumns) 100 { 101 Reader = new StreamReader(path); 102 EmptyLineBehavior = emptyLineBehavior; 103 } 104 105 /// <summary> 106 /// Reads a row of columns from the current CSV file. Returns false if no 107 /// more data could be read because the end of the file was reached. 108 /// </summary> 109 /// <param name="columns">Collection to hold the columns read</param> 110 public bool ReadRow(List<string> columns) 111 { 112 // Verify required argument 113 if (columns == null) 114 throw new ArgumentNullException("columns"); 115 116 ReadNextLine: 117 // Read next line from the file 118 CurrLine = Reader.ReadLine(); 119 CurrPos = 0; 120 // Test for end of file 121 if (CurrLine == null) 122 return false; 123 // Test for empty line 124 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 line 139 string column; 140 int numColumns = 0; 141 while (true) 142 { 143 // Read next column 144 if (CurrPos < CurrLine.Length && CurrLine[CurrPos] == Quote) 145 column = ReadQuotedColumn(); 146 else 147 column = ReadUnquotedColumn(); 148 // Add column to list 149 if (numColumns < columns.Count) 150 columns[numColumns] = column; 151 else 152 columns.Add(column); 153 numColumns++; 154 // Break if we reached the end of the line 155 if (CurrLine == null || CurrPos == CurrLine.Length) 156 break; 157 // Otherwise skip delimiter 158 Debug.Assert(CurrLine[CurrPos] == Delimiter); 159 CurrPos++; 160 } 161 // Remove any unused columns from collection 162 if (numColumns < columns.Count) 163 columns.RemoveRange(numColumns, columns.Count - numColumns); 164 // Indicate success 165 return true; 166 } 167 168 /// <summary> 169 /// Reads a quoted column by reading from the current line until a 170 /// 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 last 172 /// line in the file. Note: CurrLine may be set to null on return. 173 /// </summary> 174 private string ReadQuotedColumn() 175 { 176 // Skip opening quote character 177 Debug.Assert(CurrPos < CurrLine.Length && CurrLine[CurrPos] == Quote); 178 CurrPos++; 179 180 // Parse column 181 StringBuilder builder = new StringBuilder(); 182 while (true) 183 { 184 while (CurrPos == CurrLine.Length) 185 { 186 // End of line so attempt to read the next line 187 CurrLine = Reader.ReadLine(); 188 CurrPos = 0; 189 // Done if we reached the end of the file 190 if (CurrLine == null) 191 return builder.ToString(); 192 // Otherwise, treat as a multi-line field 193 builder.Append(Environment.NewLine); 194 } 195 196 // Test for quote character 197 if (CurrLine[CurrPos] == Quote) 198 { 199 // If two quotes, skip first and treat second as literal 200 int nextPos = (CurrPos + 1); 201 if (nextPos < CurrLine.Length && CurrLine[nextPos] == Quote) 202 CurrPos++; 203 else 204 break; // Single quote ends quoted sequence 205 } 206 // Add current character to the column 207 builder.Append(CurrLine[CurrPos++]); 208 } 209 210 if (CurrPos < CurrLine.Length) 211 { 212 // Consume closing quote 213 Debug.Assert(CurrLine[CurrPos] == Quote); 214 CurrPos++; 215 // Append any additional characters appearing before next delimiter 216 builder.Append(ReadUnquotedColumn()); 217 } 218 // Return column value 219 return builder.ToString(); 220 } 221 222 /// <summary> 223 /// Reads an unquoted column by reading from the current line until a 224 /// delimiter is found or the end of the line is reached. On return, the 225 /// current position points to the delimiter or the end of the current 226 /// line. 227 /// </summary> 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 StreamReader 240 public void Dispose() 241 { 242 Reader.Dispose(); 243 } 244 } 245 246 /// <summary> 247 /// Class for writing to comma-separated-value (CSV) files. 248 /// </summary> 249 public class CsvFileWriter : CsvFileCommon, IDisposable 250 { 251 // Private members 252 private StreamWriter Writer; 253 private string OneQuote = null; 254 private string TwoQuotes = null; 255 private string QuotedFormat = null; 256 257 /// <summary> 258 /// Initializes a new instance of the CsvFileWriter class for the 259 /// specified stream. 260 /// </summary> 261 /// <param name="stream">The stream to write to</param> 262 public CsvFileWriter(Stream stream) 263 { 264 Writer = new StreamWriter(stream); 265 } 266 267 /// <summary> 268 /// Initializes a new instance of the CsvFileWriter class for the 269 /// specified file path. 270 /// </summary> 271 /// <param name="path">The name of the CSV file to write to</param> 272 public CsvFileWriter(string path) 273 { 274 Writer = new StreamWriter(path); 275 } 276 277 /// <summary> 278 /// Writes a row of columns to the current CSV file. 279 /// </summary> 280 /// <param name="columns">The list of columns to write</param> 281 public void WriteRow(List<string> columns) 282 { 283 // Verify required argument 284 if (columns == null) 285 throw new ArgumentNullException("columns"); 286 287 // Ensure we‘re using current quote character 288 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 column 296 for (int i = 0; i < columns.Count; i++) 297 { 298 // Add delimiter if this isn‘t the first column 299 if (i > 0) 300 Writer.Write(Delimiter); 301 // Write this column 302 if (columns[i].IndexOfAny(SpecialChars) == -1) 303 Writer.Write(columns[i]); 304 else 305 Writer.Write(QuotedFormat, columns[i].Replace(OneQuote, TwoQuotes)); 306 } 307 Writer.WriteLine(); 308 } 309 310 // Propagate Dispose to StreamWriter 311 public void Dispose() 312 { 313 Writer.Dispose(); 314 } 315 } 316 }
时间: 2024-10-11 21:39:46