add stat by date
This commit is contained in:
@@ -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;
|
||||
|
22
src/lib.rs
22
src/lib.rs
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
12
src/main.rs
12
src/main.rs
@@ -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}");
|
||||
}
|
||||
|
21
src/n26.rs
21
src/n26.rs
@@ -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
64
src/stat/date.rs
Normal 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
1
src/stat/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod date;
|
Reference in New Issue
Block a user