diff options
| author | mo khan <mo@mokhan.ca> | 2025-06-28 08:41:40 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-06-28 08:41:40 -0600 |
| commit | bc3394c4a75059556ca002a9839bb24d7fbb5bbc (patch) | |
| tree | 32524cca19fb0aa3d2fee77234b4be2060f6171f | |
| parent | 71c0b703624f50eb1ab4489b1fe0a38d85ee4557 (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.rs | 26 | ||||
| -rw-r--r-- | src/tui/app.rs | 12 | ||||
| -rw-r--r-- | src/tui/dashboard.rs | 15 | ||||
| -rw-r--r-- | src/tui/tests.rs | 1 | ||||
| -rw-r--r-- | src/tui/ui.rs | 5 |
5 files changed, 47 insertions, 12 deletions
@@ -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(); |
