Skip to content
Merged
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
9 changes: 9 additions & 0 deletions revolut_data/eur_savings_2026-04-26.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Completed Date,Product name,Description,Money out,Money in,Balance
19 mar 2025,,Money brought forward,,,€0
21 mar 2025,Instant Access - Aion Bank,Deposit,,+€337.37,€337.37
22 mar 2025,Instant Access - Aion Bank,"Gross interest
Earned on 2025/03/22",,+€0.01,€337.38
23 mar 2025,Instant Access - Aion Bank,"Gross interest
Earned on 2025/03/23",,+€0.01,€337.39
14 Jul 2025,Instant Access - Aion Bank,Withdrawal,-€100,,€238.53
31 Dec 2025,,Money carried forward,,,€0
92 changes: 90 additions & 2 deletions src/csvparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
cashline.to_string().replace(",", ".")
};
let cashline_string: String = cashline_string.replace(" ", "");
let cashline_string: String = cashline_string.trim_start_matches('+').to_string();
log::info!("Processed moneyin/total amount line: {cashline_string}");
let mut euro_parser = tuple((double::<&str, Error<_>>, tag("€")));
let mut euro_parser2 = tuple((tag("€"), double::<&str, Error<_>>));
Expand Down Expand Up @@ -121,6 +122,32 @@
}
}

fn sanitize_df(df: &DataFrame) -> DataFrame {
if let Ok(col) = df.column("Description") {
if let Ok(utf) = col.utf8() {
let vals: Vec<String> = utf
.into_iter()
.map(|opt| {
opt.map(|st| {
let replaced = st
.replace("\r\n", " ")
.replace('\n', " ")
.replace('\r', " ");
replaced.split_whitespace().collect::<Vec<_>>().join(" ")
})
.unwrap_or_default()
})
.collect();
let new_series = Series::new("Description", vals);
let mut new_df = df.clone();
if new_df.with_column(new_series).is_ok() {
return new_df;
}
}
}
df.clone()
}

fn extract_dividends_transactions(df: &DataFrame) -> Result<DataFrame, &'static str> {
let df_transactions = if df.get_column_names().contains(&"Currency") {
df.select([
Expand Down Expand Up @@ -535,11 +562,11 @@
.finish()
.map_err(|e| format!("Error reading CSV: {e}"))?;

log::info!("CSV DataFrame: {df}");
log::info!("CSV DataFrame: {}", sanitize_df(&df));

let filtred_df = extract_intrest_rate_transactions(&df)?;

log::info!("Filtered data of Interest: {filtred_df}");
log::info!("Filtered data of Interest: {}", sanitize_df(&filtred_df));

ta.dates = parse_investment_transaction_dates(&filtred_df, "Completed Date")?;

Expand Down Expand Up @@ -853,6 +880,40 @@

Ok(())
}
#[test]
fn test_sanitize_df_removes_newlines() -> Result<(), String> {
let desc = vec!["Line1\nLine2", "LineA\r\nLineB"];
let dates = vec!["01 Jan 2025", "02 Jan 2025"];
let prod = vec!["P", "Q"];
let money_out = vec!["null", "-€10"];
let money_in = vec!["+€0.01", "+€5"];
let balance = vec!["€0", "€5.01"];

let df = DataFrame::new(vec![
Series::new("Completed Date", dates),
Series::new("Product name", prod),
Series::new("Description", desc),
Series::new("Money out", money_out),
Series::new("Money in", money_in),
Series::new("Balance", balance),
])
.map_err(|e| format!("DataFrame creation failed: {e}"))?;

let sanitized = sanitize_df(&df);
let desc_col = sanitized
.column("Description")
.map_err(|_| "Missing Description column")?;
let utf = desc_col.utf8().map_err(|_| "Description not utf8")?;
for opt in utf.into_iter() {
if let Some(s) = opt {
if s.contains('\n') || s.contains('\r') {
return Err(format!("Found newline in Description after sanitize: {s}"));
}
}
}

Ok(())
}

#[test]
fn test_parse_investment_incomes() -> Result<(), String> {
Expand All @@ -873,7 +934,7 @@
Ok(())
}

fn test_parse_date_helper(

Check warning on line 937 in src/csvparser.rs

View workflow job for this annotation

GitHub Actions / coverage

function `test_parse_date_helper` is never used
description: Vec<&str>,
input_dates: Vec<&str>,
expected_dates: Vec<String>,
Expand Down Expand Up @@ -1670,4 +1731,31 @@
);
Ok(())
}

#[test]
fn test_parse_revolut_eur_savings_20260426() -> Result<(), String> {
let res = parse_revolut_transactions("revolut_data/eur_savings_2026-04-26.csv");
if res.is_err() {
return Err(format!("Parsing failed: {:?}", res));
}
let dividends = res.unwrap().dividend_transactions;
// dates, incomes, taxes, symbols
let expected_result = vec![
(
"03/22/25".to_owned(),
crate::Currency::EUR(0.01),
crate::Currency::EUR(0.00),
None,
),
(
"03/23/25".to_owned(),
crate::Currency::EUR(0.01),
crate::Currency::EUR(0.00),
None,
),
];
assert_eq!(dividends, expected_result);

Ok(())
}
}
Loading