4 Commits

Author SHA1 Message Date
james ae543e5edf chore: Release ynabmunger version 0.3.0 2026-06-03 09:28:37 +02:00
james f7b645895d Format transaction amounts on output
This might want to use the same YNAB CSV format as `convert`.
2026-06-03 09:20:12 +02:00
james 14d0d82719 Tidy up amount calculation 2026-06-03 09:16:46 +02:00
james c9e005475c Add filter to transactions command 2026-06-03 09:16:05 +02:00
6 changed files with 36 additions and 11 deletions
Generated
+1 -1
View File
@@ -1963,7 +1963,7 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "ynabmunger"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"anyhow",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ynabmunger"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
publish = false
+12 -3
View File
@@ -55,6 +55,11 @@ enum Command {
/// The number of days to look back for transactions
#[arg(short, long, default_value_t = 30)]
days: i64,
/// Filter transactions by a search term in the payee field. This will match any transaction
/// whose payee contains the search term, case-insensitive.
#[arg(short, long)]
filter: Option<String>,
},
/// Convert from a bank export to a CSV you can import manually to YNAB
Convert {
@@ -141,6 +146,7 @@ async fn main() -> anyhow::Result<()> {
account,
account_id,
days,
filter,
} => {
let client = Client::new(&token);
let plan = match (plan, plan_id) {
@@ -157,12 +163,15 @@ async fn main() -> anyhow::Result<()> {
};
let transactions = match account {
Some(account) => account.list_transactions(days).await?,
None => plan.list_transactions(None, days).await?,
Some(account) => account.list_transactions(days, filter.as_deref()).await?,
None => {
plan.list_transactions(None, days, filter.as_deref())
.await?
}
};
println!("Transactions{} in the last 30 days:", accountstr);
for t in transactions {
println!("{},{},{}", t.date, t.payee, t.amount);
println!("{},{},{}", t.date, t.payee, t.format_amount());
}
Ok(())
+2 -4
View File
@@ -18,9 +18,7 @@ fn parse_amount_str(amount_str: &str) -> Result<i64, Error> {
let frac = format!("{:0<2}", frac);
let frac = frac.parse::<i64>()?;
let mut amount: i64 = 0;
amount += whole * 100 * 10;
amount += frac * 10;
let amount = whole * 100 * 10 + frac * 10;
Ok(if negative { -amount } else { amount })
}
@@ -34,7 +32,7 @@ impl Transaction {
})
}
fn format_amount(&self) -> String {
pub fn format_amount(&self) -> String {
let whole = self.amount / 10 / 100;
let frac = if self.amount < 0 { -1 } else { 1 } * (self.amount / 10) % 100;
format!("{}.{:02}", whole, frac)
+10 -2
View File
@@ -130,6 +130,7 @@ impl Plan {
&self,
account_id: Option<&str>,
days: i64,
filter: Option<&str>,
) -> Result<Vec<Transaction>, Error> {
let since_date = chrono::Local::now().date_naive() - chrono::Duration::days(days);
let since_date = since_date.format("%Y-%m-%d");
@@ -139,6 +140,7 @@ impl Plan {
Ok(transform::from_ynab_transactions(
&res.data.transactions,
account_id,
filter,
))
}
}
@@ -175,8 +177,14 @@ impl Account {
})
}
pub async fn list_transactions(&self, days: i64) -> Result<Vec<Transaction>, Error> {
self.plan.list_transactions(Some(&self.id), days).await
pub async fn list_transactions(
&self,
days: i64,
filter: Option<&str>,
) -> Result<Vec<Transaction>, Error> {
self.plan
.list_transactions(Some(&self.id), days, filter)
.await
}
pub async fn upload(&self, transactions: &[Transaction]) -> Result<(), Error> {
+10
View File
@@ -31,6 +31,7 @@ pub(crate) fn to_ynab_transactions(
pub(crate) fn from_ynab_transactions(
transactions: &[apitypes::Transaction],
account_id: Option<&str>,
filter: Option<&str>,
) -> Vec<Transaction> {
transactions
.iter()
@@ -38,6 +39,15 @@ pub(crate) fn from_ynab_transactions(
Some(id) => t.account_id == id,
None => true,
})
.filter(|t| match filter {
Some(filter) => t
.payee_name
.to_owned()
.unwrap_or(String::new())
.to_lowercase()
.contains(filter.to_lowercase().as_str()),
None => true,
})
.map(|t| Transaction {
date: t.date.to_owned(),
payee: match &t.payee_name {