Allow to average over severall months

This commit is contained in:
traxys 2023-11-22 00:20:31 +01:00
parent cc6dc16380
commit 2a74d250a0
3 changed files with 101 additions and 9 deletions

1
Cargo.lock generated
View file

@ -718,6 +718,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"directories", "directories",
"either",
"iced", "iced",
"iced_aw", "iced_aw",
"itertools", "itertools",

View file

@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
directories = "5.0.1" directories = "5.0.1"
either = "1.9.0"
iced = { version = "0.10.0", features = ["lazy"] } iced = { version = "0.10.0", features = ["lazy"] }
iced_aw = { version = "0.7.0", default-features = false, features = ["modal", "card", "number_input", "selection_list"] } iced_aw = { version = "0.7.0", default-features = false, features = ["modal", "card", "number_input", "selection_list"] }
itertools = "0.11.0" itertools = "0.11.0"

View file

@ -2,11 +2,13 @@ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::BufReader, io::BufReader,
num::NonZeroU8,
path::PathBuf, path::PathBuf,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use directories::ProjectDirs; use directories::ProjectDirs;
use either::Either;
use iced::{ use iced::{
font, subscription, theme, font, subscription, theme,
widget::{ widget::{
@ -713,7 +715,11 @@ where
} }
} }
#[derive(Serialize, Deserialize, Default, Clone)] fn one() -> NonZeroU8 {
NonZeroU8::MIN
}
#[derive(Serialize, Deserialize, Clone)]
struct Report { struct Report {
#[serde(default)] #[serde(default)]
recurring: BTreeMap<String, f64>, recurring: BTreeMap<String, f64>,
@ -725,6 +731,35 @@ struct Report {
earnings_1: f64, earnings_1: f64,
#[serde(default)] #[serde(default)]
earnings_2: f64, 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)] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)]
@ -833,6 +868,7 @@ enum Message {
SetDate(ReportDate), SetDate(ReportDate),
Load(ReportDate), Load(ReportDate),
InitFrom(ReportDate), InitFrom(ReportDate),
ChangeAverage(u8),
} }
struct Glaurung { struct Glaurung {
@ -849,11 +885,19 @@ struct EditState {
save_file: PathBuf, save_file: PathBuf,
earnings_1: f64, earnings_1: f64,
earnings_2: f64, earnings_2: f64,
average: NonZeroU8,
date: Option<ReportDate>, date: Option<ReportDate>,
} }
impl EditState { impl EditState {
fn spendings(&self) -> f64 {
self.recurring
.values()
.chain(self.variable.values().flat_map(|(_, f)| f))
.sum()
}
fn load(&mut self, report: Report) { fn load(&mut self, report: Report) {
self.recurring = report.recurring; self.recurring = report.recurring;
self.savings = report.savings; self.savings = report.savings;
@ -867,6 +911,7 @@ impl EditState {
.collect(); .collect();
self.earnings_1 = report.earnings_1; self.earnings_1 = report.earnings_1;
self.earnings_2 = report.earnings_2; self.earnings_2 = report.earnings_2;
self.average = report.average;
} }
fn init_from(&mut self, report: Report) { fn init_from(&mut self, report: Report) {
@ -879,6 +924,7 @@ impl EditState {
.into_keys() .into_keys()
.map(|k| (k, Default::default())) .map(|k| (k, Default::default()))
.collect(); .collect();
self.average = report.average;
} }
fn report(&self) -> Report { fn report(&self) -> Report {
@ -893,6 +939,7 @@ impl EditState {
.collect(), .collect(),
earnings_1: self.earnings_1, earnings_1: self.earnings_1,
earnings_2: self.earnings_2, earnings_2: self.earnings_2,
average: self.average,
} }
} }
@ -1017,6 +1064,7 @@ impl Application for Glaurung {
earnings_1: Default::default(), earnings_1: Default::default(),
earnings_2: Default::default(), earnings_2: Default::default(),
save_file: config.save_file, save_file: config.save_file,
average: one(),
date: None, date: None,
}, },
archive: config.save.archive, archive: config.save.archive,
@ -1091,21 +1139,46 @@ impl Application for Glaurung {
Message::DeleteVariable(d) => { Message::DeleteVariable(d) => {
self.edit.variable.remove(&d); self.edit.variable.remove(&d);
} }
Message::ChangeAverage(a) => {
if let Some(a) = NonZeroU8::new(a) {
self.edit.average = a;
}
}
} }
Command::none() Command::none()
} }
fn view(&self) -> Element { fn view(&self) -> Element {
let total_spendings = self let total_spendings = self.edit.spendings();
.edit
.recurring
.values()
.chain(self.edit.variable.values().flat_map(|(_, f)| f))
.sum::<f64>();
let total_earnings = self.edit.earnings_1 + self.edit.earnings_2; 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 main_factor = main_account / total_earnings;
let mut remaining_1 = self.edit.earnings_1; 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))); .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( scrollable(
column![ column![
text(date).size(TEXT_H1), text(date).size(TEXT_H1),
@ -1279,7 +1361,15 @@ impl Application for Glaurung {
.align_items(iced::Alignment::Center), .align_items(iced::Alignment::Center),
text(&format!("Total earnings: {total_earnings:.2}",)).size(TEXT_EMPH2), text(&format!("Total earnings: {total_earnings:.2}",)).size(TEXT_EMPH2),
text("Outputs").size(TEXT_H1), 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 { component(FixedAmounts {
items: &self.edit.savings, items: &self.edit.savings,
title: "Savings", title: "Savings",