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 }