Money is now handled in a money object to prevent floating point errors

This commit is contained in:
Quentin Snow 2023-02-05 21:30:54 -06:00
parent 0639383836
commit 6dc9131d96
9 changed files with 112 additions and 69 deletions

View File

@ -18,14 +18,16 @@ Budget::Models::Account Database::getAccount(const std::string &accountName, sql
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
Budget::Models::Account account(
sqlite3_column_int64(stmt, 0),
(char*)(sqlite3_column_text(stmt, 1)),
(sqlite3_column_type(stmt,2) == SQLITE_NULL) ? "" : (char*)(sqlite3_column_text(stmt, 2)),
(sqlite3_column_type(stmt, 3) == SQLITE_NULL) ? nullptr : new long double(sqlite3_column_double(stmt, 4))
);
long long id = sqlite3_column_int64(stmt, 0);
char* name = (char*)(sqlite3_column_text(stmt, 1));
char* description = (char*)sqlite3_column_text(stmt, 2);
if (sqlite3_column_type(stmt, 3) == SQLITE_NULL || sqlite3_column_type(stmt, 4) == SQLITE_NULL) {
sqlite3_finalize(stmt);
return {id, name, description, nullptr};
}
auto* money = new Budget::Models::Money(sqlite3_column_int64(stmt, 3), sqlite3_column_int64(stmt, 4));
sqlite3_finalize(stmt);
return account;
return {id, name, description, money};
} else if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
throw std::runtime_error("Account " + accountName + " does not exist");
@ -37,12 +39,12 @@ Budget::Models::Account Database::getAccount(const std::string &accountName, sql
void Database::deleteAccount(Budget::Models::Account *account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "DELETE FROM account WHERE name = ?", -1, &stmt, nullptr);
int rc = sqlite3_prepare_v2(db, "DELETE FROM account WHERE id = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed to delete accountName " + account->name);
sqlite3_bind_text(stmt, 1, account->name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 1, account->id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
@ -53,10 +55,10 @@ void Database::deleteAccount(Budget::Models::Account *account, sqlite3 *db) {
void Database::cacheAccountValue(Budget::Models::Account *account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "SELECT SUM(value) FROM ("
"SELECT value * -1 as value FROM payment WHERE accountId = ? "
int rc = sqlite3_prepare_v2(db, "SELECT SUM(dollars * 100 + cents) / 100 as nDollars, SUM(dollars * 100 + cents) % 100 as nCents FROM("
"SELECT SUM(dollars * 100 + cents) / 100 AS dollars, SUM(dollars * 100 + cents) % 100 AS cents FROM earning WHERE accountId = ? "
"UNION ALL "
"SELECT value FROM earning WHERE accountId = ?);", -1, &stmt, nullptr);
"SELECT SUM(dollars * 100 + cents) / 100 * -1 AS dollars, SUM(dollars * 100 + cents) % 100 * -1 AS cents FROM payment WHERE accountId = ?);", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing get cashedValue " + account->name);
@ -66,25 +68,31 @@ void Database::cacheAccountValue(Budget::Models::Account *account, sqlite3 *db)
rc = sqlite3_step(stmt);
double value;
if (rc == SQLITE_ROW)
value = sqlite3_column_double(stmt, 0);
else if (rc == SQLITE_DONE)
value = 0;
long long int dollars;
long long int cents;
if (rc == SQLITE_ROW) {
dollars = sqlite3_column_int64(stmt, 0);
cents = sqlite3_column_int64(stmt, 1);
}
else if (rc == SQLITE_DONE) {
dollars = 0;
cents = 0;
}
else
throw std::runtime_error("Failed get cashedValue " + account->name);
sqlite3_reset(stmt);
rc = sqlite3_prepare_v2(db, "UPDATE account SET cachedValue = ? WHERE id = ?", -1, &stmt, nullptr);
rc = sqlite3_prepare_v2(db, "UPDATE account SET cachedDollars = ?, cachedCents = ? WHERE id = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing set cashedValue " + account->name);
sqlite3_bind_double(stmt, 1, value);
sqlite3_bind_int64(stmt, 2, account->id);
sqlite3_bind_int64(stmt, 1, dollars);
sqlite3_bind_int64(stmt, 2, cents);
sqlite3_bind_int64(stmt, 3, account->id);
rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
account->cachedValue = new long double(value);
account->cachedValue = new Budget::Models::Money(dollars, cents);
return;
}
throw std::runtime_error("Failed to set cashedValue for " + std::to_string(account->id));
@ -130,18 +138,19 @@ long long int Database::createAccount(Budget::OptHandlers::CreateOperation::Flag
long long int Database::earn(Budget::Models::Account *account, Budget::OptHandlers::EarnOperation::Flags *flags, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "INSERT INTO earning (value, description, receipt, accountId, date) VALUES "
"(?, ?, ?, ?, ?);",
int rc = sqlite3_prepare_v2(db, "INSERT INTO earning (dollars, cents, description, receipt, accountId, date) VALUES "
"(?, ?, ?, ?, ?, ?);",
-1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing earn statement");
sqlite3_bind_double(stmt, 1, flags->value);
sqlite3_bind_text(stmt, 2, (flags->description.empty() ? nullptr : flags->description.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, (flags->receipt.empty() ? nullptr : flags->receipt.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 4, account->id);
sqlite3_bind_int64(stmt, 5, flags->date);
sqlite3_bind_int64(stmt, 1, flags->dollars);
sqlite3_bind_int64(stmt, 2, flags->cents);
sqlite3_bind_text(stmt, 3, (flags->description.empty() ? nullptr : flags->description.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, (flags->receipt.empty() ? nullptr : flags->receipt.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, account->id);
sqlite3_bind_int64(stmt, 6, flags->date);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
@ -155,18 +164,19 @@ long long int Database::earn(Budget::Models::Account *account, Budget::OptHandle
long long int Database::pay(Budget::Models::Account *account, Budget::OptHandlers::PaymentOperation::Flags *flags, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "INSERT INTO payment (value, description, receipt, accountId, date) VALUES "
"(?, ?, ?, ?, ?);",
int rc = sqlite3_prepare_v2(db, "INSERT INTO payment (dollars, cents, description, receipt, accountId, date) VALUES "
"(?, ?, ?, ?, ?, ?);",
-1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing pay statement");
sqlite3_bind_double(stmt, 1, flags->value);
sqlite3_bind_text(stmt, 2, (flags->description.empty() ? nullptr : flags->description.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, (flags->receipt.empty() ? nullptr : flags->receipt.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 4, account->id);
sqlite3_bind_int64(stmt, 5, flags->date);
sqlite3_bind_int64(stmt, 1, flags->dollars);
sqlite3_bind_int64(stmt, 2, flags->cents);
sqlite3_bind_text(stmt, 3, (flags->description.empty() ? nullptr : flags->description.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, (flags->receipt.empty() ? nullptr : flags->receipt.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, account->id);
sqlite3_bind_int64(stmt, 6, flags->date);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);

View File

@ -16,11 +16,11 @@
using namespace Budget;
const char* createTables = "CREATE TABLE IF NOT EXISTS accountName (id INTEGER CONSTRAINT account_pk PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, cachedValue DOUBLE);"
"CREATE UNIQUE INDEX IF NOT EXISTS account_name_uindex ON accountName (name);"
"CREATE TABLE IF NOT EXISTS earning (id INTEGER CONSTRAINT earning_pk PRIMARY KEY AUTOINCREMENT, dollars INT NOT NULL, cents INT NOT NULL, description TEXT, receipt TEXT, accountId INT NOT NULL REFERENCES accountName ON UPDATE CASCADE ON DELETE CASCADE, date INTEGER NOT NULL);"
const char* createTables = "CREATE TABLE IF NOT EXISTS account (id INTEGER CONSTRAINT account_pk PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, cachedValue DOUBLE);"
"CREATE UNIQUE INDEX IF NOT EXISTS account_name_uindex ON account (name);"
"CREATE TABLE IF NOT EXISTS earning (id INTEGER CONSTRAINT earning_pk PRIMARY KEY AUTOINCREMENT, dollars INT NOT NULL, cents INT NOT NULL, description TEXT, receipt TEXT, accountId INT NOT NULL REFERENCES account ON UPDATE CASCADE ON DELETE CASCADE, date INTEGER NOT NULL);"
"CREATE INDEX IF NOT EXISTS earning_date_index ON earning (date DESC);"
"CREATE TABLE IF NOT EXISTS payment (id INTEGER CONSTRAINT payment_pk PRIMARY KEY AUTOINCREMENT, dollars INT NOT NULL, cents INT NOT NULL, description TEXT, receipt TEXT, accountId INT NOT NULL REFERENCES accountName ON UPDATE CASCADE ON DELETE CASCADE, date INTEGER NOT NULL);"
"CREATE TABLE IF NOT EXISTS payment (id INTEGER CONSTRAINT payment_pk PRIMARY KEY AUTOINCREMENT, dollars INT NOT NULL, cents INT NOT NULL, description TEXT, receipt TEXT, accountId INT NOT NULL REFERENCES account ON UPDATE CASCADE ON DELETE CASCADE, date INTEGER NOT NULL);"
"CREATE INDEX IF NOT EXISTS payment_date_index ON payment (date DESC);";
void createRequiredFolders() {

View File

@ -7,11 +7,13 @@
#include <string>
#include <utility>
#include "money.h"
namespace Budget::Models {
class Account {
public:
Account(long long int id, std::string name, std::string description, long double* cachedValue)
: id(id), name(std::move(name)), description(std::move(description)), cachedValue(new long double(3.3)) {}
Account(long long int id, std::string name, std::string description, Money* cachedValue)
: id(id), name(std::move(name)), description(std::move(description)), cachedValue(cachedValue) {}
// ~Account() {
// delete cachedValue;
@ -20,7 +22,7 @@ namespace Budget::Models {
long long id;
std::string name;
std::string description;
long double* cachedValue;
Money* cachedValue;
};
}

View File

@ -15,22 +15,24 @@ using namespace Budget::OptHandlers;
void AccountOperation::commit() {
Budget::Models::Account account = Database::getAccount(accountName, db);
if (flags.del) {
if (flags.forceDel || Utilities::confirm("Are you sure you'd like to delete " + accountName)) {
Database::deleteAccount(&account, db);
return;
}
}
if (flags.value) {
if (account.cachedValue == nullptr)
if (account.cachedValue == nullptr) {
Database::cacheAccountValue(&account, db);
}
std::cout << *account.cachedValue << std::endl;
}
if (!flags.description.empty()) {
Database::accountDescription(&account, db);
}
if (flags.del) {
if (flags.forceDel || Utilities::confirm("Are you sure you'd like to delete " + accountName)) {
Database::deleteAccount(&account, db);
return;
}
}
}
AccountOperation::AccountOperation(sqlite3 *db, std::string account) : Operation(db), accountName(std::move(account)) {}

View File

@ -17,7 +17,8 @@ namespace Budget::OptHandlers {
explicit EarnOperation(sqlite3 *db, std::string account);
struct Flags : public Operation::Flags {
long double value;
long long dollars;
long long cents;
std::string description;
std::string receipt;
long long date = std::time(nullptr);

View File

@ -8,6 +8,7 @@
#include "paymentOperation.h"
#include "../exceptions/helpRequested.h"
#include "../exceptions/badValue.h"
#include "../utilities.h"
#include <iostream>
#include <getopt.h>
#include <cstring>
@ -182,16 +183,10 @@ void MainOptHandler::earnOptHandler(std::string account) {
optind--;
operations.push(std::move(earnOperation));
return;
case 'v': {
try {
earnOperation->flags.value = std::stod(optarg);
} catch (std::exception const &e) {
help();
std::cout << "Bad value value" << std::endl;
throw Budget::Exceptions::BadValue("Bad value, cannot parse to decimal.");
}
case 'v':
earnOperation->flags.dollars = Utilities::extractDollars(optarg);
earnOperation->flags.cents = Utilities::extractCents(optarg);
break;
}
case 'd':
earnOperation->flags.description = optarg;
break;
@ -253,16 +248,10 @@ void MainOptHandler::paymentOptHandler(std::string account) {
optind--;
operations.push(std::move(payOperation));
return;
case 'v': {
try {
payOperation->flags.value = std::stod(optarg);
} catch (std::exception const &e) {
help();
std::cout << "Bad value value" << std::endl;
throw Budget::Exceptions::BadValue("Bad value, cannot parse to decimal.");
}
case 'v':
payOperation->flags.dollars = Utilities::extractDollars(optarg);
payOperation->flags.cents = Utilities::extractCents(optarg);
break;
}
case 'd':
payOperation->flags.description = optarg;
break;

View File

@ -18,7 +18,8 @@ namespace Budget::OptHandlers {
explicit PaymentOperation(sqlite3 *db, std::string account);
struct Flags : public Operation::Flags {
long double value;
long long int dollars;
long long int cents;
std::string description;
std::string receipt;
long long date = std::time(nullptr);

View File

@ -3,6 +3,7 @@
//
#include <iostream>
#include <algorithm>
#include "utilities.h"
bool Utilities::confirm(const std::string &question) {
@ -24,3 +25,34 @@ bool Utilities::confirm(const std::string &question) {
return false;
}
}
long long int Utilities::extractDollars(const std::string &string) {
std::size_t dotPos = string.find('.');
if (dotPos == std::string::npos) {
if (hasNumber(string))
return std::stoll(string);
return 0;
} else {
std::string subStr = string.substr(0, dotPos);
if (hasNumber(subStr))
return std::stoll(string.substr(0, dotPos));
return 0;
}
}
long long int Utilities::extractCents(const std::string &string) {
std::size_t dotPos = string.find('.');
std::string centsStr = string.substr(dotPos + 1);
if (centsStr.length() == 1) {
centsStr += "0";
} else if (centsStr.length() > 2) {
centsStr = centsStr.substr(0, 2);
}
if (hasNumber(centsStr))
return std::stoll(centsStr);
return 0;
}
bool Utilities::hasNumber(const std::string &string) {
return std::ranges::any_of(string, [](const char c) { return isdigit(c); });
}

View File

@ -18,6 +18,12 @@ public:
* @return True if the user confirms the action, false otherwise
*/
static bool confirm(const std::string &question);
static long long int extractDollars(const std::string& string);
static long long int extractCents(const std::string& string);
static bool hasNumber(const std::string &string);
};