Move contribution calculation to spending trait

This commit is contained in:
traxys 2023-11-24 20:46:35 +01:00
parent 1b3839beb6
commit bc343e63c9

View file

@ -1,4 +1,10 @@
use std::{collections::BTreeMap, fs::OpenOptions, num::NonZeroU8, path::PathBuf};
use std::{
collections::BTreeMap,
fs::OpenOptions,
num::NonZeroU8,
ops::{Mul, MulAssign, Sub, SubAssign},
path::PathBuf,
};
use either::Either;
use iced::{
@ -776,12 +782,71 @@ where
type BoxIter<'a, I> = Box<dyn Iterator<Item = I> + 'a>;
#[derive(Clone, Copy, Debug)]
pub(crate) struct ContributionPoint {
person_1: f64,
person_2: f64,
}
impl ContributionPoint {
fn total(&self) -> f64 {
self.person_1 + self.person_2
}
}
impl SubAssign<ContributionPoint> for ContributionPoint {
fn sub_assign(&mut self, rhs: ContributionPoint) {
self.person_1 -= rhs.person_1;
self.person_2 -= rhs.person_2;
}
}
impl MulAssign<f64> for ContributionPoint {
fn mul_assign(&mut self, rhs: f64) {
self.person_1 *= rhs;
self.person_2 *= rhs;
}
}
impl Mul<f64> for ContributionPoint {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self {
person_1: self.person_1 * rhs,
person_2: self.person_2 * rhs,
}
}
}
impl Sub<ContributionPoint> for ContributionPoint {
type Output = Self;
fn sub(self, rhs: ContributionPoint) -> Self::Output {
Self {
person_1: self.person_1 - rhs.person_1,
person_2: self.person_2 - rhs.person_2,
}
}
}
pub(crate) struct Contribution {
main: ContributionPoint,
savings: BTreeMap<String, ContributionPoint>,
remaining: ContributionPoint,
}
pub(crate) struct PossibleContributions {
proportional: Contribution,
same_remaining: Contribution,
missing_months: u8,
}
pub(crate) trait Spendings {
fn recurring(&self) -> BoxIter<(&str, f64)>;
fn variable(&self) -> BoxIter<(&str, f64)>;
fn savings(&self) -> BoxIter<(&str, f64)>;
fn earnings_1(&self) -> f64;
fn earnings_2(&self) -> f64;
fn earnings(&self) -> ContributionPoint;
fn average(&self) -> NonZeroU8;
fn date(&self) -> Option<ReportDate>;
@ -791,6 +856,85 @@ pub(crate) trait Spendings {
.map(|(_, v)| v)
.sum()
}
fn contributions(&self, archive: &BTreeMap<ReportDate, Report>) -> PossibleContributions {
let total_spendings = self.total_spendings();
let total_earnings = self.earnings().total();
let (historic_spendings, count) = std::iter::once(total_spendings)
.chain(
match self.date() {
Some(date) => {
Either::Left(archive.iter().rev().skip_while(move |(d, _)| **d >= date))
}
None => Either::Right(archive.iter().rev()),
}
.scan(self.date(), |last, (current, report)| match last {
Some(last) if *current + 1 < *last => None,
_ => {
*last = Some(*current);
Some(report)
}
})
.map(|report| report.spendings()),
)
.take(self.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 = self.earnings();
let main = self.earnings() * main_factor;
remaining -= main;
let mut total_depositing = main_account;
let (savings_same_percent, saving_same_remaining) = self
.savings()
.map(|(account, amount)| {
let factor = amount / total_earnings;
let contributions = self.earnings() * factor;
total_depositing += amount;
remaining -= contributions;
(
(account.to_string(), contributions),
(
account.to_string(),
ContributionPoint {
person_1: amount / 2.,
person_2: amount / 2.,
},
),
)
})
.unzip();
let remaining_per_person = (total_earnings - total_depositing) / 2.;
let remaining_per_person = ContributionPoint {
person_1: remaining_per_person,
person_2: remaining_per_person,
};
PossibleContributions {
proportional: Contribution {
main,
remaining,
savings: savings_same_percent,
},
same_remaining: Contribution {
main: self.earnings() - remaining_per_person,
remaining: remaining_per_person,
savings: saving_same_remaining,
},
missing_months: self.average().get().saturating_sub(count),
}
}
}
pub struct EditState {
@ -822,12 +966,11 @@ impl Spendings for EditState {
Box::new(self.savings.iter().map(|(s, f)| (s.as_str(), *f)))
}
fn earnings_1(&self) -> f64 {
self.earnings_1
}
fn earnings_2(&self) -> f64 {
self.earnings_2
fn earnings(&self) -> ContributionPoint {
ContributionPoint {
person_1: self.earnings_1,
person_2: self.earnings_2,
}
}
fn average(&self) -> NonZeroU8 {
@ -994,111 +1137,83 @@ impl EditState {
}
}
pub(super) fn view(&self, archive: &BTreeMap<ReportDate, Report>, config: &Config) -> crate::Element {
let total_spendings = self.total_spendings();
let total_earnings = self.earnings_1() + self.earnings_2();
let (historic_spendings, count) = std::iter::once(total_spendings)
.chain(
match self.date() {
Some(date) => {
Either::Left(archive.iter().rev().skip_while(move |(d, _)| **d >= date))
}
None => Either::Right(archive.iter().rev()),
}
.scan(self.date(), |last, (current, report)| match last {
Some(last) if *current + 1 < *last => None,
_ => {
*last = Some(*current);
Some(report)
}
})
.map(|report| report.spendings()),
)
.take(self.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.earnings_1();
let mut remaining_2 = self.earnings_2();
pub(super) fn view(
&self,
archive: &BTreeMap<ReportDate, Report>,
config: &Config,
) -> crate::Element {
let contributions = self.contributions(archive);
let mut same_percent = column![
text("Same percent").size(TEXT_H2),
text("Main account").size(TEXT_EMPH2),
text(format!(
"{}: {:.2} €",
config.person_1,
self.earnings_1() * main_factor
config.person_1, contributions.proportional.main.person_1,
)),
text(format!(
"{}: {:.2} €",
config.person_2,
self.earnings_2() * main_factor
config.person_2, contributions.proportional.main.person_2,
)),
];
remaining_1 -= self.earnings_1() * main_factor;
remaining_2 -= self.earnings_2() * main_factor;
let mut total_depositing = main_account;
for (account, amount) in self.savings() {
let factor = amount / total_earnings;
for (account, contribution) in &contributions.proportional.savings {
same_percent = same_percent
.push(text(account).size(TEXT_EMPH2))
.push(text(format!(
"{}: {:.2} €",
config.person_1,
self.earnings_1() * factor
config.person_1, contribution.person_1,
)))
.push(text(format!(
"{}: {:.2} €",
config.person_2,
self.earnings_2() * factor
config.person_2, contribution.person_2,
)));
remaining_1 -= self.earnings_1() * factor;
remaining_2 -= self.earnings_2() * factor;
total_depositing += amount;
}
let remaining_per_person = (total_earnings - total_depositing) / 2.;
let mut same_remaining = column![
text("Same remaining").size(TEXT_H2),
text("Main account").size(TEXT_EMPH2),
text(format!(
"{}: {:.2} €",
config.person_1,
self.earnings_1() - remaining_per_person
config.person_1, contributions.same_remaining.main.person_1
)),
text(format!(
"{}: {:.2} €",
config.person_2,
self.earnings_2() - remaining_per_person
config.person_2, contributions.same_remaining.main.person_2
)),
];
for (account, amount) in &self.savings {
for (account, contribution) in &contributions.proportional.savings {
same_remaining = same_remaining
.push(text(account).size(TEXT_EMPH2))
.push(text(format!("{}: {:.2}", config.person_1, amount / 2.,)))
.push(text(format!("{}: {:.2}", config.person_2, amount / 2.,)));
.push(text(format!(
"{}: {:.2} €",
config.person_1, contribution.person_1
)))
.push(text(format!(
"{}: {:.2} €",
config.person_2, contribution.person_2
)));
}
let per_person = row![
same_percent
.push(text("Remaining").size(TEXT_EMPH2))
.push(text(format!("{}: {remaining_1:.2}", config.person_1,)))
.push(text(format!("{}: {remaining_2:.2}", config.person_2,)),),
.push(text(format!(
"{}: {:.2} €",
config.person_1, contributions.proportional.remaining.person_1
)))
.push(text(format!(
"{}: {:.2} €",
config.person_2, contributions.proportional.remaining.person_2
))),
horizontal_space(Length::Fill),
same_remaining
.push(text("Remaining").size(TEXT_EMPH2))
.push(text(format!("{remaining_per_person:.2} € per person"))),
.push(text(format!(
"{:.2} € per person",
contributions.same_remaining.remaining.person_1
))),
]
.align_items(Alignment::Start);
@ -1134,13 +1249,9 @@ impl EditState {
.push(button(text("Init from last month")).on_press(Message::InitFrom(last)));
}
let missing_warning = if count < self.average.get() {
format!(
" (Missing {} months for average)",
self.average.get() - count
)
} else {
"".into()
let missing_warning = match contributions.missing_months {
0 => "".into(),
n => format!(" (Missing {n} months for average)",),
};
scrollable(
@ -1167,7 +1278,7 @@ impl EditState {
let def = rule::StyleSheet::appearance(theme, &theme::Rule::Default);
rule::Appearance { width: 3, ..def }
}),
text(&format!("Total spendings: {total_spendings:.2}",)).size(TEXT_EMPH2),
text(&format!("Total spendings: {:.2}", self.total_spendings())).size(TEXT_EMPH2),
text("Earnings").size(TEXT_H1),
row![
text(&config.person_1).size(TEXT_EMPH1),
@ -1187,10 +1298,11 @@ impl EditState {
})
]
.align_items(iced::Alignment::Center),
text(&format!("Total earnings: {total_earnings:.2}",)).size(TEXT_EMPH2),
text(&format!("Total earnings: {:.2}", self.earnings().total())).size(TEXT_EMPH2),
text("Outputs").size(TEXT_H1),
text(&format!(
"Main account: {main_account:.2} €{missing_warning}"
"Main account: {:.2} €{missing_warning}",
contributions.same_remaining.main.total(),
))
.size(TEXT_EMPH2),
row![