Create a directory
Lab14/with the following structure:
Lab14/
├── csv-utils/
│ ├── include/
│ │ └── csv-utils.h
│ ├── source/
│ │ └── csv-utils.cpp
│ ├── compile_flags.txt
│ └── makefile
├── LICENSE
└── README.mdFill the following files.
#ifndef CSV_UTILS_H
#define CSV_UTILS_H
#include <map>
#include <string>
#include <vector>
#include <fstream>
using CsvRow = std::map<std::string, std::string>;
using CsvTable = std::vector<CsvRow>;
class CsvUtils {
public:
static CsvTable readFile(const std::string& filePath);
static void writeFile(const std::string& filePath, const CsvTable& table);
static std::vector<std::string> getColumnNames(const CsvTable& table);
static std::string getValue(const CsvTable& table, int rowIndex, const std::string& columnName);
private:
static constexpr char SEPARATOR = ';';
static constexpr char QUOTE = '"';
static std::vector<std::string> splitLine(const std::string& line);
static void checkColumnCount(const std::vector<std::string>& headers, const std::vector<std::string>& values);
static void checkColumnNames(const std::vector<std::string>& expectedColumns, const std::vector<std::string>& actualColumns);
static CsvRow buildRow(const std::vector<std::string>& headers, const std::vector<std::string>& values);
static void writeHeaders(std::ofstream& file, const std::vector<std::string>& headers);
static void writeValues(std::ofstream& file, const CsvRow& row, const std::vector<std::string>& headers);
static void checkQuotes(const std::string& value);
};
#endif#include "csv-utils.h"
#include <string>
#include <vector>
#include <fstream>
#include <stdexcept>
#include <algorithm>
CsvTable CsvUtils::readFile(const std::string& filePath) {
std::ifstream file(filePath);
if (!file.is_open()) {
throw std::runtime_error("Cannot open the CSV file.");
}
std::string line;
if (!std::getline(file, line)) {
throw std::invalid_argument("The CSV file is empty.");
}
std::vector<std::string> headers = splitLine(line);
CsvTable table;
while (std::getline(file, line)) {
std::vector<std::string> values = splitLine(line);
table.push_back(buildRow(headers, values));
}
return table;
}
void CsvUtils::writeFile(const std::string& filePath, const CsvTable& table) {
if (table.empty()) {
throw std::invalid_argument("Cannot write an empty table.");
}
std::ofstream file(filePath);
if (!file.is_open()) {
throw std::runtime_error("Cannot write the CSV file.");
}
std::vector<std::string> headers = getColumnNames(table);
writeHeaders(file, headers);
for (const CsvRow& row : table) {
checkColumnNames(headers, getColumnNames({row}));
writeValues(file, row, headers);
}
}
std::vector<std::string> CsvUtils::getColumnNames(const CsvTable& table) {
if (table.empty()) {
throw std::invalid_argument("Cannot get column names from an empty table.");
}
std::vector<std::string> columnNames;
for (const std::pair<const std::string, std::string>& column : table[0]) {
columnNames.push_back(column.first);
}
return columnNames;
}
std::string CsvUtils::getValue(const CsvTable& table, int rowIndex, const std::string& columnName) {
if (rowIndex < 0 || rowIndex >= table.size()) {
throw std::out_of_range("Row index out of range.");
}
const CsvRow& row = table[rowIndex];
if (row.find(columnName) == row.end()) {
throw std::invalid_argument("Column name not found.");
}
return row.at(columnName);
}
std::vector<std::string> CsvUtils::splitLine(const std::string& line) {
std::vector<std::string> values;
std::string currentValue;
for (char character : line) {
if (character != SEPARATOR) {
currentValue += character;
} else {
values.push_back(currentValue);
currentValue.clear();
}
}
values.push_back(currentValue);
for (const std::string& value : values) {
checkQuotes(value);
}
return values;
}
CsvRow CsvUtils::buildRow(const std::vector<std::string>& headers, const std::vector<std::string>& values) {
checkColumnCount(headers, values);
CsvRow row;
for (int index = 0; index < headers.size(); index++) {
row[headers[index]] = values[index];
}
return row;
}
void CsvUtils::checkColumnCount(const std::vector<std::string>& headers, const std::vector<std::string>& values) {
if (headers.size() != values.size()) {
throw std::invalid_argument("CSV row does not contain the correct number of columns.");
}
}
void CsvUtils::checkColumnNames(const std::vector<std::string>& expectedColumns, const std::vector<std::string>& actualColumns) {
checkColumnCount(expectedColumns, actualColumns);
for (const std::string& expectedColumn : expectedColumns) {
if (std::find(actualColumns.begin(), actualColumns.end(),expectedColumn) == actualColumns.end()) {
throw std::invalid_argument("CSV table does not contain the expected columns.");
}
}
}
void CsvUtils::checkQuotes(const std::string& value) {
if (value.find(QUOTE) != std::string::npos) {
throw std::invalid_argument("Quotes are not supported.");
}
}
void CsvUtils::writeHeaders(std::ofstream& file, const std::vector<std::string>& headers) {
file << headers[0];
for (int index = 1; index < headers.size(); index++) {
file << SEPARATOR << headers[index];
}
file << '\n';
}
void CsvUtils::writeValues(std::ofstream& file, const CsvRow& row, const std::vector<std::string>& headers) {
file << row.at(headers[0]);
for (int index = 1; index < headers.size(); index++) {
file << SEPARATOR << row.at(headers[index]);
}
file << '\n';
}-xc++
-std=c++17
-Iincludeall:
mkdir -p lib
g++ -std=c++17 -Iinclude -c source/csv-utils.cpp -o lib/csv-utils.o
ar rcs lib/libcsvutils.a lib/csv-utils.o
rm -f lib/csv-utils.oWe are going to learn how to document a small project called CsvUtils, a C++ library that allows users to read, write, and query simple CSV files.
makefile
When we execute make, the following instructions are performed:
mkdir -p libcreates thelibdirectory (insidecsv-utils/) if it does not already exist.g++ -std=c++17 -Iinclude -c source/csv-utils.cpp -o lib/csv-utils.ocompiles the filesource/csv-utils.cppinto the object filelib/csv-utils.o.ar rcs lib/libcsvutils.a lib/csv-utils.ocreates the static librarylib/libcsvutils.acontaining the object filelib/csv-utils.o. This file can later be used by a linker when compiling a program that usescsv-utils.rm -f lib/csv-utils.oremoves the object filelib/csv-utils.o.
The names lib/ and lib<...>.a follow Unix/Linux naming conventions.
Reading the code¶
First, let us try to understand the code before adding comments and documentation.
Look at the contents of
csv-utils.h.
CsvTable in csv-utils.h
CsvTable in csv-utils.hWe have defined the following types in our library:
using CsvRow = std::map<std::string, std::string>;
using CsvTable = std::vector<CsvRow>;using is a C++ directive that allows us to define an alias (CsvRow and CsvTable) for a type (std::map<std::string, std::string> and std::vector<std::map<std::string, std::string>>, respectively).
A CSV row (CsvRow) is represented by a dictionary (std::map).
CsvRow row = {
{"name", "Alice"},
{"grade", "20"}
};In this structure:
the key (
std::string) corresponds to a column name; andthe value (
std::string) corresponds to the contents of the associated cell.
For example:
row["name"] // returns "Alice"A CSV table (CsvTable) is represented by a vector of CSV rows.
CsvTable table = {
{
{"name", "Alice"},
{"grade", "20"}
},
{
{"name", "Bob"},
{"grade", "19"}
}
};For example:
table[1]["grade"] // returns "19"The corresponding CSV file is:
name;grade
Alice;20
Bob;19SEPARATOR = ';'
SEPARATOR = ';'CSV files can also use , as a separator, but in our simplified CSV processing library, we only use ; as the separator.
Look at the implementation of
checkQuotes.
Quotes in CSV files
The CSV format supports quoted values, but our simplified CSV processing library does not.
Look at the implementation of
readFile. Do you understand why the first line is handled separately?
readFile
readFileThis line at the beginning of the function (after the exception checks) indicates that the first line of the CSV file is used as the header row.
std::vector<std::string> headers = splitLine(line);Look at the implementation of
splitLine. Do you understand why we callvalues.push_back(currentValue)after thefor (char character : line)loop?
splitLine
splitLineThis line after the for (char character : line) loop is necessary because the last value in a CSV row is not followed by a separator.
values.push_back(currentValue);Look at the implementation of
buildRowand thencheckColumnCount.
Number of columns
We assume that every row in the CSV file must contain the same number of columns as the header row.
Look at the implementation of
writeFile, thengetColumnNamesandcheckColumnNames.
Column names
We use the first row of the CsvTable as the reference for the column names.
std::vector<std::string> columnNames;
for (const std::pair<const std::string, std::string>& column : table[0]) {
columnNames.push_back(column.first);
}We then assume that every row in the CsvTable uses the same column names as the first row by checking them with checkColumnNames.
Finally, look at the implementations of
writeHeaders,writeValues, andgetValue.
Commenting¶
After understanding the code, add comments to clarify or emphasize the importance of different parts of the implementation.
Hints
I believe there are only four comments worth adding, one in each of the following functions: readFile, writeFile, getColumnNames, and splitLine.
The remaining specifications will appear in the documentation.
Documenting¶
Document the
CsvUtilsclass as shown in class.
Hints
Where should the documentation be written, in
csv-utils.horcsv-utils.cpp?Should you document the
publicmembers, theprivatemembers, or both?Have you identified all the possible
throwsfor each method?
To help you, here is the number of possible throws for each method:
readFile: 4writeFile: 4getColumnNames: 1getValue: 2splitLine: 1checkColumnCount: 1checkColumnNames: 2buildRow: 1writeHeaders: 0writeValues: 0checkQuotes: 1
README¶
Some Markdown syntax
#creates headings. The more#characters you use, the smaller the heading.[text](link)creates a hyperlink.To create a
cppcode block (you may also usebash,txt, etc.):
```cpp
int number = 42;
```**bold text**makes text bold.*italic text*makes text italic.-or*creates bullet lists.1.,2., etc. create numbered lists.> quotefollowed by> - authorcreates a quotation with an author.inserts an image and displays the alternative text when the image cannot be loaded or when it is hovered.- [ ]creates an unchecked checkbox and- [x]a checked checkbox.---inserts a horizontal line.
Fill in your
README.mdstarting with the title and a general description of the project.Add a
Main Featuressection with a short description of the four main features.Add a
Documentationsection with a link or path to the documentation from the root of the project, which will be located at/csv-utils/docs/html/index.htmlafter generating the documentation with Doxygen in the next section.Add an
Installationsection with the following instructions.
Installation
Compile the library by running the following command inside `csv-utils/`:
```bash
make
```
The static library is generated in the following directory:
```text
csv-utils/lib/libcsvutils.a
```
Copy `csv-utils/` together with its `include/` and `lib/` directories into your project. An example is provided in `my-app/`.
To use the library, compile your program with the following options:
```bash
g++ -std=c++17 main.cpp -Ipath/to/csv-utils/include -Lpath/to/csv-utils/lib -lcsvutils -o application
```Compilation options
We have already seen the -I option, which specifies the location of header files.
The new options introduced here are:
-L, which tells the linker (the final stage of compilation) where to find our static library.-lcsvutils, which tells the linker to link against thelibcsvutils.alibrary (l<library>is transformed intolib<library>.a).
Create the directory
Lab14/my-app/with the following structure.
Lab14/my-app/
└── external/csv-utils/
├── include/
│ └── csv-utils.h
├── lib/
│ └── libcsvutils.a
├── main.cpp
├── compile_flags.txt
└── makefileCopy the following contents into the corresponding files.
#include "csv-utils.h"
#include <iostream>
int main() {
CsvTable table = {
{{"name", "Alice"}, {"grade", "20"}},
{{"name", "Bob"}, {"grade", "19"}}
};
CsvUtils::writeFile("students.csv", table);
CsvTable loadedTable = CsvUtils::readFile("students.csv");
std::cout << CsvUtils::getValue(loadedTable, 0, "name") << '\n';
std::cout << CsvUtils::getValue(loadedTable, 0, "grade") << '\n';
return 0;
}-xc++
-std=c++17
-Iexternal/csv-utils/includeall:
g++ -std=c++17 main.cpp -Iexternal/csv-utils/include -Lexternal/csv-utils/lib -lcsvutils -o my-app
run:
./my-app
clean:
rm my-appTest the code in
my-app/yourself usingmakeandmake run.Add a
Usagesection where you provide examples demonstrating the main features of the library.Add a
Limitationssubsection toUsagewhere you explain the limitations of the CSV files that can be read or written by the library.Add a
Licensesection stating that this mini-project is distributed under the MIT License, then copy the MIT License provided in the course material (after modifying the header) into theLICENSEfile.
Doxyfile¶
To generate the documentation with Doxygen, we first need to generate a Doxyfile configuration file.
Generate the
Doxyfileby running the following command insidecsv-utils/:
doxygen -gModify the following settings in the
Doxyfile:
PROJECT_NAME = "CsvUtils"INPUT = include ../README.mdOUTPUT_DIRECTORY = docsRECURSIVE = YES(to include subdirectories ofinclude/; this is not necessary here but is useful in the general case)GENERATE_HTML = YESGENERATE_LATEX = NOUSE_MDFILE_AS_MAINPAGE = ../README.md
Generate the documentation by running the following command inside
csv-utils/:
doxygen DoxyfileYou should now see a docs/ directory inside csv-utils/ containing the generated documentation.
View the generated documentation by opening
csv-utils/docs/html/index.htmlin a web browser.
Document classes and their public methods in header files (syntax).
Remember the main elements that should be included in a
README(README guidelines).Write clean code and only comment the parts that genuinely need clarification (principles of good comments).