Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 125 additions & 114 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ resolver = "2"
[workspace.dependencies]
anyhow = "1.0.104"
chrono = "0.4.45"
diesel = { version = "2.3.13", default-features = false, features = [
diesel = { version = "2.3.14", default-features = false, features = [
"chrono",
"returning_clauses_for_sqlite_3_35",
"sqlite",
] }
rex-app = "0.3.0"
rex-db = "0.3.0"
rex-shared = "0.3.0"
rex-app = "0.3.2"
rex-db = "0.3.2"
rex-shared = "0.3.2"
strum = "0.28.0"
strum_macros = "0.28"

Expand Down
2 changes: 1 addition & 1 deletion app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rex-app"
version = "0.3.1"
version = "0.3.2"
edition = "2024"
description = "Core functionality for Rex"
homepage = "https://github.com/TheRustyPickle/Rex"
Expand Down
9 changes: 0 additions & 9 deletions app/src/views/summary_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ pub(crate) struct PeakMonthlyMovement {
pub(crate) amount: Cent,
}

impl PeakMonthlyMovement {
pub(crate) fn new(date: NaiveDate) -> Self {
Self {
date,
amount: Cent::new(0),
}
}
}

#[derive(Debug)]
pub(crate) struct SummaryNet {
pub(crate) total_income: Dollar,
Expand Down
95 changes: 50 additions & 45 deletions app/src/views/summary_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use chrono::{Datelike, NaiveDate};
use rex_db::ConnCache;
use rex_db::models::{FetchNature, FullTx, TxType};
use rex_shared::models::{Cent, Dollar};
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};

use crate::utils::{compare_change, compare_change_opt, get_percentages, month_year_to_unique};
use crate::views::{
Expand Down Expand Up @@ -206,23 +206,29 @@ impl SummaryView {
let mut income_tags = HashMap::new();
let mut expense_tags = HashMap::new();

// Only the primary tag. In case in previous month tag x was secondary but in the current
// month x is primary tag, the amount will become inaccurate and secondary tag becomes
// counted. So do not count anything except the primary tag
for tx in &self.txs {
for tag in &tx.tags {
match tx.tx_type {
TxType::Income => {
let value = income_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
*value += tx.amount;
}
TxType::Expense => {
let value = expense_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
*value += tx.amount;
}
TxType::Transfer
| TxType::Borrow
| TxType::Lend
| TxType::BorrowRepay
| TxType::LendRepay => {}
// First tag is always the primary tag
let Some(tag) = tx.tags.first() else {
continue;
};

match tx.tx_type {
TxType::Income => {
let value = income_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
*value += tx.amount;
}
TxType::Expense => {
let value = expense_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
*value += tx.amount;
}
TxType::Transfer
| TxType::Borrow
| TxType::Lend
| TxType::BorrowRepay
| TxType::LendRepay => {}
}
}

Expand Down Expand Up @@ -258,20 +264,13 @@ impl SummaryView {

let mut ongoing_month = 0;

let mut ongoing_date = NaiveDate::default();

let mut peak_earning = PeakMonthlyMovement::default();
let mut peak_expense = PeakMonthlyMovement::default();

let mut last_peak_earning = PeakMonthlyMovement::default();
let mut last_peak_expense = PeakMonthlyMovement::default();
// Income and expense totals per month, keyed by year-month
let mut monthly_totals: BTreeMap<i32, (NaiveDate, Cent, Cent)> = BTreeMap::new();

let mut outstanding_borrows = Cent::new(0);
let mut outstanding_lends = Cent::new(0);

for tx in &self.txs {
ongoing_date = tx.date.date();

let time_unique = month_year_to_unique(tx.date.month() as i32, tx.date.year());

if ongoing_month == 0 {
Expand All @@ -281,18 +280,15 @@ impl SummaryView {
if time_unique != ongoing_month {
ongoing_month = time_unique;
total_month_checked += 1;

if last_peak_earning.amount > peak_earning.amount {
peak_earning = last_peak_earning;
last_peak_earning = PeakMonthlyMovement::new(tx.date.date());
}

if last_peak_expense.amount > peak_expense.amount {
peak_expense = last_peak_expense;
last_peak_expense = PeakMonthlyMovement::new(tx.date.date());
}
}

let month_start = tx.date.date().with_day(1).unwrap();
let month_total = monthly_totals.entry(time_unique).or_insert((
month_start,
Cent::new(0),
Cent::new(0),
));

match tx.tx_type {
TxType::Income => {
total_income += tx.amount;
Expand All @@ -308,7 +304,7 @@ impl SummaryView {
biggest_earning.method.clone_from(&tx.from_method.name);
}

last_peak_earning.amount += tx.amount;
month_total.1 += tx.amount;
}
TxType::Expense => {
total_expense += tx.amount;
Expand All @@ -324,7 +320,7 @@ impl SummaryView {
biggest_expense.method.clone_from(&tx.from_method.name);
}

last_peak_expense.amount += tx.amount;
month_total.2 += tx.amount;
}
TxType::Borrow => {
outstanding_borrows += tx.amount;
Expand All @@ -344,14 +340,23 @@ impl SummaryView {

total_month_checked += 1;

if last_peak_earning.amount > peak_earning.amount {
peak_earning = last_peak_earning;
peak_earning.date = ongoing_date;
}
let mut peak_earning = PeakMonthlyMovement::default();
let mut peak_expense = PeakMonthlyMovement::default();

for (date, earning, expense) in monthly_totals.values() {
if *earning > peak_earning.amount {
peak_earning = PeakMonthlyMovement {
date: *date,
amount: *earning,
};
}

if last_peak_expense.amount > peak_expense.amount {
peak_expense = last_peak_expense;
peak_expense.date = ongoing_date;
if *expense > peak_expense.amount {
peak_expense = PeakMonthlyMovement {
date: *date,
amount: *expense,
};
}
}

let (income_percentage, expense_percentage) =
Expand All @@ -363,7 +368,7 @@ impl SummaryView {
Some(total_income.dollar() / f64::from(total_month_checked))
};

let mut average_expense = if total_income == 0 {
let mut average_expense = if total_expense == 0 {
Some(Dollar::new(0.0))
} else {
Some(total_expense.dollar() / f64::from(total_month_checked))
Expand Down
94 changes: 94 additions & 0 deletions app/tests/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,44 @@ fn summary_peak_across_months() {
fs::remove_file(file_name).unwrap();
}

#[test]
fn summary_peak_does_not_accumulate_across_months() {
let file_name = "test_summary_peak_no_accumulate.sqlite";
let mut db_conn = create_test_db(file_name);

// Jan is the true peak. Feb and Mar are each lower, but sum to more than Jan
for (date, income, expense) in [
("2024-01-10", "1000.00", "800.00"),
("2024-02-10", "600.00", "500.00"),
("2024-03-10", "600.00", "500.00"),
] {
add_tx(&mut db_conn, date, "I", "Cash", "", income, "Income", "A");
add_tx(&mut db_conn, date, "E", "Cash", "", expense, "Expense", "B");
}

let summary_view = db_conn
.get_summary_with_str("January", "2024", FetchNature::Yearly)
.unwrap();
let full = summary_view.generate_summary(None, &db_conn);

let peak = full.peak_array();
assert_eq!(peak[0][1], "01-2024");
assert_eq!(peak[0][2], "1000.00");
assert_eq!(peak[1][1], "01-2024");
assert_eq!(peak[1][2], "800.00");

// Same for all time
let summary_view = db_conn
.get_summary_with_str("January", "2024", FetchNature::All)
.unwrap();
let peak = summary_view.generate_summary(None, &db_conn).peak_array();
assert_eq!(peak[0][1], "01-2024");
assert_eq!(peak[0][2], "1000.00");

drop(db_conn);
fs::remove_file(file_name).unwrap();
}

#[test]
fn summary_lend_borrows() {
let file_name = "test_summary_lend_borrow.sqlite";
Expand Down Expand Up @@ -468,6 +506,62 @@ fn summary_tags_array_with_compare() {
fs::remove_file(file_name).unwrap();
}

#[test]
fn summary_tags_compare_counts_primary_tag_only_on_both_sides() {
let file_name = "test_summary_tags_compare_primary.sqlite";
let mut db_conn = create_test_db(file_name);

// August: Work primary on one tx, secondary on another
add_tx(
&mut db_conn,
"2024-08-01",
"A",
"Cash",
"",
"500.00",
"Income",
"Work",
);
add_tx(
&mut db_conn,
"2024-08-02",
"B",
"Cash",
"",
"300.00",
"Income",
"Side, Work",
);
// September: the same Work income as the primary tag in August
add_tx(
&mut db_conn,
"2024-09-01",
"C",
"Cash",
"",
"500.00",
"Income",
"Work",
);

let sep = db_conn
.get_summary_with_str("September", "2024", FetchNature::Monthly)
.unwrap();
let aug = db_conn
.get_summary_with_str("August", "2024", FetchNature::Monthly)
.unwrap();

let tags = sep.tags_array(Some(&aug), &db_conn);
let work_row = tags.iter().find(|r| r[0] == "Work").unwrap();

assert_eq!(work_row[1], "500.00");
// 500 against 500, not against the 800 of every tx that merely mentions Work
assert_eq!(work_row[5], "↑0.00");

drop(db_conn);
fs::remove_file(file_name).unwrap();
}

#[test]
fn summary_with_last_summary_mom_comparison() {
let file_name = "test_summary_mom.sqlite";
Expand Down
2 changes: 1 addition & 1 deletion db/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rex-db"
version = "0.3.1"
version = "0.3.2"
edition = "2024"
description = "Rex database models using diesel"
homepage = "https://github.com/TheRustyPickle/Rex"
Expand Down
2 changes: 1 addition & 1 deletion shared/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rex-shared"
version = "0.3.1"
version = "0.3.2"
edition = "2024"
description = "Shared code for Rex"
homepage = "https://github.com/TheRustyPickle/Rex"
Expand Down
4 changes: 2 additions & 2 deletions tui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rex-tui"
version = "0.3.1"
version = "0.3.2"
edition = "2024"
description = "A TUI app for managing Incomes and Expenses."
readme = "../README.md"
Expand Down Expand Up @@ -31,7 +31,7 @@ rex-shared.workspace = true
rfd = "0.17.2"
semver = "1.0.28"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
serde_json = "1.0.154"
strum.workspace = true
strum_macros.workspace = true
thousands = "0.2.0"
Expand Down
Loading