Basic demonstrator
Works fine but can be slow, especially because of heavy IO.
This commit is contained in:
commit
8f768ff9e0
8 changed files with 414 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
cmake-*/
|
||||
**/.idea/
|
||||
|
20
CMakeLists.txt
Normal file
20
CMakeLists.txt
Normal file
|
@ -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)
|
153
LicenceOperations.cpp
Normal file
153
LicenceOperations.cpp
Normal file
|
@ -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<std::string>{}(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<std::string>{}(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;
|
||||
}
|
36
LicenceOperations.h
Normal file
36
LicenceOperations.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Created by trotfunky on 02/10/2021.
|
||||
//
|
||||
|
||||
#ifndef AUTOPOINTSPERMIS_LICENCEOPERATIONS_H
|
||||
#define AUTOPOINTSPERMIS_LICENCEOPERATIONS_H
|
||||
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
|
||||
#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
|
26
README.md
Normal file
26
README.md
Normal file
|
@ -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 ?
|
97
create.cpp
Normal file
97
create.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <iostream>
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
#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;
|
||||
}
|
23
db_info.h
Normal file
23
db_info.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Created by trotfunky on 02/10/2021.
|
||||
//
|
||||
|
||||
#ifndef AUTOPOINTSPERMIS_DB_INFO_H
|
||||
#define AUTOPOINTSPERMIS_DB_INFO_H
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
56
main.cpp
Normal file
56
main.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#include <iostream>
|
||||
#include <sqlite3.h>
|
||||
#include <ctime>
|
||||
|
||||
#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;
|
||||
}
|
Loading…
Add table
Reference in a new issue