BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Input / Output
.NET 2.0+

Creating CSV Files

Comma-separated values (CSV) files can be used to transfer information between systems that support no other common interface. CSV files can be read using standard functionality from the .NET framework but creating them requires a custom class.

Configuration Properties

We can now add the configuration properties to the CsvWriter class. Let's start with the characters for the delimiter and quote. Both of these properties return and set the matching field 's value. They also call the ResetQuotableCharacters method in order that the list of special characters remains correct. The Quote property also initialises the _singleQuote and _doubleQuote fields that are used when constructing the CSV file data.

public char Delimiter
{
    get
    {
        return _delimiter;
    }
    set
    {
        _delimiter = value;
        ResetQuotableCharacters();
    }
}

public char Quote
{
    get
    {
        return _quote;
    }
    set
    {
        _quote = value;
        _singleQuote = value.ToString();
        _doubleQuote = _singleQuote + _singleQuote;
        ResetQuotableCharacters();
    }
}

We also need a property to allow the comment token to be updated or read. This property provides simple interaction with the underlying field and prevents the comment token being null or an empty field. We could add extra checks to this property, and the Quote and Delimiter properties, to ensure that their characters do not clash and incorrectly generate lines that could be either comments or data. However, I've omitted these in this case, leaving it up to the consumer of the class to ensure that they do not configure it incorrectly.

public string CommentToken
{
    get
    {
        return _commentToken;
    }
    set
    {
        if (value == null) throw new ArgumentNullException("value");
        if (value.Length == 0) throw new ArgumentException("Value must not be empty");

        _commentToken = value;
    }
}

Constructors

The StreamWriter class includes three constructors that allow you to open a new file for writing. One of these allows you to set the path to the file, specify if existing files should be overwritten or new data should be appended, and to set the character encoding for the file. We can take advantage of this constructor by creating our own version with a matching signature and calling the base member. In our constructor we also want to set the default values for the Delimiter, Quote and CommentToken properties.

Add the constructor as follows:

public CsvWriter(string path, bool append, Encoding encoding)
    : base(path, append, encoding)
{
    Delimiter = ',';
    Quote = '"';
    CommentToken = "## ";
}

When instantiating a new StreamWriter, you can omit the second and third parameters. If not supplied, the class uses default settings for the write mode and the encoding. Existing files will be overwritten and the encoding will be UTF-8. We should create similar constructors to give a consistent experience with the standard class:

public CsvWriter(string file) : this(file, false, Encoding.UTF8) { }

public CsvWriter(string file, bool append) : this(file, append, Encoding.UTF8) { }

NB: StreamWriter also includes constructors for working with streams instead of files. You could easily add matching constructors to allow CsvWriter to do the same.

Writing CSV Data

We can now begin to add the methods that write data and comments to the target CSV file. We'll start with the more complex operation, which is storing CSV data. The StreamWriter class includes several overloaded versions of the WriteLine method, which adds a line to the open file. We'll add a new overload that accepts a string array argument. Each item in the array will become one comma-separated field in a new row in the target file.

The public method definition is shown below. In it we loop through the items in the array and call the WriteField method for the current item. The only complication is that we will want the WriteField method to know if the item is the first in the array. This extra information will determine whether a delimiter is needed or not. On completion of the loop we call WriteLine with no arguments. This overloaded version is provided by the base class. It simply adds a line break.

public void WriteLine(string[] fields)
{
    bool first = true;
    foreach (string field in fields)
    {
        WriteField(first, field);
        first = false;
    }
    WriteLine();
}

The WriteField method checks if the field being stored is the first of the row. If it is not, a delimiter is written to the file. Next, the field's data is adjusted according to its use of special characters using the WritePreparedField method, before being added to the file.

private void WriteField(bool first, string field)
{
    if (!first)
    {
        Write(_delimiter);
    }
    WritePreparedField(field);
}

WritePreparedField performs two actions. Firstly it adjusts the field data to deal with special characters and quotes by calling PrepareField. Next it uses the Write method, provided by the StreamWriter base class, to add the prepared text to the file.

private void WritePreparedField(string field)
{
    string prepared = PrepareField(field);
    Write(prepared);
}

PrepareField performs two operations upon the value that it is passed in order that it can safely be added to the CSV file. Firstly, any quote characters present are duplicated so that they are correctly escaped. Next, if any of the characters in the passed field exist within the list of quotable characters, the entire string is wrapped with quote characters.

private string PrepareField(string field)
{
    string quotesDoubled = DoubleQuotes(field);
    string prepared = AddQuotesIfRequired(quotesDoubled);
    return prepared;
}

private string DoubleQuotes(string field)
{
    return field.Replace(_singleQuote, _doubleQuote);
}

private string AddQuotesIfRequired(string field)
{
    foreach (char quotable in _quotableCharacters)
    {
        if (field.StartsWith(_commentToken) || field.Contains(quotable))
        {
            return string.Format("{0}{1}{0}", _quote, field);
        }
    }
    return field;
}
9 December 2012