summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-06-28 08:41:40 -0600
committermo khan <mo@mokhan.ca>2025-06-28 08:41:40 -0600
commitbc3394c4a75059556ca002a9839bb24d7fbb5bbc (patch)
tree32524cca19fb0aa3d2fee77234b4be2060f6171f
parent71c0b703624f50eb1ab4489b1fe0a38d85ee4557 (diff)
fix: handle NaN/infinity values in TUI to prevent formatting crashes
- Fix SQL SUM() returning NULL converted to NaN by using Option<f64> - Add is_finite() checks before all float formatting operations - Filter out non-finite values from category sorting - Ensure safe fallbacks for all financial calculations - Fix test fixtures to include transaction ID field - Remove unused import warning This fixes the Tab key crash caused by formatting NaN/infinity values when switching between views in the TUI. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--src/db.rs26
-rw-r--r--src/tui/app.rs12
-rw-r--r--src/tui/dashboard.rs15
-rw-r--r--src/tui/tests.rs1
-rw-r--r--src/tui/ui.rs5
5 files changed, 47 insertions, 12 deletions
diff --git a/src/db.rs b/src/db.rs
index c781f71..a84534b 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -215,28 +215,41 @@ pub fn get_income_vs_expenses_filtered(
(Some(from), Some(to)) => {
let mut stmt = conn.prepare("SELECT SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as income, SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END) as expenses FROM transactions WHERE date >= ? AND date <= ?")?;
let row = stmt.query_row(params![from, to], |row| {
- Ok((row.get::<_, f64>(0)?, row.get::<_, f64>(1)?))
+ Ok((
+ row.get::<_, Option<f64>>(0)?.unwrap_or(0.0),
+ row.get::<_, Option<f64>>(1)?.unwrap_or(0.0),
+ ))
})?;
Ok(row)
}
(Some(from), None) => {
let mut stmt = conn.prepare("SELECT SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as income, SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END) as expenses FROM transactions WHERE date >= ?")?;
let row = stmt.query_row(params![from], |row| {
- Ok((row.get::<_, f64>(0)?, row.get::<_, f64>(1)?))
+ Ok((
+ row.get::<_, Option<f64>>(0)?.unwrap_or(0.0),
+ row.get::<_, Option<f64>>(1)?.unwrap_or(0.0),
+ ))
})?;
Ok(row)
}
(None, Some(to)) => {
let mut stmt = conn.prepare("SELECT SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as income, SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END) as expenses FROM transactions WHERE date <= ?")?;
let row = stmt.query_row(params![to], |row| {
- Ok((row.get::<_, f64>(0)?, row.get::<_, f64>(1)?))
+ Ok((
+ row.get::<_, Option<f64>>(0)?.unwrap_or(0.0),
+ row.get::<_, Option<f64>>(1)?.unwrap_or(0.0),
+ ))
})?;
Ok(row)
}
(None, None) => {
let mut stmt = conn.prepare("SELECT SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as income, SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END) as expenses FROM transactions")?;
- let row =
- stmt.query_row([], |row| Ok((row.get::<_, f64>(0)?, row.get::<_, f64>(1)?)))?;
+ let row = stmt.query_row([], |row| {
+ Ok((
+ row.get::<_, Option<f64>>(0)?.unwrap_or(0.0),
+ row.get::<_, Option<f64>>(1)?.unwrap_or(0.0),
+ ))
+ })?;
Ok(row)
}
}
@@ -676,6 +689,7 @@ mod tests {
fn create_test_transactions() -> Vec<Transaction> {
vec![
Transaction {
+ id: 0,
date: NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
description: "WAL-MART #3011 CALGARY, AB".to_string(),
amount: -27.26,
@@ -683,6 +697,7 @@ mod tests {
category: Some("Groceries".to_string()),
},
Transaction {
+ id: 0,
date: NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
description: "SQ *EARLS DALHOUSIE Calgary, AB".to_string(),
amount: -10.89,
@@ -690,6 +705,7 @@ mod tests {
category: Some("Dining".to_string()),
},
Transaction {
+ id: 0,
date: NaiveDate::from_ymd_opt(2024, 6, 14).unwrap(),
description: "PAYROLL DEPOSIT Gitlab Canada".to_string(),
amount: 5578.40,
diff --git a/src/tui/app.rs b/src/tui/app.rs
index 2558ea3..5ef6123 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -115,6 +115,12 @@ impl App {
};
app.refresh_data().await?;
+
+ // Initialize list state after loading data
+ if !app.transactions.is_empty() {
+ app.transaction_list_state.select(Some(0));
+ }
+
Ok(app)
}
@@ -162,7 +168,10 @@ impl App {
let event_handler = EventHandler::new(250);
loop {
- terminal.draw(|f| ui::draw(f, self))?;
+ if let Err(e) = terminal.draw(|f| ui::draw(f, self)) {
+ eprintln!("Drawing error: {}", e);
+ return Err(e.into());
+ }
match event_handler.next()? {
Event::Key(key_event) => {
@@ -252,6 +261,7 @@ impl App {
fn move_selection_down(&mut self) {
if self.current_view == View::Transactions
+ && !self.transactions.is_empty()
&& self.selected_transaction_index < self.transactions.len().saturating_sub(1) {
self.selected_transaction_index += 1;
self.transaction_list_state.select(Some(self.selected_transaction_index));
diff --git a/src/tui/dashboard.rs b/src/tui/dashboard.rs
index 3c4d979..cc72f53 100644
--- a/src/tui/dashboard.rs
+++ b/src/tui/dashboard.rs
@@ -140,8 +140,10 @@ fn draw_cash_flow(f: &mut Frame, app: &App, area: Rect) {
}
fn draw_top_categories(f: &mut Frame, app: &App, area: Rect) {
- let mut categories: Vec<(&String, &f64)> = app.spending_by_category.iter().collect();
- categories.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
+ let mut categories: Vec<(&String, &f64)> = app.spending_by_category.iter()
+ .filter(|(_, amount)| amount.is_finite())
+ .collect();
+ categories.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
let max_spending = categories.first().map(|(_, v)| **v).unwrap_or(0.0);
@@ -150,12 +152,17 @@ fn draw_top_categories(f: &mut Frame, app: &App, area: Rect) {
.take(5)
.map(|(category, amount)| {
let bar_width = 10;
- let ratio = if max_spending > 0.0 { *amount / max_spending } else { 0.0 };
+ let safe_amount = if amount.is_finite() { **amount } else { 0.0 };
+ let ratio = if max_spending > 0.0 && max_spending.is_finite() {
+ safe_amount / max_spending
+ } else {
+ 0.0
+ };
let filled = (ratio * bar_width as f64) as usize;
let bar = format!("{}{}", "â–ˆ".repeat(filled), "â–‘".repeat(bar_width - filled));
ListItem::new(Line::from(vec![
- Span::raw(format!("{:<12} ${:>6.0} ", category, amount)),
+ Span::raw(format!("{:<12} ${:>6.0} ", category, safe_amount)),
Span::styled(bar, Style::default().fg(Color::Yellow)),
]))
})
diff --git a/src/tui/tests.rs b/src/tui/tests.rs
index 13924d5..9d6f890 100644
--- a/src/tui/tests.rs
+++ b/src/tui/tests.rs
@@ -1,6 +1,5 @@
#[cfg(test)]
mod tests {
- use super::*;
#[test]
fn test_truncate_string_with_utf8() {
diff --git a/src/tui/ui.rs b/src/tui/ui.rs
index ceb66b4..4161c7d 100644
--- a/src/tui/ui.rs
+++ b/src/tui/ui.rs
@@ -132,7 +132,10 @@ fn draw_budgets(f: &mut Frame, app: &App, area: Rect) {
Line::from(vec![
Span::raw(format!("{:<15} ", category)),
Span::styled(bar, Style::default().fg(status_color)),
- Span::raw(format!(" ${:.0}/${:.0}", spent.max(0.0), budget.max(0.0))),
+ Span::raw(format!(" ${:.0}/${:.0}",
+ if spent.is_finite() { spent.max(0.0) } else { 0.0 },
+ if budget.is_finite() { budget.max(0.0) } else { 0.0 }
+ )),
])
})
.collect();