Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
test.csv
budget.csv
assigned_transactions.csv
new.csv
new.csv
/cycles
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ edition = "2021"
[dependencies]
ansi-to-tui = "4.0.1"
anyhow = "1.0.94"
chrono = "0.4.38"
chrono = {version = "0.4.38", features = ["serde"]}
color-eyre = "0.6.3"
crossterm = "0.27.0"
csv = "1.1"
encoding = "0.2"
itertools = "0.14.0"
notify = "6.1.1"
piechart = "1.0.0"
ratatui = "0.26.2"
Expand Down
4 changes: 3 additions & 1 deletion src/consts.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub const ASSIGNED_TRANSACTIONS_FILE_NAME: &str = "assigned_transactions.csv";
pub const NEW_TRANSACTIONS_FILE_NAME: &str = "new.csv";
pub const BUDGET_FILE_NAME: &str = "budget.csv";
pub const CYCLES_FOLDER: &str = "./cycles";
pub const NUMBER_OF_DAYS_IN_CYCLE: f32 = 365.0 / 12.0;
pub const ONE_WEEK_OF_THE_CYCLE: f32 = 365.0 / 52.0;
3 changes: 2 additions & 1 deletion src/csv/models/assigned_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use chrono::NaiveDate;
use serde::Deserialize;

use super::comparable_transaction::ComparableTransaction;

#[derive(Debug, Deserialize)]
pub struct AssignedTransaction {
pub code: String,
pub date: String,
pub date: NaiveDate,
pub label: String,
pub amount: f32,
}
Expand Down
8 changes: 4 additions & 4 deletions src/csv/models/budget_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::Deserialize;

use super::{list_item::ListItem, BudgetItemType};

#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub struct BudgetItem {
pub label: String,
Expand All @@ -12,11 +12,11 @@ pub struct BudgetItem {
pub setting: BudgetItemType,
}

impl ListItem for BudgetItem {
impl ListItem<BudgetItem> for BudgetItem {
fn get_list_label(&self) -> ratatui::prelude::Text {
Text::raw(self.label.to_string())
}
fn get_savable_value(&self) -> Vec<String> {
vec![String::from(&self.code)]
fn get_savable_value(&self) -> BudgetItem {
self.clone()
}
}
31 changes: 31 additions & 0 deletions src/csv/models/cycle_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::{fs::File, path::PathBuf, str::FromStr};

use anyhow::Result;
use chrono::Utc;

use crate::csv::models::list_item::ListItem;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CycleFile {
pub path: PathBuf,
pub list_label: String,
}

impl ListItem<CycleFile> for CycleFile {
fn get_list_label(&self) -> ratatui::prelude::Text {
ratatui::text::Text::raw(self.list_label.clone())
}
fn get_savable_value(&self) -> CycleFile {
self.clone()
}
}

impl CycleFile {
pub fn create_new_file() -> Result<CycleFile> {
let now_string = Utc::now().format("%Y-%m-%d_%H-%M-%S");
let list_label = format!("./cycles/{now_string}.csv");
let path = PathBuf::from_str(list_label.as_str())?;
File::create(&path)?;
Ok(CycleFile { path, list_label })
}
}
13 changes: 0 additions & 13 deletions src/csv/models/deserializers.rs

This file was deleted.

6 changes: 3 additions & 3 deletions src/csv/models/list_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use std::fmt::Debug;

use ratatui::text::Text;

pub trait ListItem {
pub trait ListItem<T: PartialEq> {
fn get_list_label(&self) -> Text;
fn get_savable_value(&self) -> Vec<String>;
fn get_savable_value(&self) -> T;
}

impl Debug for dyn ListItem {
impl<T: PartialEq> Debug for dyn ListItem<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ListItem{{{}}}", self.get_list_label())
}
Expand Down
5 changes: 3 additions & 2 deletions src/csv/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ mod assigned_transaction;
mod budget_item;
mod budget_item_type;
mod comparable_transaction;
mod deserializers;
mod cycle_file;
pub mod list_item;
mod transaction;

pub use assigned_transaction::AssignedTransaction;
pub use budget_item::BudgetItem;
pub use budget_item_type::BudgetItemType;
pub use comparable_transaction::ComparableTransaction;
pub use cycle_file::CycleFile;
pub use transaction::Transaction;

#[derive(Debug)]
pub struct ParseResult {
pub transactions: Vec<Transaction>,
pub transactions_to_be_assigned: Vec<Transaction>,
pub balance: f32,
}
41 changes: 26 additions & 15 deletions src/csv/models/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
use std::str::FromStr;

use chrono::NaiveDate;
use itertools::Itertools;
use ratatui::text::Text;
use serde::Deserialize;
use serde::de::Error;
use serde::{Deserialize, Deserializer};

use super::comparable_transaction::ComparableTransaction;
use super::deserializers;
use super::list_item::ListItem;

#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub struct Transaction {
pub date: String,
#[serde(alias = "Date", deserialize_with = "deserialze_european_date")]
pub date: NaiveDate,
#[serde(rename = "Libellé")]
pub label: String,
#[serde(
rename = "Montant",
deserialize_with = "deserializers::deserialize_amount"
)]
#[serde(rename = "Montant")]
pub amount: f32,
#[serde(alias = "Solde")]
pub balance: f32,
}

impl ListItem for Transaction {
fn deserialze_european_date<'de, D>(d: D) -> Result<NaiveDate, D::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(d)?;
let (day, month, year) = string
.split("/")
.collect_tuple()
.ok_or(Error::custom("Missing date parts"))?;
let date_string = format!("{year}-{month}-{day}");
NaiveDate::from_str(&date_string).map_err(Error::custom)
}

impl ListItem<Transaction> for Transaction {
fn get_list_label(&self) -> ratatui::prelude::Text {
Text::raw(format!("{} - {} - {}", self.date, self.label, self.amount))
}
fn get_savable_value(&self) -> Vec<String> {
vec![
self.date.to_string(),
self.label.to_string(),
self.amount.to_string(),
]
fn get_savable_value(&self) -> Transaction {
self.clone()
}
}

Expand Down
20 changes: 10 additions & 10 deletions src/csv/parsers/assigned_transactions.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use anyhow::Result;
use std::path::PathBuf;

use anyhow::{Context, Result};
use csv::ReaderBuilder;
use itertools::Itertools;

use crate::csv::models::AssignedTransaction;

pub fn parse_assigned_transactions_csv(path: &str) -> Result<Vec<AssignedTransaction>> {
let mut reader = ReaderBuilder::new()
pub fn parse_assigned_transactions_csv(path: &PathBuf) -> Result<Vec<AssignedTransaction>> {
ReaderBuilder::new()
.delimiter(b',')
.has_headers(false)
.from_path(path)?;
let mut items = Vec::new();
for result in reader.deserialize() {
let record: AssignedTransaction = result?;
items.push(record)
}
Ok(items)
.from_path(path)?
.deserialize()
.map(|x| x.context("Could not deserialize csv"))
.try_collect()
}
26 changes: 20 additions & 6 deletions src/csv/parsers/transaction_csv.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
use anyhow::{anyhow, Result};
use csv::ReaderBuilder;
use encoding::all::ISO_8859_15;
use encoding::all::ISO_8859_1;
use encoding::Encoding;
use itertools::Itertools;
use std::fs::File;
use std::io::Read;

use crate::csv::models;
use crate::csv::models::{self, AssignedTransaction, ComparableTransaction};

pub fn parse_transaction_csv(path: &str) -> Result<models::ParseResult> {
pub fn parse_transaction_csv(
path: &str,
assigned_transactions: &[AssignedTransaction],
) -> Result<models::ParseResult> {
let mut file_content = Vec::new();
let mut file = File::open(path)?;
file.read_to_end(&mut file_content)?;
let encoded_file = ISO_8859_15
let encoded_file = ISO_8859_1
.decode(&file_content, encoding::DecoderTrap::Replace)
.map_err(|_| anyhow!("Could not get file encoding for {}", path))?;

let transactions = get_transactions(&encoded_file)?;
let balance = transactions.last().map(|x| x.balance).unwrap_or(0.0);

let transactions_to_be_assigned = transactions
.into_iter()
.filter(|x| {
!assigned_transactions
.iter()
.any(|y| x.get_comparable_value() == y.get_comparable_value())
})
.collect_vec();

Ok(models::ParseResult {
balance,
transactions,
transactions_to_be_assigned,
})
}

fn get_transactions(information: &str) -> Result<Vec<models::Transaction>> {
let mut reader = ReaderBuilder::new()
.delimiter(b';')
.delimiter(b',')
.from_reader(information.as_bytes());
let mut transactions = Vec::new();
for result in reader.deserialize() {
Expand Down
Loading