diff --git a/src/edit.rs b/src/edit.rs index cec5e7b..ed1f70e 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -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 + '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 for ContributionPoint { + fn sub_assign(&mut self, rhs: ContributionPoint) { + self.person_1 -= rhs.person_1; + self.person_2 -= rhs.person_2; + } +} + +impl MulAssign for ContributionPoint { + fn mul_assign(&mut self, rhs: f64) { + self.person_1 *= rhs; + self.person_2 *= rhs; + } +} + +impl Mul 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 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, + 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; @@ -791,6 +856,85 @@ pub(crate) trait Spendings { .map(|(_, v)| v) .sum() } + + fn contributions(&self, archive: &BTreeMap) -> 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, 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, + 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![