Database Manager: A C++ Console Application

Joined
Jun 12, 2020
Messages
17
Reaction score
0
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;
}
Project Description
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.
Prerequisites

  • A C++ compiler compatible with C++17 or higher.
  • The C++ standard library (including for file management).
Compilation
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

  1. Create a New Table: Select the option to create a new table and follow the instructions to enter the table name and column names.
  2. Use an Existing Table: Select an existing table to manage its records.
  3. Add a Record: Enter the values for each column of the record.
  4. Display All Records: Display all records from the selected table.
  5. Modify a Record: Search for a record by its key and modify the values.
  6. Delete a Record: Delete a record by providing its key.
Notes

  • 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.
 
Joined
Jun 12, 2020
Messages
17
Reaction score
0
Introducing the Enhanced C++ Database Management System
I am excited to share an improved version of my C++ Database Management System, which I previously published. This updated version incorporates several enhancements aimed at improving usability, performance, and code organization.

Key Features of the Enhanced Version:​


  1. User -Friendly Console Interface: The application now features a more intuitive console interface, allowing users to navigate through menus using arrow keys. This makes it easier to manage database tables and records.
  2. Improved Error Handling: The code has been refactored to provide clearer error messages and handle exceptions more gracefully, ensuring a smoother user experience.
  3. Dynamic Record Display: Users can now view records in a more structured format, with clear labels and organized output, making it easier to read and understand the data.
  4. Enhanced Data Management: The system supports adding, updating, deleting, and searching for records with improved input validation to prevent errors.
  5. Count-Min Sketch Implementation: The new version includes a Count-Min Sketch algorithm for efficient frequency estimation, which can be useful for analyzing large datasets.
  6. Modular Design: The code is organized into distinct classes and functions, promoting better maintainability and readability.
  7. Cross-Platform Compatibility: The application is designed to work seamlessly on both Windows and Unix-like systems, ensuring a wider range of usability.

Code Overview​


The code is structured into several key components, including:

  • ConsoleColor Class: Manages console text colors for better visual feedback.
  • IUser Interface and ConsoleUser Interface: Abstracts user input and output handling.
  • Menu and MenuOption Classes: Facilitates the creation of interactive menus.
  • Database Class: Handles table creation, record management, and file operations.
  • TUID Class: Generates unique IDs for records using a thread-safe approach.

Getting Started​


To run the application, simply compile the code and execute the resulting binary. You will be prompted to create or select a database table, after which you can manage records as needed.
I hope you find this enhanced version useful for your projects or studies. I welcome any feedback or suggestions for further improvements!
Feel free to check out the code below:
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 <array>
#include <chrono>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <map>
#include <mutex>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#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;

// -----------------------------------------------------------------------------
// Gestion des couleurs de la console
// -----------------------------------------------------------------------------
class ConsoleColor {
public:
    enum class TextColor : int {
        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 class BackgroundColor : int {
        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(std::string_view message, TextColor textColor = TextColor::WHITE, BackgroundColor bgColor = BackgroundColor::BG_BLACK) {
    #if defined(_WIN32)
        HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
        SetConsoleTextAttribute(handle, (static_cast<int>(bgColor) << 4) | static_cast<int>(textColor));
        std::cout << message;
        SetConsoleTextAttribute(handle, static_cast<int>(TextColor::WHITE));
    #else
        std::cout << message;
    #endif
    }

    static void println(std::string_view message, TextColor textColor = TextColor::WHITE, BackgroundColor bgColor = BackgroundColor::BG_BLACK) {
        print(std::string(message) + "\n", textColor, bgColor);
    }
    
    static void setColor(TextColor textColor, BackgroundColor bgColor) {
        HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
        SetConsoleTextAttribute(handle, (static_cast<int>(bgColor) << 4) | static_cast<int>(textColor));
    }

    static void reset() {
        setColor(TextColor::WHITE, BackgroundColor::BG_BLACK);
    }
    
};

// -----------------------------------------------------------------------------
// Interface et classes pour le menu interactif
// -----------------------------------------------------------------------------
class IUserInterface {
public:
    virtual void display(std::string_view message) = 0;
    virtual char getInput() = 0;
    virtual ~IUserInterface() = default;
};

class ConsoleUserInterface : public IUserInterface {
public:
    void display(std::string_view message) override {
        std::cout << message << std::endl;
    }

    char getInput() override {
        return _getch();
    }
};

class MenuOption {
public:
    MenuOption(std::string title, std::function<void()> action)
        : title_(std::move(title)), action_(std::move(action)) {}

    void select() {
        action_();
    }

    std::string getTitle() const {
        return title_;
    }

private:
    std::string title_;
    std::function<void()> action_;
};

class Menu {
public:
    Menu(std::string title, int width, int height)
        : title_(std::move(title)), width_(width), height_(height) {}
    void addOption(const MenuOption& option) {
        options_.push_back(option);
    }
    void displayMenu(IUserInterface* ui) { // Correction ici
    ConsoleColor::println("=== " + title_ + " ===", ConsoleColor::TextColor::YELLOW, ConsoleColor::BackgroundColor::BG_BLACK);
    for (std::size_t i = 0; i < options_.size(); ++i) {
        if (currentSelection_ == i) {
            ConsoleColor::print("> " + options_[i].getTitle(), ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLUE);
        } else {
            ConsoleColor::print("  " + options_[i].getTitle(), ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLACK);
        }
        std::cout << std::endl;
    }
    ConsoleColor::println("====================", ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLACK);
    ConsoleColor::println("Up/Down arrows to navigate, Left arrow to go back, Right arrow or Enter to select, Escape to exit.", ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLACK);
}

    // Navigation par touches.
    // Si la touche « gauche » est pressée, une exception est levée pour signaler le retour.
    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 std::runtime_error("Return");
            } else if (direction == 77) { // Flèche droite
                select();
            }
        } else if (input == 13) { // Entrée
            select();
        }
    }

    void select() {
        options_[currentSelection_].select();
    }
    
    void navigateUp() {
    currentSelection_ = (currentSelection_ + options_.size() - 1) % options_.size();
}

void navigateDown() {
    currentSelection_ = (currentSelection_ + 1) % options_.size();
}

private:
    std::string title_;
    std::vector<MenuOption> options_;
    std::size_t currentSelection_ = 0;
    int width_;
    int height_;
};

// -----------------------------------------------------------------------------
// Utilitaires CSV et hachage
// -----------------------------------------------------------------------------
inline std::string trim(std::string_view s) noexcept {
    const auto begin = s.find_first_not_of(" \t\n\r");
    if (begin == std::string_view::npos)
        return "";
    const auto end = s.find_last_not_of(" \t\n\r");
    return std::string(s.substr(begin, end - begin + 1));
}

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;
}
/// 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 (Count-Min Sketch avec logique de médiane)
// -----------------------------------------------------------------------------
class Sketch {
public:
    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))) {}

    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];
            }
        }
    }

    int medianOfMeans(const std::vector<int>& values) const noexcept {
        std::vector<int> temp = values;
        std::sort(temp.begin(), temp.end());
        return temp[temp.size() / 2];
    }

    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:
    std::size_t d_;
    std::size_t w_;
    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) {}

    int getNextID() noexcept {
        std::lock_guard<std::mutex> lock(mutex_);
        int next = ++lastID_;
        sketch_.update(next);
        return next;
    }

    void reset(int value = 0) noexcept {
        std::lock_guard<std::mutex> lock(mutex_);
        lastID_ = value;
    }

    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 pour les enregistrements et la 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é format "0001" -> enregistrement
};

// -----------------------------------------------------------------------------
// Classe Database
// -----------------------------------------------------------------------------
class Database {
public:
    Database() : tableDir_("db_tables") {
        initializeFolder();
        loadExistingTables();
    }

    // Crée une nouvelle table et l'enregistre
    // Retourne std::optional<Table> pour optimiser le retour en cas d'erreur.
    std::optional<Table> createTable() {
        Table newTable;
        std::cout << "Enter the name of the new table: ";
        std::getline(std::cin, newTable.name);
        if (newTable.name.empty()) {
            throw std::runtime_error("The name of the table cannot be empty.");
        }
        if (tables_.find(newTable.name) != tables_.end()) {
            throw std::runtime_error("A table with the same name already exists.");
        }

        std::cout << "Enter the names of the columns (separated by commas): ";
        std::string columnsInput;
        std::getline(std::cin, columnsInput);
        // Colonnes réservées
        newTable.columns.push_back("KeyID");
        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) {
            throw std::runtime_error("Error while saving the table: " + std::string(e.what()));
        }
        tables_[newTable.name] = newTable;
       std::cout << "Table '" << newTable.name << "' created successfully." << std::endl;
        return newTable;
    }

    // Sélectionne une table existante via son nom
    Table* selectTable() {
    if (tables_.empty()) {
        std::cout << "No table exists. Please create one first." << std::endl;
        return nullptr;
    }

    std::vector<std::string> tableNames;  // Remove 'const' here to allow push_back
    for (const auto &entry : tables_) {
        tableNames.push_back(entry.first);
    }

    size_t currentSelection = 0;

    while (true) {
        system("cls"); // Ou system("clear") sous Linux/macOS
        ConsoleColor::println("\n===== TABLE SELECTION =====", ConsoleColor::TextColor::YELLOW);
        
        for (size_t i = 0; i < tableNames.size(); ++i) {
            if (i == currentSelection) {
                ConsoleColor::println("> " + tableNames[i], ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLUE);
            } else {
                ConsoleColor::println("  " + tableNames[i], ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLACK);
            }
        }

        ConsoleColor::println("\nUse the up and down arrows to navigate, press Enter to select, and Escape to go back.", ConsoleColor::TextColor::GRAY);

        int ch = _getch();
        if (ch == 27) { // Échap
            return nullptr;
        } else if (ch == 224) { // Touche spéciale
            int arrow = _getch();
            if (arrow == 72) { // Flèche haut
                currentSelection = (currentSelection == 0) ? tableNames.size() - 1 : currentSelection - 1;
            } else if (arrow == 80) { // Flèche bas
                currentSelection = (currentSelection + 1) % tableNames.size();
            }
        } else if (ch == 13) { // Entrée
            const std::string& name = tableNames[currentSelection];
            auto it = tables_.find(name);
            if (it == tables_.end()) {
                std::cout << "Table not found." << std::endl;
                return nullptr;
            }
            try {
                loadTable(it->second);
            } catch (const std::exception &e) {
                throw std::runtime_error("Error while loading the table: " + std::string(e.what()));
            }
            return &it->second;
        }
    }
}


    
    void displayOptions(const std::vector<std::string>& options, size_t currentSelection) {
    for (size_t i = 0; i < options.size(); ++i) {
        if (i == currentSelection) {
            ConsoleColor::println("> " + options[i], ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLUE);
        } else {
            ConsoleColor::println("  " + options[i], ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLACK);
        }
    }
}

bool handleInput(int ch, size_t& currentSelection, size_t optionsCount, Table& table) {
    if (ch == 27) { // Touche Échap
        return true; // Indique qu'on veut sortir du menu
    } else if (ch == 224) { // Touche spéciale flèches sous Windows
        int arrow = _getch();
        if (arrow == 72) { // Flèche haut
            currentSelection = (currentSelection == 0) ? optionsCount - 1 : currentSelection - 1;
        } else if (arrow == 80) { // Flèche bas
            currentSelection = (currentSelection + 1) % optionsCount;
        }
    } else if (ch == 13) { // Entrée
        switch (currentSelection) {
            case 0: addRecord(table); break;
            case 1: displayRecords(table); break;
            case 2: searchRecord(table); break;
            case 3: updateRecord(table); break;
            case 4: deleteRecord(table); break;
            case 5: return true; // Retour au menu principal
        }
        ConsoleColor::println("\nPress any key to continue...");
        _getch();
    }
    return false; // Indique qu'on reste dans le menu
}


    // Menu interactif pour gérer les enregistrements (ajout, affichage, modification et suppression)
    void recordMenu(Table& table) {
    const std::vector<std::string> options = {
        "Add a record",
        "Display all records",
        "Search for a record (by KeyID)",
        "Edit a record",
        "Delete a record",
        "Return to the main menu"
    };

    size_t currentSelection = 0;

    while (true) {
        system("cls"); // Ou system("clear") sous Linux/macOS
        ConsoleColor::println("\n===== RECORDS MENU =====", ConsoleColor::TextColor::YELLOW);
        displayOptions(options, currentSelection);

        ConsoleColor::println("\nUse the up and down arrows to navigate, press Enter to select, and Escape to go back.", ConsoleColor::TextColor::GRAY);


        int ch = _getch();
        if (handleInput(ch, currentSelection, options.size(), table)) {
            break; // Sortie du menu
        }
    }
}
    std::string getInputForColumn(const std::string& columnName) {
    std::string input;
    std::cout << "Enter the value for '" << columnName << "' : ";
    std::getline(std::cin, input);
    if (input.find_first_not_of(" \t\n\r") == std::string::npos) {
        throw std::runtime_error("The value for '" + columnName + "' cannot be empty.");
    }
    return input;
}
    // Ajoute un enregistrement à la table active.
    void addRecord(Table& table) {
    Record newRecord;
    newRecord.id = uidGenerator_.getNextID();
    const std::string key = generateKeyID(newRecord.id);
    
    // Saisie pour chaque colonne (à partir de la 3ème colonne)
    for (std::size_t i = 2; i < table.columns.size(); ++i) {
        newRecord.data.push_back(getInputForColumn(table.columns[i]));
    }

    if (table.records.find(key) != table.records.end()) {
        throw std::runtime_error("Error: a record with this ID already exists.");
    }

    table.records[key] = std::move(newRecord);
    saveTable(table);
    std::cout << "Record added successfully. Generated key: " << key << std::endl;
}

    // Affiche tous les enregistrements d'une table.
    /*
    void displayRecords(const Table& table) const noexcept {
    if (table.records.empty()) {
        std::cout << "No records exist in this table." << std::endl;
        return;
    }

    // Largeurs fixes pour l'affichage
    const int keyWidth     = 10;
    const int idWidth      = 10;
    const int dataColWidth = 15;
    const std::string colSeparator = " "; // séparateur entre les colonnes

    // Nombre total de colonnes affichées (clé, id et données éventuelles)
    int nbDisplayedCols = 2 + (table.columns.size() > 2 ? table.columns.size() - 2 : 0);

    // Calcul de la largeur totale (on a nbDisplayedCols - 1 séparateurs)
    int totalWidth = keyWidth + idWidth + (nbDisplayedCols - 2) * dataColWidth + dataColWidth
                     + (nbDisplayedCols - 1) * static_cast<int>(colSeparator.size());

    // Affichage des en-têtes avec couleur de fond (alignement à gauche pour l'entête)
    ConsoleColor::setColor(ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLUE);
    std::cout << std::left << std::setw(keyWidth) << "Key" << colSeparator
              << std::left << std::setw(idWidth)  << "ID" << colSeparator;
    // Affichage des en-têtes pour les colonnes de données (sans séparateur final)
    for (std::size_t i = 2; i < table.columns.size(); ++i) {
        std::cout << std::left << std::setw(dataColWidth) << table.columns[i];
        if (i < table.columns.size() - 1) {  // ajouter le séparateur si ce n'est PAS le dernier
            std::cout << colSeparator;
        }
    }
    ConsoleColor::reset();
    std::cout << std::endl;

    // Affichage de la ligne de séparation
    std::cout << std::string(totalWidth, '-') << std::endl;

    // Affichage des enregistrements
    for (const auto& [key, rec] : table.records) {
        ConsoleColor::setColor(ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLACK);
        // Affichage des deux premières colonnes avec séparateur
        std::cout << std::left << std::setw(keyWidth) << key << colSeparator
                  << std::left << std::setw(idWidth)  << rec.id << colSeparator;
        // Affichage des champs de données
        std::size_t nbData = rec.data.size();
        for (std::size_t i = 0; i < nbData; ++i) {
            // Pour le dernier champ, utiliser un alignement à droite
            if (i == nbData - 1) {
                std::cout << std::right << std::setw(dataColWidth) << rec.data[i];
                // Pas de séparateur final
            } else {
                std::cout << std::left << std::setw(dataColWidth) << rec.data[i] << colSeparator;
            }
        }
        ConsoleColor::reset();
        std::cout << std::endl;

        // Ligne de séparation après chaque enregistrement
        std::cout << std::string(totalWidth, '-') << std::endl;
    }
}
*/
void displayRecords(const Table& table) const noexcept {
    if (table.records.empty()) {
        std::cout << "No records exist in this table." << std::endl;
        return;
    }
    
    // On regroupe les enregistrements dans un vecteur pour disposer d'un ordre fixe
    std::vector<std::pair<std::string, Record>> recVector(table.records.begin(), table.records.end());
    
    // S'assurer que currentIndex est initialisé à 0
    std::size_t currentIndex = 0;
    
    // Fonction lambda pour afficher un enregistrement avec un design amélioré
    auto displayRecordAt = [&]() {
        // Effacer l'écran (dépendant du système)
        #ifdef _WIN32
            system("cls");
        #else
            system("clear");
        #endif
        
        const auto& key = recVector[currentIndex].first;
        const Record& rec = recVector[currentIndex].second;
        
        const int labelWidth = 12;
        const int fieldWidth = 30;
        // Calculer la largeur totale (marges et bordures comprises)
        const int totalWidth = 2 + labelWidth + fieldWidth + 2;
        
        // Affichage de l'entête avec bandeau de couleur
        ConsoleColor::setColor(ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLUE);
        std::cout << "\n" << std::string(totalWidth, '=') << std::endl;
        // Afficher le numéro d'enregistrement en ajoutant 1 à currentIndex
        std::cout << std::left << std::setw(totalWidth)
                  << "  Record (" + std::to_string(currentIndex + 1) + " sur " + std::to_string(recVector.size()) + ")  "
                  << std::endl;
        std::cout << std::string(totalWidth, '=') << std::endl;
        ConsoleColor::reset();
        
        // Générer la bordure
        std::string borderLine = "+" + std::string(totalWidth - 2, '-') + "+";
        
        // Lambda pour afficher un champ donné
        auto printField = [&](const std::string &label, const auto &value) {
            std::ostringstream oss;
            oss << value;
            std::cout << "| " << std::left << std::setw(labelWidth) << label
                      << " : " << std::left << std::setw(fieldWidth) << oss.str() << " |" << std::endl;
        };
        
        std::cout << borderLine << std::endl;
        // Affichage des champs avec coloration pour les libellés
        ConsoleColor::setColor(ConsoleColor::TextColor::YELLOW, ConsoleColor::BackgroundColor::BG_BLACK);
        printField("KEY", key);
        printField("ID", rec.id);
        ConsoleColor::reset();
        
        // Afficher les autres champs, à partir du 3ème élément de table.columns
        for (std::size_t i = 0; i < rec.data.size(); ++i) {
            std::string columnTitle = (i + 2 < table.columns.size())
                                      ? table.columns[i + 2]
                                      : ("Field" + std::to_string(i));
            printField(columnTitle, rec.data[i]);
        }
        
        std::cout << borderLine << std::endl;
        
        // Afficher le message d'aide en bas
        ConsoleColor::setColor(ConsoleColor::TextColor::LIGHT_BLUE, ConsoleColor::BackgroundColor::BG_BLACK);
        std::cout << "\nUtilisez ← et → pour naviguer, ou tapez 'q' pour quitter." << std::endl;
        ConsoleColor::reset();
    };
    
    // Afficher le premier enregistrement en s'assurant que currentIndex vaut 0
    displayRecordAt();
    
    // Boucle pour la navigation entre enregistrements
    bool quit = false;
    while (!quit) {
        #ifdef _WIN32
            int ch = _getch();
            if (ch == 'q' || ch == 'Q') {
                quit = true;
            }
            else if (ch == 224) { // touche spéciale (flèches, ...)
                int arrow = _getch();
                if (arrow == 75) { // flèche gauche
                    if (currentIndex > 0) {
                        --currentIndex;
                        displayRecordAt();
                    }
                }
                else if (arrow == 77) { // flèche droite
                    if (currentIndex + 1 < recVector.size()) {
                        ++currentIndex;
                        displayRecordAt();
                    }
                }
            }
        #else
            int ch = _getch();
            if (ch == 'q' || ch == 'Q') {
                quit = true;
            }
            else if (ch == 27) { // caractère d'échappement
                int ch1 = _getch();
                int ch2 = _getch();
                if(ch1 == 91) {
                    if(ch2 == 68) { // flèche gauche
                        if (currentIndex > 0) {
                            --currentIndex;
                            displayRecordAt();
                        }
                    }
                    else if(ch2 == 67) { // flèche droite
                        if (currentIndex + 1 < recVector.size()) {
                            ++currentIndex;
                            displayRecordAt();
                        }
                    }
                }
            }
        #endif
    }
}



    // Recherche un enregistrement par CleID.
   void searchRecord(const Table& table) const {
    std::cout << "Enter the key of the record to search for: ";
    std::string key;
    std::getline(std::cin, key);
    auto it = table.records.find(key);
    if (it == table.records.end()) {
        std::cout << "Record not found." << std::endl;
        return;
    }

    const Record& rec = it->second;
    // Affichage formaté du résultat
    ConsoleColor::setColor(ConsoleColor::TextColor::WHITE, ConsoleColor::BackgroundColor::BG_BLUE);
    std::cout << "\n=== Record found ===" << std::endl;
    ConsoleColor::reset();

    // Largeur pour l'intitulé des champs
    const int labelWidth = 10;
    std::cout << std::left << std::setw(labelWidth) << "KEY:" << key    << std::endl;
    std::cout << std::left << std::setw(labelWidth) << "ID:"  << rec.id << std::endl;

    // Affichage des autres champs en utilisant les colonnes à partir du 3ème élément de table.columns
    for (std::size_t i = 0; i < rec.data.size(); ++i) {
        // Sécurité : si table.columns ne contient pas d'élément à l'indice i+2, on fournit un nom générique
        std::string columnTitle = (i + 2 < table.columns.size()) ? table.columns[i + 2] : "Field" + std::to_string(i);
        std::cout << std::left << std::setw(labelWidth) << (columnTitle + ":")
                  << rec.data[i] << std::endl;
    }
    std::cout << "\n";
}

    // Permet de modifier un enregistrement existant.
    void updateRecord(Table& table) {
        std::cout << "Enter the key of the record to modify: : ";
        std::string key;
        std::getline(std::cin, key);
        auto it = table.records.find(key);
        if (it == table.records.end()) {
            std::cout << "Record not found." << std::endl;
            return;
        }
        Record& rec = it->second;
        for (std::size_t i = 0; i < rec.data.size(); ++i) {
            std::cout << "Current value for '" << table.columns[i + 2] << "' : " << rec.data[i] << std::endl;
            std::cout << "Enter the new value (leave empty to keep) : ";
            std::string nouvelleValeur;
            std::getline(std::cin, nouvelleValeur);
            if (!nouvelleValeur.empty()) {
                rec.data[i] = nouvelleValeur;
            }
        }
        try {
            saveTable(table);
        } catch (const std::exception &e) {
            throw std::runtime_error("Error while saving the table: " + std::string(e.what()));
        }
        std::cout << "Record updated successfully." << std::endl;
    }

    // Permet de supprimer un enregistrement existant.
    void deleteRecord(Table& table) {
        std::cout << "Enter the key of the record to delete : ";
        std::string key;
        std::getline(std::cin, key);
        auto it = table.records.find(key);
        if (it == table.records.end()) {
            std::cout << "Record not found." << std::endl;
            return;
        }
        table.records.erase(it);
        try {
            saveTable(table);
        } catch (const std::exception &e) {
            throw std::runtime_error("Error while saving the table: " + std::string(e.what()));
        }
        std::cout << "Record deleted successfully." << std::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("Error while creating the folder: " + 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("Error while opening the file " + path + " for writing.");
        }
        // É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("Error while writing to the file " + 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("File " + path + " not found.");
        }
        table.records.clear();
        std::string line;
        bool firstLine = true;
        while (std::getline(file, line)) {
            if (firstLine) {
                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&) {
                std::cerr << "Error converting the ID for the key " << key << std::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();

        // Mise à jour du TUID avec l'ID maximum observé.
        int maxID = 0;
        for (const auto &entry : table.records) {
            maxID = std::max(maxID, entry.second.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) {
            std::cerr << "Error while reading the directory: " << e.what() << std::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("Main Menu", 80, 24);
                mainMenu.addOption(MenuOption("Manage the database", [this]() { runDatabaseMenu(); }));
                mainMenu.addOption(MenuOption("Quit", []() {
                    ConsoleColor::println("Goodbye!", ConsoleColor::TextColor::RED);
                    std::exit(EXIT_SUCCESS);
                }));
                runMenu(mainMenu);
                exitFlag = true;
            } catch (const std::runtime_error &e) {
                if (std::string(e.what()) != "Back")
                    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.displayMenu(userInterface_.get());
        char input = userInterface_->getInput();

        // Gestion des touches spéciales pour la navigation
        if (input == -32) { // Touche spéciale
            char direction = userInterface_->getInput();
            if (direction == 72) { // Flèche haut
                // Logique pour naviguer vers le haut
                // Vous pouvez ajouter une méthode dans Menu pour gérer cela
                menu.navigateUp();
                continue;
            } else if (direction == 80) { // Flèche bas
                // Logique pour naviguer vers le bas
                // Vous pouvez ajouter une méthode dans Menu pour gérer cela
                menu.navigateDown();
                continue;
            }
        }

        if (input == 27) { // Échap
            break;
        }

        try {
            menu.navigate(input);
        } catch (const std::runtime_error &e) {
            if (std::string(e.what()) == "Retour") {
                return;
            } else {
                ConsoleColor::println("Error: " + std::string(e.what()), ConsoleColor::TextColor::RED);
                std::cout << "Press any key to try again...";
                userInterface_->getInput(); // Attendre que l'utilisateur appuie sur une touche
            }
        }
    }
}


    void runDatabaseMenu() {
        Database db;
        while (true) {
            clearScreen();
            Menu dbMenu("Database Menu", 80, 24);
            dbMenu.addOption(MenuOption("Create a new table", [&db]() {
                try {
                    auto newTableOpt = db.createTable();
                    if (newTableOpt)
                        db.recordMenu(db.selectTable() ? *db.selectTable() : newTableOpt.value());
                } catch (const std::exception &e) {
                    std::cerr << e.what() << std::endl;
                    system("pause");
                }
            }));
            dbMenu.addOption(MenuOption("Use an existing table", [&db]() {
                Table* table = db.selectTable();
                if (table)
                    db.recordMenu(*table);
            }));
            dbMenu.addOption(MenuOption("Back", []() {}));
            try {
                runMenu(dbMenu);
                break;
            } catch (const std::runtime_error &e) {
                if (std::string(e.what()) != "Back")
                    throw;
                else
                    break;
            }
        }
    }
};

// -----------------------------------------------------------------------------
// Fonction main
// -----------------------------------------------------------------------------
int main() {
    try {
        auto ui = std::make_unique<ConsoleUserInterface>();
        MenuManager manager(std::move(ui));
        manager.run();

        // Exemple d'utilisation directe de la base de données pour tester TUID et Sketch
        Database db;
        auto tableOpt = db.createTable();
        if (tableOpt) {
            Table* table = db.selectTable();
            if (table) {
                db.addRecord(*table);
                db.displayRecords(*table);

                TUID generator;
                int nextID = generator.getNextID();
                std::cout << "\nNext generated ID : " << nextID << "\n";
                auto indices = generator.querySketch(nextID, 3);
                std::cout << "Indices returned by querySketch : ";
                for (const auto idx : indices) {
                    std::cout << idx << " ";
                }
                std::cout << "\nPress any key to close...";
                (void)getchar();
            }
        }
    } catch (const std::exception &ex) {
        std::cerr << "Exception caught in main : " << ex.what() << "\n";
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
 
Joined
Sep 20, 2022
Messages
231
Reaction score
38
Feedback:

When office workers need a simple database, they use a spread sheet, ignore all the calculation features, and just use it like a word processor. Horrifying.

That's just my opinion, I base that on a very small sample of office workers.

I've never found simple database programs like this useful, on the other hand, I made a csv text editor that let me move columns around, and that was very useful for editing table files.

I used those table routines for everything; editing data files, viewing program output, prompting for screen input, directory browser, and a program editor.
 
Last edited:
Joined
Jun 12, 2020
Messages
17
Reaction score
0
I understand the difficulty of proposing something useful and effective for those who work in offices. Indeed, I also coded the database with a mini web server, but lacking confidence, I preferred to share the console application. As for spreadsheets, many, I think, are still stuck in the technological shock that spreadsheets represented in the 90s... and believe they can do everything with them. However, the algorithms of databases are not very effective in Excel formulas; perhaps it would be better to write the table join (for example) and file management codes in macros (VBA). I'm not entirely convinced of the effectiveness in the end.
 
Joined
Jun 12, 2020
Messages
17
Reaction score
0
I would like to thank you for the positive feedback and clarify that using the mini web server comes with security risks that not all users will be able to manage in addition to everything else. Thank you again.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
474,266
Messages
2,571,075
Members
48,772
Latest member
Backspace Studios

Latest Threads

Top