From 2a74d250a02555317ccb88944a6240f6c8e721ed Mon Sep 17 00:00:00 2001 From: traxys Date: Wed, 22 Nov 2023 00:20:31 +0100 Subject: [PATCH] Allow to average over severall months --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3e6249..acb2b81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,6 +718,7 @@ version = "0.1.0" dependencies = [ "anyhow", "directories", + "either", "iced", "iced_aw", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 5028afb..1f853a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0.75" directories = "5.0.1" +either = "1.9.0" iced = { version = "0.10.0", features = ["lazy"] } iced_aw = { version = "0.7.0", default-features = false, features = ["modal", "card", "number_input", "selection_list"] } itertools = "0.11.0" diff --git a/src/main.rs b/src/main.rs index 0e12724..9499bf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,13 @@ use std::{ collections::{BTreeMap, HashMap}, fs::{File, OpenOptions}, io::BufReader, + num::NonZeroU8, path::PathBuf, }; use anyhow::anyhow; use directories::ProjectDirs; +use either::Either; use iced::{ font, subscription, theme, widget::{ @@ -713,7 +715,11 @@ where } } -#[derive(Serialize, Deserialize, Default, Clone)] +fn one() -> NonZeroU8 { + NonZeroU8::MIN +} + +#[derive(Serialize, Deserialize, Clone)] struct Report { #[serde(default)] recurring: BTreeMap, @@ -725,6 +731,35 @@ struct Report { earnings_1: f64, #[serde(default)] earnings_2: f64, + #[serde(default = "one")] + average: NonZeroU8, +} + +impl Report { + fn spendings(&self) -> f64 { + self.recurring + .values() + .copied() + .chain( + self.variable + .values() + .filter_map(|expr| calc::calc_parser::calc(expr).ok()), + ) + .sum() + } +} + +impl Default for Report { + fn default() -> Self { + Self { + recurring: Default::default(), + savings: Default::default(), + variable: Default::default(), + earnings_1: Default::default(), + earnings_2: Default::default(), + average: NonZeroU8::MIN, + } + } } #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)] @@ -833,6 +868,7 @@ enum Message { SetDate(ReportDate), Load(ReportDate), InitFrom(ReportDate), + ChangeAverage(u8), } struct Glaurung { @@ -849,11 +885,19 @@ struct EditState { save_file: PathBuf, earnings_1: f64, earnings_2: f64, + average: NonZeroU8, date: Option, } impl EditState { + fn spendings(&self) -> f64 { + self.recurring + .values() + .chain(self.variable.values().flat_map(|(_, f)| f)) + .sum() + } + fn load(&mut self, report: Report) { self.recurring = report.recurring; self.savings = report.savings; @@ -867,6 +911,7 @@ impl EditState { .collect(); self.earnings_1 = report.earnings_1; self.earnings_2 = report.earnings_2; + self.average = report.average; } fn init_from(&mut self, report: Report) { @@ -879,6 +924,7 @@ impl EditState { .into_keys() .map(|k| (k, Default::default())) .collect(); + self.average = report.average; } fn report(&self) -> Report { @@ -893,6 +939,7 @@ impl EditState { .collect(), earnings_1: self.earnings_1, earnings_2: self.earnings_2, + average: self.average, } } @@ -1017,6 +1064,7 @@ impl Application for Glaurung { earnings_1: Default::default(), earnings_2: Default::default(), save_file: config.save_file, + average: one(), date: None, }, archive: config.save.archive, @@ -1091,21 +1139,46 @@ impl Application for Glaurung { Message::DeleteVariable(d) => { self.edit.variable.remove(&d); } + Message::ChangeAverage(a) => { + if let Some(a) = NonZeroU8::new(a) { + self.edit.average = a; + } + } } Command::none() } fn view(&self) -> Element { - let total_spendings = self - .edit - .recurring - .values() - .chain(self.edit.variable.values().flat_map(|(_, f)| f)) - .sum::(); + let total_spendings = self.edit.spendings(); let total_earnings = self.edit.earnings_1 + self.edit.earnings_2; - let main_account = total_spendings; + let (historic_spendings, count) = std::iter::once(total_spendings) + .chain( + match self.edit.date { + Some(date) => Either::Left( + self.archive + .iter() + .rev() + .skip_while(move |(d, _)| **d >= date), + ), + None => Either::Right(self.archive.iter().rev()), + } + .scan(self.edit.date, |last, (current, report)| match last { + Some(last) if *current + 1 < *last => None, + _ => { + *last = Some(*current); + Some(report) + } + }) + .map(|report| report.spendings()), + ) + .take(self.edit.average.get() as _) + .fold((0., 0), |(acc, count), spending| { + (acc + spending, count + 1) + }); + + let main_account = historic_spendings / count as f64; let main_factor = main_account / total_earnings; let mut remaining_1 = self.edit.earnings_1; @@ -1233,6 +1306,15 @@ impl Application for Glaurung { .push(button(text("Init from last month")).on_press(Message::InitFrom(last))); } + let missing_warning = if count < self.edit.average.get() { + format!( + " (Missing {} months for average)", + self.edit.average.get() - count + ) + } else { + "".into() + }; + scrollable( column![ text(date).size(TEXT_H1), @@ -1279,7 +1361,15 @@ impl Application for Glaurung { .align_items(iced::Alignment::Center), text(&format!("Total earnings: {total_earnings:.2} €",)).size(TEXT_EMPH2), text("Outputs").size(TEXT_H1), - text(&format!("Main account: {main_account:.2} €")).size(TEXT_EMPH2), + text(&format!( + "Main account: {main_account:.2} €{missing_warning}" + )) + .size(TEXT_EMPH2), + row![ + text("Averaging factor"), + number_input(self.edit.average.get(), u8::MAX, Message::ChangeAverage), + ] + .align_items(Alignment::Center), component(FixedAmounts { items: &self.edit.savings, title: "Savings",