Créez un répertoire
TP14/avec la structure suivante :
TP14/
├── csv-utils/
│ ├── include/
│ │ └── csv-utils.h
│ ├── source/
│ │ └── csv-utils.cpp
│ ├── compile_flags.txt
│ └── makefile
├── LICENSE
└── README.mdRemplissez les fichiers suivants et laissez les autres fichiers vides pour l’instant.
#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("Impossible d'ouvrir le fichier CSV.");
}
std::string line;
if (!std::getline(file, line)) {
throw std::invalid_argument("Le fichier CSV est vide.");
}
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("Impossible d'écrire une table CSV vide.");
}
std::ofstream file(filePath);
if (!file.is_open()) {
throw std::runtime_error("Impossible d'écrire dans le fichier CSV.");
}
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("Impossible de récupérer les noms de colonnes d'une table vide.");
}
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("L'indice de ligne est hors limites.");
}
const CsvRow& row = table[rowIndex];
if (row.find(columnName) == row.end()) {
throw std::invalid_argument("La colonne n'existe pas.");
}
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("La ligne CSV ne contient pas le bon nombre de colonnes.");
}
}
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("La table CSV ne contient pas les bonnes colonnes.");
}
}
}
void CsvUtils::checkQuotes(const std::string& value) {
if (value.find(QUOTE) != std::string::npos) {
throw std::invalid_argument("Les guillemets ne sont pas supportés par ce format CSV.");
}
}
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.oNous allons apprendre à documenter un mini-projet CsvUtils qui est une bibliothèque C++ permettant de lire, écrire et interroger des fichiers CSV simples.
makefile
Quand nous exécutons make, les instructions suivantes sont réalisées :
mkdir -p libcrée le répertoirelib(danscsv-utils/) s’il n’existe pas.g++ -std=c++17 -Iinclude -c source/csv-utils.cpp -o lib/csv-utils.ocompile le fichiersource/csv-utils.cppen objetlib/csv-utils.o.ar rcs lib/libcsvutils.a lib/csv-utils.ocrée une bibliothèque statiquelib/libcsvutils.acontenant l’objetlib/csv-utils.o. Il s’agit d’un fichier qui pourra être utilisé par un linker pour compiler un programme qui utilisecsv-utils.rm -f lib/csv-utils.osupprime l’objetlib/csv-utils.o.
Les noms lib/ et lib<...>.a sont des conventions de nommage Unix/Linux.
Lecture du code¶
D’abord, essayons de comprendre le code avant de rajouter des commentaires et la documentation.
Regardez le contenu de
csv-utils.h.
CsvTable dans csv-utils.h
CsvTable dans csv-utils.hNous avons défini les types suivants dans notre bibliothèque :
using CsvRow = std::map<std::string, std::string>;
using CsvTable = std::vector<CsvRow>;using est une directive C++ qui permet de donner un alias (CsvRow et CsvTable) au type (std::map<std::string, std::string> et std::vector<std::map<std::string, std::string>> respectivement).
Une ligne CSV (CsvRow) est représentée par un dictionnaire (std::map).
CsvRow row = {
{"name", "Alice"},
{"grade", "20"}
};Dans cette structure :
la clé (
std::string) correspond au nom d’une colonne etla valeur (
std::string) correspond au contenu de la cellule.
Par exemple :
row["name"] // retourne "Alice"Une table CSV (CsvTable) est représentée par un vecteur de lignes CSV.
CsvTable table = {
{
{"name", "Alice"},
{"grade", "20"}
},
{
{"name", "Bob"},
{"grade", "19"}
}
};Par exemple :
table[1]["grade"] // retourne "19"Le fichier CSV correspondant :
name;grade
Alice;20
Bob;19SEPARATOR = ';'
SEPARATOR = ';'Les fichiers CSV peuvent aussi utiliser , comme séparateur mais dans notre version simplifiée de traitement de fichiers CSV, nous utilisons seulement ; comme séparateur.
Regardez le code de
checkQuotes.
Les guillemets en CSV
Le format CSV supporte les guillemets mais notre bibliothèque de traitement simplifié des fichiers CSV ne les supporte pas.
Regardez le code de
readFile. Comprenez-vous pourquoi la première ligne est traitée séparément ?
readFile
readFileCette ligne dès le début de la fonction (après les exceptions) indique que la première ligne du fichier CSV est utilisée comme une ligne d’en-tête.
std::vector<std::string> headers = splitLine(line);Regardez le code de
splitLine. Comprenez-vous pourquoi nous faisonsvalues.push_back(currentValue)après la bouclefor (char character : line)?
splitLine
splitLineCette ligne après la boucle for (char character : line) est nécessaire car la dernière valeur d’une ligne CSV n’est pas suivie d’un séparateur.
values.push_back(currentValue);Regardez le code de
buildRowpuischeckColumnCount.
Nombre de colonnes
Nous considérons que toutes les lignes du fichier CSV doivent avoir le même nombre de colonnes que l’en-tête.
Regardez le code de
writeFilepuisgetColumnNamesetcheckColumnNames.
Noms des colonnes
Nous utilisons la première ligne de CsvTable comme référence pour les noms des colonnes.
std::vector<std::string> columnNames;
for (const std::pair<const std::string, std::string>& column : table[0]) {
columnNames.push_back(column.first);
}Puis, nous considérons que toutes les lignes de CsvTable utilisent les mêmes noms de colonnes que cette première ligne en vérifiant avec checkColumnNames.
Finalement, regardez le code de
writeHeaders,writeValuesetgetValue.
Commenter¶
Après avoir compris le code, ajoutez des commentaires pour clarifier ou souligner l’importance des différentes parties du code.
Indices
Je considère qu’il n’y a que 4 commentaires à inclure, un commentaire dans chacune des fonctions suivantes : readFile, writeFile, getColumnNames et splitLine.
Les autres spécifications apparaîtront dans la documentation.
Documenter¶
Documentez la classe
CsvUtilscomme vu en cours.
Indices
Où faut-il écrire la documentation, dans
csv-utils.hou danscsv-utils.cpp?Que faut-il documenter,
public,privateou les deux ?Avez-vous vu toutes les
throwspossible de chaque méthode ?
Pour vous aider, voici le nombre de throws possibles par méthode :
- readFile : 4
- writeFile : 4
- getColumnNames : 1
- getValue : 2
- splitLine : 1
- checkColumnCount : 1
- checkColumnNames : 2
- buildRow : 1
- writeHeaders : 0
- writeValues : 0
- checkQuotes : 1
README¶
Quelques syntaxes Markdown
#fait des titres. Plus il y a de#, plus le titre est petit.[texte](lien)crée des liens hypertextes.Pour créer des blocs de code
cpp(vous pouvez aussi utiliserbash,txt, etc.) :
```cpp
int number = 42;
```**texte en gras**met le texte en gras.*texte en italique*met le texte en italique.-ou*crée des listes à puces.1.,2., etc. crée des listes numérotées.> citationsuivi de> - auteurcrée une citation avec un auteur.insère une image et affiche le texte alternatif quand l’image ne peut pas être chargée ou quand nous survolons l’image.- [ ]crée une case à cocher, et- [x]une case cochée.---insère une ligne horizontale.
Remplissez votre
README.mden commençant par le titre et une description générale de ce mini-projet.Ajoutez une section
Fonctionnalités principalesavec une courte description des (quatre) fonctionnalités principales.Ajoutez une section
Documentationavec un lien ou chemin vers la documentation depuis la racine du projet qui sera/csv-utils/docs/html/index.htmlquand nous allons générer la documentation avec Doxygen dans la partie suivante.Ajoutez une section
Installationavec les instructions suivantes.
Installation
Compiler la bibliothèque en exécutant la commande suivante dans `csv-utils/` :
```bash
make
```
La bibliothèque statique est générée dans le dossier :
```text
csv-utils/lib/libcsvutils.a
```
Recopiez `csv-utils/` avec `include/` et `lib/` dans votre projet. Un exemple se trouve dans `my-app/`.
Pour utiliser la bibliothèque, compilez avec les options suivantes :
```bash
g++ -std=c++17 main.cpp -Ichemin/vers/csv-utils/include -Lchemin/vers/csv-utils/lib -lcsvutils -o application
```Options de compilation
Nous avons vu -I pour indiquer le chemin vers les headers.
Les nouvelles options ici sont :
-Lpour indiquer au linker (dernière étape de la compilation) le chemin vers notre bibliothèque statique.-lcsvutilspour indiquer au linker qu’il faut lier la bibliothèquelibcsvutils.a(l<librairie>est transformé enlib<librairie>.a).
Créez le répertoire
TP14/my-app/avec la structure suivante.
TP14/my-app/
└── external/csv-utils/
├── include/
│ └── csv-utils.h
├── lib/
│ └── libcsvutils.a
├── main.cpp
├── compile_flags.txt
└── makefileRecopiez le contenu suivant dans les fichiers correspondants.
#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-appTestez le code de
my-app/vous-même avecmakeetmake run.Ajoutez une section
Utilisationoù vous donnez des exemples d’utilisation des fonctionnalités principales.Ajoutez une sous-section
LimitationsàUtilisationoù vous expliquez les limitations sur les fichiers CSV que nous pouvons lire ou écrire.Ajoutez une section
Licencequi indique que ce mini-projet est distribué sous licence MIT et recopiez la licence MIT dans le cours (en modifiant l’en-tête) dans le fichierLICENSE.
Doxyfile¶
Pour générer la documentation avec Doxygen, nous allons d’abord générer un fichier de configuration Doxyfile.
Générez le
Doxyfileavec la commande suivante danscsv-utils/:
doxygen -gModifiez les paramètres suivants du
Doxyfile:
PROJECT_NAME = "CsvUtils"INPUT = include ../README.mdOUTPUT_DIRECTORY = docsRECURSIVE = YES(pour inclure les sous-dossiers deinclude/, ce qui n’est pas nécessaire ici mais utile dans le cas général)GENERATE_HTML = YESGENERATE_LATEX = NOUSE_MDFILE_AS_MAINPAGE = ../README.md
Générez la documentation avec la commande suivante dans
csv-utils/:
doxygen DoxyfileVous devez voir un répertoire docs/ dans csv-utils/ qui contient le contenu de votre documentation.
Consultez la documentation générée en ouvrant
csv-utils/docs/html/index.htmldans un navigateur web.
Documenter les classes et leurs méthodes publiques dans les headers (syntaxe).
Retenir les points principaux à inclure dans un
README.Coder proprement et ne commenter que les points nécessaires (principes des bons commentaires).