Move contribution calculation to spending trait
This commit is contained in:
parent
1b3839beb6
commit
bc343e63c9
1 changed files with 193 additions and 81 deletions
274
src/edit.rs
274
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<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![
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue