C++:
/*
MIT License
Copyright (c) 2025 CoTon_TiGe_MoUaRf
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <map>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <memory>
#include <limits>
#include <conio.h> // Pour _getch() sur Windows
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define VC_EXTRALEAN
#include <Windows.h> // Pour afficher les couleurs
#endif // _WIN32
namespace fs = std::filesystem;
using namespace std;
/************************************
* Gestion des couleurs de la console
************************************/
class ConsoleColor {
public:
enum TextColor {
BLACK = 0,
DARK_BLUE = 1,
DARK_GREEN = 2,
LIGHT_BLUE = 3,
DARK_RED = 4,
MAGENTA = 5,
ORANGE = 6,
LIGHT_GRAY = 7,
GRAY = 8,
BLUE = 9,
GREEN = 10,
CYAN = 11,
RED = 12,
PINK = 13,
YELLOW = 14,
WHITE = 15
};
enum BackgroundColor {
BG_BLACK = 0,
BG_DARK_BLUE = 1,
BG_DARK_GREEN = 2,
BG_LIGHT_BLUE = 3,
BG_DARK_RED = 4,
BG_MAGENTA = 5,
BG_ORANGE = 6,
BG_LIGHT_GRAY = 7,
BG_GRAY = 8,
BG_BLUE = 9,
BG_GREEN = 10,
BG_CYAN = 11,
BG_RED = 12,
BG_PINK = 13,
BG_YELLOW = 14,
BG_WHITE = 15
};
static void print(const string &message, TextColor textColor = WHITE, BackgroundColor bgColor = BG_BLACK) {
#if defined(_WIN32)
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(handle, bgColor << 4 | textColor);
cout << message;
SetConsoleTextAttribute(handle, WHITE);
#else
cout << message;
#endif
}
static void println(const string &message, TextColor textColor = WHITE, BackgroundColor bgColor = BG_BLACK) {
print(message + "\n", textColor, bgColor);
}
};
/************************************
* Classes pour le menu interactif
************************************/
class MenuOption {
public:
MenuOption(const string &title, function<void()> action)
: title(title), action(action) {}
void select() {
action();
}
string getTitle() const {
return title;
}
private:
string title;
function<void()> action;
};
class IUserInterface {
public:
virtual void display(const string &message) = 0;
virtual char getInput() = 0;
virtual ~IUserInterface() = default;
};
class ConsoleUserInterface : public IUserInterface {
public:
void display(const string &message) override {
cout << message << endl;
}
char getInput() override {
return _getch();
}
};
/************************************
* Classe Menu (menu interactif dans la console)
************************************/
class Menu {
public:
Menu(const string &title, int width, int height)
: title(title), width(width), height(height) {}
void addOption(const MenuOption &option) {
options.push_back(option);
}
void display(IUserInterface *ui) {
ConsoleColor::println("=== " + title + " ===", ConsoleColor::YELLOW, ConsoleColor::BG_BLACK);
for (size_t i = 0; i < options.size(); ++i) {
if (currentSelection == i) {
ConsoleColor::print("> " + options[i].getTitle(), ConsoleColor::WHITE, ConsoleColor::BG_BLUE);
} else {
ConsoleColor::print(" " + options[i].getTitle(), ConsoleColor::WHITE, ConsoleColor::BG_BLACK);
}
cout << endl;
}
ConsoleColor::println("====================", ConsoleColor::WHITE, ConsoleColor::BG_BLACK);
ConsoleColor::println("Utilisez ↑/↓ pour naviguer, ← pour revenir, → ou Entrée pour sélectionner, Échap pour quitter.", ConsoleColor::WHITE, ConsoleColor::BG_BLACK);
}
void navigate(char input) {
if (input == -32) { // touche spéciale
char direction = _getch();
if (direction == 72) { // Flèche haut
currentSelection = (currentSelection + options.size() - 1) % options.size();
} else if (direction == 80) { // Flèche bas
currentSelection = (currentSelection + 1) % options.size();
} else if (direction == 75) { // Flèche gauche
throw runtime_error("Retour");
} else if (direction == 77) { // Flèche droite
select();
}
} else if (input == 13) { // Entrée
select();
}
}
void select() {
options[currentSelection].select();
}
private:
string title;
vector<MenuOption> options;
size_t currentSelection = 0;
int width;
int height;
};
/************************************
* Utilitaires CSV
************************************/
/// Trim d'une chaîne (supprime espaces en début et fin)
inline std::string trim(const std::string& s) noexcept {
const auto begin = s.find_first_not_of(" \t\n\r");
if (begin == std::string::npos) return "";
const auto end = s.find_last_not_of(" \t\n\r");
return s.substr(begin, end - begin + 1);
}
/// Parse une ligne CSV en utilisant un séparateur donné
inline std::vector<std::string> parseCSVLine(const std::string& line, char delimiter = ';') noexcept {
std::vector<std::string> tokens;
std::istringstream ss(line);
std::string token;
while (std::getline(ss, token, delimiter)) {
tokens.push_back(trim(token));
}
return tokens;
}
/************************************
* Classe de hachage utilitaire
************************************/
/// Permet de réaliser un hachage 2-universel simple.
class Hasher {
public:
template <typename T>
static std::size_t hash(std::size_t seed, const T& val) noexcept {
std::hash<T> hasher;
return hasher(val) ^ (seed * 0x9e3779b97f4a7c15ULL);
}
};
/************************************
* Classe Sketch
************************************/
/// Implémente le Count-Min Sketch avec une logique de médiane.
class Sketch {
public:
/// Constructeur initialisant une structure 3D de compteurs.
Sketch(std::size_t d, std::size_t w, std::size_t r) noexcept
: d(d), w(w), r(r),
counters(d, std::vector<std::vector<int>>(w, std::vector<int>(r, 0))) {}
/// Mise à jour du sketch avec un élément x.
template <typename T>
void update(const T& x) noexcept {
for (std::size_t o = 0; o < r; ++o) {
for (std::size_t i = 0; i < d; ++i) {
std::size_t j = Hasher::hash(i, x) % w;
++counters[i][j][o];
}
}
}
/// Calcul de la médiane d'un vecteur d'entiers.
int medianOfMeans(const std::vector<int>& values) const noexcept {
auto temp = values;
std::sort(temp.begin(), temp.end());
return temp[temp.size() / 2];
}
/// Interroge le sketch en retournant les indices des topV valeurs les plus élevées.
template <typename T>
std::vector<std::size_t> query(const T& q, std::size_t topV) const noexcept {
std::vector<int> similarities;
similarities.reserve(w);
for (std::size_t j = 0; j < w; ++j) {
std::vector<int> temps;
for (std::size_t i = 0; i < d; ++i) {
temps.insert(temps.end(), counters[i][j].begin(), counters[i][j].end());
}
similarities.push_back(medianOfMeans(temps));
}
std::vector<std::size_t> indices(w);
for (std::size_t i = 0; i < w; ++i)
indices[i] = i;
std::sort(indices.begin(), indices.end(), [&](std::size_t a, std::size_t b) {
return similarities[a] > similarities[b];
});
if (topV > indices.size()) topV = indices.size();
indices.resize(topV);
return indices;
}
private:
const std::size_t d;
const std::size_t w;
const std::size_t r;
std::vector<std::vector<std::vector<int>>> counters;
};
/************************************
* Classe TUID : gestion d'ID et du Sketch
************************************/
class TUID {
public:
TUID() noexcept
: lastID(0), sketch(4, 100, 5) {}
/// Génère et renvoie le prochain identifiant unique.
int getNextID() noexcept {
std::lock_guard<std::mutex> lock(mutex_);
int next = ++lastID;
sketch.update(next);
return next;
}
/// Remet à zéro le générateur d'ID.
void reset(int value = 0) noexcept {
std::lock_guard<std::mutex> lock(mutex_);
lastID = value;
}
/// Interroge le sketch et retourne les indices correspondants.
std::vector<std::size_t> querySketch(const int& q, std::size_t topV = 5) const noexcept {
return sketch.query(q, topV);
}
private:
int lastID;
Sketch sketch;
mutable std::mutex mutex_;
};
/************************************
* Structures représentant un enregistrement et une table
************************************/
struct Record {
int id; // ID auto-généré unique
std::vector<std::string> data; // Données pour les colonnes (hors CleID et ID)
};
struct Table {
std::string name; // Nom de la table
std::vector<std::string> columns; // Liste des colonnes (les deux premières étant réservées)
std::map<std::string, Record> records; // Clé au format "0001" -> enregistrement
};
/************************************
* Classe Database
************************************/
class Database {
public:
Database() : tableDir("db_tables") {
initializeFolder();
loadExistingTables();
}
/// Crée une nouvelle table et l'enregistre.
Table* createTable() {
Table newTable;
cout << "Entrez le nom de la nouvelle table : ";
std::getline(std::cin, newTable.name);
if (newTable.name.empty()) {
cerr << "Le nom de la table ne peut pas être vide." << endl;
return nullptr;
}
if (tables.find(newTable.name) != tables.end()) {
cerr << "Une table du même nom existe déjà." << endl;
return nullptr;
}
cout << "Entrez les noms des colonnes (séparés par des virgules) : ";
std::string columnsInput;
std::getline(std::cin, columnsInput);
// Les deux premières colonnes sont réservées.
newTable.columns.push_back("CleID");
newTable.columns.push_back("ID");
auto tokens = parseCSVLine(columnsInput, ',');
for (const auto& col : tokens) {
if (!col.empty())
newTable.columns.push_back(col);
}
try {
saveTable(newTable);
} catch (const std::exception& e) {
cerr << "Erreur lors de l'enregistrement de la table : " << e.what() << endl;
return nullptr;
}
tables[newTable.name] = newTable;
cout << "Table '" << newTable.name << "' créée avec succès." << endl;
return &tables[newTable.name];
}
/// Sélectionne une table existante via son nom.
Table* selectTable() {
if (tables.empty()) {
cout << "Aucune table n'existe. Veuillez en créer une d'abord." << endl;
return nullptr;
}
cout << "Liste des tables existantes :" << endl;
for (const auto& [name, table] : tables)
cout << "- " << name << "\n";
cout << "Entrez le nom de la table à utiliser : ";
std::string name;
std::getline(std::cin, name);
auto it = tables.find(name);
if (it == tables.end()) {
cout << "Table non trouvée." << endl;
return nullptr;
}
try {
loadTable(it->second);
} catch (const std::exception& e) {
cerr << "Erreur lors du chargement de la table : " << e.what() << endl;
return nullptr;
}
return &it->second;
}
/// Menu interactif pour gérer les enregistrements (ajout, affichage, mise à jour et suppression).
void recordMenu(Table& table) {
while (true) {
cout << "\n===== MENU DES ENREGISTREMENTS =====" << endl;
cout << "1. Ajouter un enregistrement" << endl;
cout << "2. Afficher tous les enregistrements" << endl;
cout << "3. Rechercher un enregistrement (par CleID)" << endl;
cout << "4. Modifier un enregistrement" << endl;
cout << "5. Supprimer un enregistrement" << endl;
cout << "6. Retour au menu principal" << endl;
cout << "Votre choix : ";
std::string choix;
std::getline(std::cin, choix);
if (choix == "1") {
addRecord(table);
} else if (choix == "2") {
displayRecords(table);
} else if (choix == "3") {
searchRecord(table);
} else if (choix == "4") {
updateRecord(table);
} else if (choix == "5") {
deleteRecord(table);
} else if (choix == "6") {
break;
} else {
cout << "Choix invalide, veuillez réessayer." << endl;
}
}
}
/// Ajoute un enregistrement à la table active.
void addRecord(Table& table) {
Record newRecord;
newRecord.id = uidGenerator.getNextID();
const std::string key = generateKeyID(newRecord.id);
const auto numColumns = table.columns.size();
std::string input;
for (std::size_t i = 2; i < numColumns; ++i) { // Colonnes à partir de la troisième
cout << "Entrez la valeur pour '" << table.columns[i] << "' : ";
std::getline(std::cin, input);
if (input.find_first_not_of(" \t\n\r") == std::string::npos) {
cerr << "La valeur pour '" << table.columns[i] << "' ne peut être vide." << endl;
return;
}
newRecord.data.push_back(input);
}
if (table.records.find(key) != table.records.end()) {
cerr << "Erreur : un enregistrement avec cet ID existe déjà." << endl;
return;
}
table.records[key] = std::move(newRecord);
try {
saveTable(table);
} catch (const std::exception& e) {
cerr << "Erreur lors de l'enregistrement de la table : " << e.what() << endl;
return;
}
cout << "Enregistrement ajouté avec succès. Clé générée : " << key << endl;
}
/// Affiche tous les enregistrements d'une table.
void displayRecords(const Table& table) const noexcept {
if (table.records.empty()) {
cout << "Aucun enregistrement n'existe dans cette table." << endl;
return;
}
cout << "\nEnregistrements de la table '" << table.name << "' :" << endl;
for (const auto& [key, rec] : table.records) {
cout << key << " => ID: " << rec.id;
for (std::size_t i = 0; i < rec.data.size(); ++i) {
cout << ", " << table.columns[i + 2] << ": " << rec.data[i];
}
cout << "\n";
}
}
/// Recherche un enregistrement en fonction de la clé (CleID).
void searchRecord(const Table& table) const {
cout << "Entrez la clé de l'enregistrement à rechercher : ";
std::string key;
std::getline(std::cin, key);
auto it = table.records.find(key);
if (it == table.records.end()) {
cout << "Enregistrement non trouvé." << endl;
return;
}
const Record& rec = it->second;
cout << "Enregistrement trouvé : " << key << " => ID: " << rec.id;
for (std::size_t i = 0; i < rec.data.size(); ++i) {
cout << ", " << table.columns[i + 2] << ": " << rec.data[i];
}
cout << "\n";
}
/// Permet de modifier un enregistrement existant.
void updateRecord(Table& table) {
cout << "Entrez la clé de l'enregistrement à modifier : ";
std::string key;
std::getline(std::cin, key);
auto it = table.records.find(key);
if (it == table.records.end()) {
cout << "Enregistrement non trouvé." << endl;
return;
}
Record& rec = it->second;
for (std::size_t i = 0; i < rec.data.size(); ++i) {
cout << "Valeur actuelle pour '" << table.columns[i + 2] << "' : " << rec.data[i] << endl;
cout << "Entrez la nouvelle valeur (laisser vide pour conserver) : ";
std::string nouvelleValeur;
std::getline(std::cin, nouvelleValeur);
if (!nouvelleValeur.empty()) {
rec.data[i] = nouvelleValeur;
}
}
try {
saveTable(table);
} catch (const std::exception& e) {
cerr << "Erreur lors de la sauvegarde de la table : " << e.what() << endl;
return;
}
cout << "Enregistrement mis à jour avec succès." << endl;
}
/// Permet la suppression d'un enregistrement existant.
void deleteRecord(Table& table) {
cout << "Entrez la clé de l'enregistrement à supprimer : ";
std::string key;
std::getline(std::cin, key);
auto it = table.records.find(key);
if (it == table.records.end()) {
cout << "Enregistrement non trouvé." << endl;
return;
}
table.records.erase(it);
try {
saveTable(table);
} catch (const std::exception& e) {
cerr << "Erreur lors de la sauvegarde de la table : " << e.what() << endl;
return;
}
cout << "Enregistrement supprimé avec succès." << endl;
}
private:
const std::string tableDir;
std::map<std::string, Table> tables;
TUID uidGenerator;
/// Initialise le dossier de sauvegarde des tables.
void initializeFolder() {
try {
if (!fs::exists(tableDir))
fs::create_directory(tableDir);
} catch (const fs::filesystem_error& e) {
throw std::runtime_error("Erreur lors de la création du dossier : " + std::string(e.what()));
}
}
/// Génère une clé (format "0001") à partir d'un ID.
std::string generateKeyID(int id) const {
std::ostringstream oss;
oss << std::setw(4) << std::setfill('0') << id;
return oss.str();
}
/// Sauvegarde une table dans un fichier CSV.
void saveTable(const Table &table) const {
const std::string path = tableDir + "/" + table.name + ".csv";
std::ofstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Erreur lors de l'ouverture du fichier " + path + " pour écriture.");
}
// Écriture de l'en-tête
for (std::size_t i = 0; i < table.columns.size(); ++i) {
file << table.columns[i];
if (i + 1 < table.columns.size())
file << ";";
}
file << "\n";
// Écriture des enregistrements
for (const auto& [key, rec] : table.records) {
file << key << ";" << rec.id;
for (const auto& d : rec.data)
file << ";" << d;
file << "\n";
}
file.close();
if (file.fail()) {
throw std::runtime_error("Erreur lors de l'écriture dans le fichier " + path);
}
}
/// Charge une table depuis un fichier CSV.
void loadTable(Table &table) {
const std::string path = tableDir + "/" + table.name + ".csv";
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Fichier " + path + " introuvable.");
}
table.records.clear();
std::string line;
bool firstLine = true;
while (std::getline(file, line)) {
if (firstLine) { // En-tête
table.columns = parseCSVLine(line, ';');
firstLine = false;
continue;
}
if (line.empty()) continue;
auto tokens = parseCSVLine(line, ';');
if (tokens.size() < 2) continue;
Record rec;
const std::string key = tokens[0];
try {
rec.id = std::stoi(tokens[1]);
} catch (const std::exception&) {
cerr << "Erreur de conversion de l'ID pour la clé " << key << endl;
continue;
}
for (std::size_t i = 2; i < tokens.size(); ++i)
rec.data.push_back(tokens[i]);
table.records[key] = std::move(rec);
}
file.close();
// Met à jour le TUID avec l'ID maximum observé.
int maxID = 0;
for (const auto& [key, rec] : table.records) {
maxID = std::max(maxID, rec.id);
}
uidGenerator.reset(maxID);
}
/// Charge toutes les tables existantes dans le dossier.
void loadExistingTables() {
try {
if (!fs::exists(tableDir))
return;
for (const auto& entry : fs::directory_iterator(tableDir)) {
if (entry.is_regular_file() && entry.path().extension() == ".csv") {
Table table;
table.name = entry.path().stem().string();
loadTable(table);
tables[table.name] = std::move(table);
}
}
} catch (const fs::filesystem_error& e) {
cerr << "Erreur lors de la lecture du répertoire : " << e.what() << endl;
}
}
};
/************************************
* Classe MenuManager
************************************/
class MenuManager {
public:
MenuManager(std::unique_ptr<IUserInterface> ui) : userInterface(std::move(ui)) {}
void run() {
bool exitFlag = false;
while (!exitFlag) {
try {
Menu mainMenu("Menu Principal", 80, 24);
mainMenu.addOption(MenuOption("Gérer la base de données", [this]() { runDatabaseMenu(); }));
mainMenu.addOption(MenuOption("Quitter", []() {
ConsoleColor::println("Au revoir!", ConsoleColor::RED);
exit(0);
}));
runMenu(mainMenu);
exitFlag = true;
} catch (const runtime_error &e) {
if (string(e.what()) != "Retour")
throw;
}
}
}
private:
std::unique_ptr<IUserInterface> userInterface;
void clearScreen() {
#if defined(_WIN32)
system("cls");
#else
system("clear");
#endif
}
void runMenu(Menu &menu) {
while (true) {
clearScreen();
menu.display(userInterface.get());
char input = userInterface->getInput();
if (input == 27) { // Échap
break;
}
try {
menu.navigate(input);
} catch (const runtime_error &e) {
if (string(e.what()) == "Retour")
return;
}
}
}
void runDatabaseMenu() {
Database db;
while (true) {
clearScreen();
Menu dbMenu("Menu Base de Données", 80, 24);
dbMenu.addOption(MenuOption("Créer une nouvelle table", [&db]() {
Table* table = db.createTable();
if (table)
db.recordMenu(*table);
}));
dbMenu.addOption(MenuOption("Utiliser une table existante", [&db]() {
Table* table = db.selectTable();
if (table)
db.recordMenu(*table);
}));
dbMenu.addOption(MenuOption("Retour", []() {}));
try {
runMenu(dbMenu);
break;
} catch (const std::runtime_error &e) {
if (string(e.what()) != "Retour")
throw;
else
break;
}
}
}
};
/************************************
* Fonction main
************************************/
int main() {
try {
unique_ptr<IUserInterface> ui = make_unique<ConsoleUserInterface>();
MenuManager manager(std::move(ui));
manager.run();
// Exemple d'utilisation direct de la base de données pour tester le TUID et le Sketch :
Database db;
Table* table = db.createTable();
if (table) {
db.addRecord(*table);
db.displayRecords(*table);
TUID generator;
int nextID = generator.getNextID();
cout << "\nProchain ID généré : " << nextID << "\n";
auto indices = generator.querySketch(nextID, 3);
cout << "Indices retournés par querySketch : ";
for (const auto idx : indices) {
cout << idx << " ";
}
cout << "\nAppuyez sur une touche pour fermer...";
(void)getchar();
}
} catch (const std::exception &ex) {
cerr << "Exception capturée dans main : " << ex.what() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
This project is a console application in C++ that allows for the management of a simple database. It includes features to create tables, add, display, modify, and delete records. The application uses an interactive menu to navigate through the different options.
Features
- Console Color Management: Allows displaying colored messages for better readability.
- Interactive Menu: A menu system that enables the user to easily navigate between different options.
- Record Management: Adding, displaying, searching, modifying, and deleting records in the tables.
- Table Management: Creating new tables and selecting existing tables.
- Use of Advanced Data Structures: Utilizes Count-Min Sketch for hashing operations and TUID for managing unique identifiers.
- A C++ compiler compatible with C++17 or higher.
- The C++ standard library (including for file management).
To compile the project, use the following command in your terminal:
bash
g++ -o database_manager main.cpp -std=c++17
Make sure that the file main.cpp contains the provided source code.
Execution
To run the application, use the following command:
bash
./database_manager
Usage
- Create a New Table: Select the option to create a new table and follow the instructions to enter the table name and column names.
- Use an Existing Table: Select an existing table to manage its records.
- Add a Record: Enter the values for each column of the record.
- Display All Records: Display all records from the selected table.
- Modify a Record: Search for a record by its key and modify the values.
- Delete a Record: Delete a record by providing its key.
- The first two columns of each table are reserved for CleID and ID.
- Records are saved in CSV files in the db_tables folder.
- In case of an error, appropriate error messages will be displayed in the console.