I recently had the need to import and export CSV files in an MFC application. A CSV (Comma-Delimited Values) file is a plain-text file where each row contains one or more fields, separated by commas.
CSV files are probably best known for their use by Microsoft Excel. CSV files provide a convenient format for sharing spreadsheet data between applications, particularly when you consider having your application work directly with native Excel files would be a very complex task.
The isn’t very complex. Mostly just some simple parsing. One trick is when a data field contains a comma. Since commas are used to delimit fields, this would cause problems and so such fields are enclosed in double quotes. And since double quotes have special meaning, we have problems if a data field contains a double quote and so such fields are also enclosed in double quotes, and pairs of double quotes are interpreted to mean a single double quote in the data.
My header file is shown in Listing 1 and my source file is shown in listing 2.
#pragma once
#include "afx.h"
class CCSVFile : public CStdioFile
{
public:
enum Mode { modeRead, modeWrite };
CCSVFile(LPCTSTR lpszFilename, Mode mode = modeRead);
~CCSVFile(void);
bool ReadData(CStringArray &arr);
void WriteData(CStringArray &arr);
#ifdef _DEBUG
Mode m_nMode;
#endif
};
Listing 1: CSVFile.h
The class is very simple: it only contains two methods (besides the constructor and destructor).
Note it is up to the caller to ensure that only the ReadData()
method is called when the constructor specified read mode, and only the WriteData()
method is called when the constructor specified write mode. To help enforce this, the code asserts if it is not the case when _DEBUG
is defined.
#include "StdAfx.h"
#include "CSVFile.h"
CCSVFile::CCSVFile(LPCTSTR lpszFilename, Mode mode)
: CStdioFile(lpszFilename, (mode == modeRead) ?
CFile::modeRead|CFile::shareDenyWrite|CFile::typeText
:
CFile::modeWrite|CFile::shareDenyWrite|CFile::modeCreate|CFile::typeText)
{
#ifdef _DEBUG
m_nMode = mode;
#endif
}
CCSVFile::~CCSVFile(void)
{
}
bool CCSVFile::ReadData(CStringArray &arr)
{
ASSERT(m_nMode == modeRead);
CString sLine;
if (!ReadString(sLine))
return false;
LPCTSTR p = sLine;
int nValue = 0;
while (*p != '\0')
{
CString s;
if (*p == '"')
{
p++;
while (*p != '\0')
{
if (*p == '"')
{
p++;
if (*p != '"')
{
p++;
break;
}
}
s.AppendChar(*p++);
}
}
else
{
while (*p != '\0' && *p != ',')
{
s.AppendChar(*p++);
}
if (*p != '\0')
p++;
}
if (nValue < arr.GetCount())
arr[nValue] = s;
else
arr.Add(s);
nValue++;
}
if (arr.GetCount() > nValue)
arr.RemoveAt(nValue, arr.GetCount() - nValue);
return true;
}
void CCSVFile::WriteData(CStringArray &arr)
{
static TCHAR chQuote = '"';
static TCHAR chComma = ',';
ASSERT(m_nMode == modeWrite);
for (int i = 0; i < arr.GetCount(); i++)
{
if (i > 0)
WriteString(_T(","));
bool bComma = (arr[i].Find(chComma) != -1);
bool bQuote = (arr[i].Find(chQuote) != -1);
if (bComma || bQuote)
{
Write(&chQuote, sizeof(TCHAR));
if (bQuote)
{
for (int j = 0; j < arr[i].GetLength(); i++)
{
if (arr[i][j] == chQuote)
Write(&chQuote, sizeof(TCHAR));
TCHAR ch = arr[i][j];
Write(&ch, sizeof(TCHAR));
}
}
else
{
WriteString(arr[i]);
}
Write(&chQuote, sizeof(TCHAR));
}
else
{
WriteString(arr[i]);
}
}
WriteString(_T("\n"));
}
Listing 2: CSVFile.cpp
There are many ways to go about parsing text. I was more comfortable manually stepping through the text, character-by-character and so that’s what the code does.
As implied by the names, ReadData()
is used to read to a CSV file while WriteData()
is used to write to one. Each line of data is stored in a CStringArray[]
, which is passed by reference to both functions. The caller must call these methods once for each line. When calling ReadData()
, false
is returned when the end of the file is reached.
As mentioned, the code is fairly simple, but I’ve actually found I’ve used this code on several occasions and even ported it to C#. Perhaps you will find it useful as well.