commit 8f768ff9e06347afdf9fc023e84dcc0031d5779f Author: Teo-CD Date: Sun Oct 3 19:42:28 2021 +0100 Basic demonstrator Works fine but can be slow, especially because of heavy IO. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5a0462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cmake-*/ +**/.idea/ + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d9b54b5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.18) +project(AutoPointsPermis) + +set(CMAKE_CXX_STANDARD 20) + +set(CommonFiles + db_info.h + LicenceOperations.cpp + LicenceOperations.h) + +add_executable(AutoPointsPermis-CreateDb + create.cpp + ${CommonFiles}) + +add_executable(AutoPointsPermis + main.cpp + ${CommonFiles}) + +target_link_libraries(AutoPointsPermis sqlite3) +target_link_libraries(AutoPointsPermis-CreateDb sqlite3) \ No newline at end of file diff --git a/LicenceOperations.cpp b/LicenceOperations.cpp new file mode 100644 index 0000000..2490fa1 --- /dev/null +++ b/LicenceOperations.cpp @@ -0,0 +1,153 @@ +// +// Created by trotfunky on 02/10/2021. +// + +#include "LicenceOperations.h" + +void dbError(const std::string& errorMessage, char* dbError) { + std::cerr << errorMessage << " : " << std::endl << dbError << std::endl; + sqlite3_free(dbError); +} + +int addNewLicence(sqlite3* db, const std::string& licenceID) { + // Shift the hash two places down to fit properly in an SQLite integer + std::string idHash = std::to_string(std::hash{}(licenceID) >> 2); + char* errorMessage = nullptr; + int errorCode; + errorCode = sqlite3_exec(db, ("INSERT INTO " + pointsTable + " VALUES (" + idHash + + ", '" + licenceID + "', 12, 0, 0, 0, 0);").c_str(), + nullptr, nullptr, &errorMessage); + + if (errorCode) { + dbError("Error adding new licence with ID " + licenceID, errorMessage); + } + + return errorCode; +} + +int applyOffence(sqlite3* db, const std::string& licenceID, int lostPoints, offenceClass offenceClass) { + // Shift the hash two places down to fit properly in an SQLite integer + std::string idHash = std::to_string(std::hash{}(licenceID) >> 2); + char* errorMessage = nullptr; + int errorCode; + char** licenceInfo = nullptr; + + // Retrieve points and grave offence info, that's all we need. + errorCode = sqlite3_get_table(db, ("SELECT " + pointsColumn + "," + graveOffenceColumn + ", " + onePointCountdownColumn + + " FROM " + pointsTable + " WHERE " + keyColumn + " = " + idHash + ";").c_str(), + &licenceInfo, nullptr, nullptr, &errorMessage); + if (errorCode) { + dbError("Error getting info for licence " + licenceID, errorMessage); + return errorCode; + } + + int points = std::stoi(licenceInfo[3]); + bool hasGraveOffence = std::stoi(licenceInfo[4]); + // Can only change from false to true for offences of class 4 or 5 + bool newGraveOffence = offenceClass > offenceClass::Three && !hasGraveOffence; + + int newTenYearCountdown = -1; + // Only set for the first new offence when at maximum points, clears if class five offence + if (points == 12 && offenceClass < offenceClass::Five) { + newTenYearCountdown = 3653; + } else if (offenceClass == offenceClass::Five) { + newTenYearCountdown = 0; + } + + int newCountdown; + int onePointCountdown = std::stoi(licenceInfo[5]); + int newOnePointCountdown = -1; + // This minimizes affected rows for one point offences when it's the first offence at maximum points. + // Not sure if that's useful. + if (lostPoints == 1 && points == 12) { + newCountdown = 183; + } else { + newCountdown = (2 + (hasGraveOffence || newGraveOffence)) * 365; + if (lostPoints == 1) { + newOnePointCountdown = 183; + } else if (onePointCountdown) { + newOnePointCountdown = 0; + } + } + + points -= lostPoints; + // Licence is revoked, reset everything. + if (points <= 0) { + points = 0; + newCountdown = 0; + newOnePointCountdown = 0; + newTenYearCountdown = 0; + } + + errorCode = sqlite3_exec( + db, ("UPDATE " + pointsTable + " SET " + + pointsColumn + " = " + std::to_string(points) + ", " + + countdownColumn + " = " + std::to_string(newCountdown) + + (newOnePointCountdown >= 0 ? ", " + onePointCountdownColumn + " = " + std::to_string(newOnePointCountdown) : "") + + (newTenYearCountdown >= 0 ? ", " + tenYearsCountdownColumn + " = " + std::to_string(newTenYearCountdown) : "") + + (newGraveOffence ? ", " + graveOffenceColumn + " = 1" : "") + + " WHERE " + keyColumn + " = " + idHash + ";").c_str(), + nullptr, nullptr, &errorMessage); + + if (errorCode) { + dbError("Error updating licence " + licenceID, errorMessage); + } + + return errorCode; +} + +int countdown(void* db, int columnCount, char** rowData, char** columnNames) { + char* errorMessage = nullptr; + int errorCode; + + int points = std::stoi(rowData[1]); + int newPoints = -1; + int countdown = std::stoi(rowData[2]); + int onePointCoutdown = std::stoi(rowData[3]); + int tenYearCountdown = std::stoi(rowData[4]); + + if (tenYearCountdown == 1 || countdown == 1) { + // If resetting the counters, don't do anything else + return reset((sqlite3*)db, rowData[0]); + } else if (onePointCoutdown == 1) { + newPoints = points + 1; + } + + countdown -= 1; + onePointCoutdown -= 1; + tenYearCountdown -= 1; + + errorCode = sqlite3_exec( + (sqlite3*)db, ("UPDATE " + pointsTable + " SET " + + countdownColumn + " = " + std::to_string(countdown) + + (newPoints >= 0 ? ", " + pointsColumn + " = " + std::to_string(newPoints) : "") + + (onePointCoutdown >= 0 ? ", " + onePointCountdownColumn + " = " + std::to_string(onePointCoutdown) : "") + + (tenYearCountdown >= 0 ? ", " + tenYearsCountdownColumn + " = " + std::to_string(tenYearCountdown) : "") + + " WHERE " + keyColumn + " = " + rowData[0]).c_str(), + nullptr, nullptr, &errorMessage); + + if (errorCode) { + dbError("Error while updating coutdowns for licence hash " + std::string(rowData[0]), errorMessage); + } + + return errorCode; +} + +int reset(sqlite3* db, const std::string& idHash) { + char* errorMessage = nullptr; + int errorCode; + + errorCode = sqlite3_exec(db, ("UPDATE " + pointsTable + " SET " + + pointsColumn + " = 12, " + + countdownColumn + " = 0," + + onePointCountdownColumn + " = 0," + + tenYearsCountdownColumn + " = 0," + + " WHERE " + keyColumn + " = " + idHash).c_str(), + nullptr, nullptr, &errorMessage); + + if (errorCode) { + dbError("Error resetting licence " + idHash, errorMessage); + } + + return errorCode; +} diff --git a/LicenceOperations.h b/LicenceOperations.h new file mode 100644 index 0000000..a0c2710 --- /dev/null +++ b/LicenceOperations.h @@ -0,0 +1,36 @@ +// +// Created by trotfunky on 02/10/2021. +// + +#ifndef AUTOPOINTSPERMIS_LICENCEOPERATIONS_H +#define AUTOPOINTSPERMIS_LICENCEOPERATIONS_H + +#include +#include +#include +#include + +#include "db_info.h" + +// Assign values directly to prevent starting from 0 without using a dummy value +enum class offenceClass { + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5 +}; + +void dbError(const std::string& errorMessage, char* dbError); + +/// Adds a new licence with 12 points and no counters running. +int addNewLicence(sqlite3* db, const std::string& licenceID); +/// Apply a new offence to an existing licence. +int applyOffence(sqlite3* db, const std::string& licenceID, int lostPoints, offenceClass offenceClass); +/// Apply the countdowns, check for completion and add points back. +/// Fetched columns : idHash, Points, Countdown, OnePointCoutdown, TenYearCountdown +int countdown(void* db, int columnCount, char** rowData, char** columnNames); +/// Resets all counters and maximum points for an existing licence +int reset(sqlite3* db, const std::string& idHash); + +#endif //AUTOPOINTSPERMIS_LICENCEOPERATIONS_H diff --git a/README.md b/README.md new file mode 100644 index 0000000..02b8f8f --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# AutoPointsPermis + +A silly project recreating what would be needed to handle the basics +of French driver licence point handling + +# Usage + +This project builds two executables. One creates and fills the database +with licences and offences, the other is there to simulate daily checks +to see if points need to be awarded back. + +# Building +## Dependencies + + - Sqlite3 + +# Improvements + +Currently, everything uses very naive SQL querying, with the main optimisation +being batching. There is certainly smarter ways to go about it and some ways +to make it run faster. + +The daily program could be improved to be a server of sorts, allowing +the manipulation of the database in a controlled fashion. (Telnet ?) + +Maybe test with another DB ? diff --git a/create.cpp b/create.cpp new file mode 100644 index 0000000..e9cfd0a --- /dev/null +++ b/create.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include + +#include "db_info.h" +#include "LicenceOperations.h" + +constexpr int totalLicences = 50'000'000; +constexpr float offenceRate = 0.2; + +/// Generate a random string of numbers and letters +/// ASCII letters are contiguous so juste add a random offset to 'a'. +std::string createRandomID() { + char newID[IDLength]; + for (char& idChar : newID) { + if (rand() % 2) { + idChar = 'a' + rand() % 26; + } else { + idChar = '0' + rand() % 10; + } + } + return newID; +} + +int main() +{ + srand(time(nullptr)); + + sqlite3* db; + char* errorMessage = nullptr; + int errorCode; + + errorCode = sqlite3_open(dbName.c_str(), &db); +// errorCode = sqlite3_open("TestDB.sqlite3", &db); + if (errorCode) { + std::cerr << "Error opening database" << std::endl << sqlite3_errmsg(db) << std::endl; + sqlite3_close(db); + return -1; + } + + errorCode = sqlite3_exec(db, ("CREATE TABLE IF NOT EXISTS " + pointsTable + "(" + + keyColumn + " UNSIGNED BIG INT PRIMARY KEY NOT NULL," + + idColumn + " TEXT NOT NULL," + + pointsColumn + " INT NOT NULL," + + countdownColumn + " INT NOT NULL," + + onePointCountdownColumn + " INT NOT NULL," + + tenYearsCountdownColumn + " INT NOT NULL," + + graveOffenceColumn + " INT NOT NULL);").c_str(), + nullptr, nullptr, &errorMessage); + + if (errorCode) { + dbError("Error creating table " + pointsTable, errorMessage); + sqlite3_close(db); + return -1; + } + + // Batch all inits + errorCode = sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, &errorMessage); + if (errorCode) { + dbError("Error while beginning transaction", errorMessage); + sqlite3_close(db); + return -1; + } + + std::ios::sync_with_stdio(false); + std::time_t startTime = std::time(nullptr); + for (int i = 0; i < totalLicences; i++) { + const std::string& ID = createRandomID(); + errorCode = addNewLicence(db, ID); + if (rand() < RAND_MAX * offenceRate) { + applyOffence(db, ID, rand() % 6 + 1, (offenceClass)(rand() % 5 + 1)); + } + + if (!(i % 10000)) { + std::cout << "Created " << i << " licences" << std::endl; + } + + if (errorCode) { + break; + } + } + + errorCode = sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, &errorMessage); + if (errorCode) { + dbError("Error while closing transaction", errorMessage); + sqlite3_close(db); + return -1; + } + + std::cout << "Db created successfully !" << std::endl; + std::cout << totalLicences << " licences created in " << std::time(nullptr) - startTime << std::endl; + sqlite3_close(db); + return 0; +} diff --git a/db_info.h b/db_info.h new file mode 100644 index 0000000..5778c0b --- /dev/null +++ b/db_info.h @@ -0,0 +1,23 @@ +// +// Created by trotfunky on 02/10/2021. +// + +#ifndef AUTOPOINTSPERMIS_DB_INFO_H +#define AUTOPOINTSPERMIS_DB_INFO_H + +#include + +const std::string dbName = "Permis.sqlite3"; +const std::string pointsTable = "POINTS"; + +const std::string keyColumn = "LicenseHash"; +const std::string idColumn = "LicenseID"; +const std::string pointsColumn = "Points"; +const std::string countdownColumn = "Countdown"; +const std::string onePointCountdownColumn = "OneCountdown"; +const std::string tenYearsCountdownColumn = "TenYCountdown"; +const std::string graveOffenceColumn = "HasGrave"; + +constexpr int IDLength = 40; + +#endif //AUTOPOINTSPERMIS_DB_INFO_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..902ba10 --- /dev/null +++ b/main.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +#include "db_info.h" +#include "LicenceOperations.h" + +int main() +{ + char* errorMessage = nullptr; + int errorCode; + sqlite3* db = nullptr; + + if (sqlite3_open(dbName.c_str(), &db)) { + std::cerr << "Error opening database : " << std::endl << sqlite3_errmsg(db) << std::endl; + sqlite3_close(db); + return -1; + } + + std::time_t startTime = std::time(nullptr); + + errorCode = sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, &errorMessage); + if (errorCode) { + dbError("Error while beginning transaction", errorMessage); + sqlite3_close(db); + return -1; + } + + errorCode = sqlite3_exec(db, ("SELECT " + + keyColumn + ", " + + pointsColumn + ", " + + countdownColumn + ", " + + onePointCountdownColumn + ", " + + tenYearsCountdownColumn + + " FROM " + pointsTable + + " WHERE " + pointsColumn + " > 0 AND " + pointsColumn + " < 12;").c_str(), + countdown, db, &errorMessage); + if (errorCode) { + dbError("Error while running coutdowns", errorMessage); + sqlite3_close(db); + return -1; + } + + errorCode = sqlite3_exec(db, "END TRANSACTION;", nullptr, nullptr, &errorMessage); + if (errorCode) { + dbError("Error while closing transaction", errorMessage); + sqlite3_close(db); + return -1; + } + + + std::cout << "Successfully updated database in " << std::time(nullptr) - startTime << "s" << std::endl; + + sqlite3_close(db); + return 0; +}