add stat by date

This commit is contained in:
Philipp Hofer
2025-10-17 09:57:12 +02:00
parent 1dcf8207aa
commit 073a6cca63
6 changed files with 122 additions and 20 deletions

View File

@@ -1,7 +1,7 @@
use crate::{Account, Transaction, TransactionDetails}; use crate::{Account, Transaction, TransactionDetails};
use chrono::{Datelike, NaiveDate}; use chrono::{Datelike, NaiveDate};
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Category { pub enum Category {
Zero815, Zero815,
Book, Book,
@@ -40,7 +40,7 @@ pub enum Category {
Hardware(Hardware), Hardware(Hardware),
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Hardware { pub enum Hardware {
Laptop, Laptop,
Remarkable, Remarkable,
@@ -53,9 +53,9 @@ impl From<Hardware> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Travel { pub enum Travel {
eSIM, Esim,
} }
impl From<Travel> for Category { impl From<Travel> for Category {
@@ -64,7 +64,7 @@ impl From<Travel> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Tech { pub enum Tech {
LLM, LLM,
MediaMarkt, MediaMarkt,
@@ -80,7 +80,7 @@ impl From<Tech> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Restaurant { pub enum Restaurant {
Proper, Proper,
FastFood, FastFood,
@@ -94,7 +94,7 @@ impl From<Restaurant> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Sport { pub enum Sport {
Squash, Squash,
SportOkay, SportOkay,
@@ -108,7 +108,7 @@ impl From<Sport> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Groceries { pub enum Groceries {
Hofer, Hofer,
Lidl, Lidl,
@@ -125,7 +125,7 @@ impl From<Groceries> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Transport { pub enum Transport {
LinzLinien, LinzLinien,
Nextbike, Nextbike,
@@ -141,7 +141,7 @@ impl From<Transport> for Category {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Donation { pub enum Donation {
RemarkableRCU, RemarkableRCU,
Signal, Signal,
@@ -377,7 +377,7 @@ pub fn cat(details: &TransactionDetails) -> Category {
return Category::Household("Schuhe".into()); return Category::Household("Schuhe".into());
} }
if details.desc == "AIRALO" { if details.desc == "AIRALO" {
return Travel::eSIM.into(); return Travel::Esim.into();
} }
if details.desc == "Autobahnrasthaus Chiem" || details.desc == "Cafe+Co AT 311530" { if details.desc == "Autobahnrasthaus Chiem" || details.desc == "Cafe+Co AT 311530" {
return Category::CoffeeToGo; return Category::CoffeeToGo;

View File

@@ -1,7 +1,10 @@
pub mod classifier; pub mod classifier;
mod n26;
pub mod stat;
use crate::classifier::Category; use crate::classifier::Category;
use chrono::NaiveDate; use chrono::NaiveDate;
use std::fmt::Display;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Account { pub enum Account {
@@ -21,3 +24,22 @@ pub struct Transaction {
pub details: TransactionDetails, pub details: TransactionDetails,
pub cat: Category, pub cat: Category,
} }
pub struct Overview {
transactions: Vec<Transaction>,
}
impl Overview {
pub fn new() -> Self {
Self {
transactions: Vec::new(),
}
}
}
impl Display for Overview {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{} transactions", self.transactions.len()))?;
Ok(())
}
}

View File

@@ -1,5 +1,13 @@
mod n26; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn main() { fn main() {
n26::Transaction::from_file("./data/n26-2025-10-16.csv"); tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.init();
let mut overview = fin::Overview::new();
overview.n26("./data/n26-2025-10-16.csv");
overview.by_month();
println!("{overview}");
} }

View File

@@ -1,12 +1,20 @@
use crate::{Account, Overview, TransactionDetails};
use chrono::NaiveDate; use chrono::NaiveDate;
use csv::Reader; use csv::Reader;
use fin::{Account, TransactionDetails};
use serde::Deserialize; use serde::Deserialize;
use std::fs::File; use std::fs::File;
use tracing::info; use tracing::info;
impl Overview {
pub fn n26(&mut self, path: &str) {
let mut transactions = Transaction::from_file(path);
info!("Added {} transactions.", transactions.len());
self.transactions.append(&mut transactions);
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Transaction { struct Transaction {
#[serde(rename = "Booking Date")] #[serde(rename = "Booking Date")]
booking_date: String, booking_date: String,
#[serde(rename = "Value Date")] #[serde(rename = "Value Date")]
@@ -31,7 +39,7 @@ pub struct Transaction {
exchange_rate: Option<f64>, exchange_rate: Option<f64>,
} }
impl From<Transaction> for fin::Transaction { impl From<Transaction> for crate::Transaction {
fn from(value: Transaction) -> Self { fn from(value: Transaction) -> Self {
let date = if let Some(date) = value.value_date { let date = if let Some(date) = value.value_date {
// If value_date exist, use this as it's earlier and closer to real date... // If value_date exist, use this as it's earlier and closer to real date...
@@ -42,6 +50,7 @@ impl From<Transaction> for fin::Transaction {
}; };
let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").unwrap(); let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").unwrap();
let amount_in_cent = (value.amount_eur * 100.).round() as i64; let amount_in_cent = (value.amount_eur * 100.).round() as i64;
let amount_in_cent = amount_in_cent.abs();
let desc = if let Some(desc) = value.partner_name { let desc = if let Some(desc) = value.partner_name {
desc.clone() desc.clone()
} else { } else {
@@ -60,16 +69,14 @@ impl From<Transaction> for fin::Transaction {
} }
impl Transaction { impl Transaction {
pub fn from_file(path: &str) -> Vec<fin::Transaction> { fn from_file(path: &str) -> Vec<crate::Transaction> {
info!("Parsing {path}..."); info!("Parsing {path}...");
let mut ret = Vec::new(); let mut ret = Vec::new();
let file = File::open(path).unwrap(); let file = File::open(path).unwrap();
let mut rdr = Reader::from_reader(file); let mut rdr = Reader::from_reader(file);
for (idx, result) in rdr.deserialize::<Transaction>().enumerate() { for result in rdr.deserialize::<Transaction>() {
println!("{idx}");
let tx: Transaction = result.unwrap(); let tx: Transaction = result.unwrap();
println!("{tx:#?}");
ret.push(tx.into()); ret.push(tx.into());
} }

64
src/stat/date.rs Normal file
View File

@@ -0,0 +1,64 @@
use crate::{classifier::Category, Overview, Transaction};
use chrono::Datelike;
use std::collections::HashMap;
impl Overview {
pub fn by_month(&self) {
let min_date = self.get_first_transaction().details.date;
let max_date = self.get_last_transaction().details.date;
let mut min_date = min_date.with_day(1).unwrap();
while min_date < max_date {
let month = min_date.format("%Y-%m").to_string();
print!("{month}: ");
let txs = self.get_expenses_for_month(&month);
print!("{} txs, ", txs.len());
let sum: i64 = txs.iter().map(|t| t.details.amount_in_cent).sum::<i64>() / 100;
println!("{sum}");
for (cat, amount) in self.get_top_5_cats_for_month(&month) {
println!(" - {cat:?} ({})", amount / 100);
}
println!();
min_date = min_date + chrono::Months::new(1);
}
}
fn get_top_5_cats_for_month(&self, month: &str) -> Vec<(Category, i64)> {
let mut ret = HashMap::new();
let txs = self.get_expenses_for_month(month);
for tx in txs {
if let Some(c) = ret.get_mut(&tx.cat) {
*c += tx.details.amount_in_cent;
} else {
ret.insert(tx.cat.clone(), tx.details.amount_in_cent);
}
}
let mut ret = ret.into_iter().collect::<Vec<(Category, i64)>>();
ret.sort_by_key(|(_, amount)| *amount);
ret.into_iter().rev().take(5).collect()
}
fn get_expenses_for_month(&self, month: &str) -> Vec<&Transaction> {
self.transactions
.iter()
.filter(|t| {
t.details.date.format("%Y-%m").to_string() == month && t.cat != Category::Transfer
})
.collect()
}
fn get_first_transaction(&self) -> &Transaction {
self.transactions
.iter()
.min_by_key(|t| t.details.date)
.unwrap()
}
fn get_last_transaction(&self) -> &Transaction {
self.transactions
.iter()
.max_by_key(|t| t.details.date)
.unwrap()
}
}

1
src/stat/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod date;