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

View File

@@ -1,7 +1,10 @@
pub mod classifier;
mod n26;
pub mod stat;
use crate::classifier::Category;
use chrono::NaiveDate;
use std::fmt::Display;
#[derive(Debug, PartialEq)]
pub enum Account {
@@ -21,3 +24,22 @@ pub struct Transaction {
pub details: TransactionDetails,
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() {
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 csv::Reader;
use fin::{Account, TransactionDetails};
use serde::Deserialize;
use std::fs::File;
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)]
pub struct Transaction {
struct Transaction {
#[serde(rename = "Booking Date")]
booking_date: String,
#[serde(rename = "Value Date")]
@@ -31,7 +39,7 @@ pub struct Transaction {
exchange_rate: Option<f64>,
}
impl From<Transaction> for fin::Transaction {
impl From<Transaction> for crate::Transaction {
fn from(value: Transaction) -> Self {
let date = if let Some(date) = value.value_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 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 {
desc.clone()
} else {
@@ -60,16 +69,14 @@ impl From<Transaction> for fin::Transaction {
}
impl Transaction {
pub fn from_file(path: &str) -> Vec<fin::Transaction> {
fn from_file(path: &str) -> Vec<crate::Transaction> {
info!("Parsing {path}...");
let mut ret = Vec::new();
let file = File::open(path).unwrap();
let mut rdr = Reader::from_reader(file);
for (idx, result) in rdr.deserialize::<Transaction>().enumerate() {
println!("{idx}");
for result in rdr.deserialize::<Transaction>() {
let tx: Transaction = result.unwrap();
println!("{tx:#?}");
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;