diff --git a/src/database.cpp b/src/database.cpp index cb7695a..83a0ce4 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -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); diff --git a/src/main.cpp b/src/main.cpp index a184c09..f07c14b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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() { diff --git a/src/models/account.h b/src/models/account.h index 295fecc..73e3b3f 100644 --- a/src/models/account.h +++ b/src/models/account.h @@ -7,11 +7,13 @@ #include #include +#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; }; } diff --git a/src/optHandlers/accountOperation.cpp b/src/optHandlers/accountOperation.cpp index 57aeed2..bdc1d67 100644 --- a/src/optHandlers/accountOperation.cpp +++ b/src/optHandlers/accountOperation.cpp @@ -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)) {} diff --git a/src/optHandlers/earnOperation.h b/src/optHandlers/earnOperation.h index 052566d..937e279 100644 --- a/src/optHandlers/earnOperation.h +++ b/src/optHandlers/earnOperation.h @@ -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); diff --git a/src/optHandlers/mainOptHandler.cpp b/src/optHandlers/mainOptHandler.cpp index 65b6693..0a38da3 100644 --- a/src/optHandlers/mainOptHandler.cpp +++ b/src/optHandlers/mainOptHandler.cpp @@ -8,6 +8,7 @@ #include "paymentOperation.h" #include "../exceptions/helpRequested.h" #include "../exceptions/badValue.h" +#include "../utilities.h" #include #include #include @@ -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; diff --git a/src/optHandlers/paymentOperation.h b/src/optHandlers/paymentOperation.h index 7630c33..5ea53f1 100644 --- a/src/optHandlers/paymentOperation.h +++ b/src/optHandlers/paymentOperation.h @@ -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); diff --git a/src/utilities.cpp b/src/utilities.cpp index 0d6c7b6..b7b73a0 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -3,6 +3,7 @@ // #include +#include #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); }); +} diff --git a/src/utilities.h b/src/utilities.h index 57c2e09..fcaaa37 100644 --- a/src/utilities.h +++ b/src/utilities.h @@ -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); };