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 either::Either;
|
||||||
use iced::{
|
use iced::{
|
||||||
|
|
@ -776,12 +782,71 @@ where
|
||||||
|
|
||||||
type BoxIter<'a, I> = Box<dyn Iterator<Item = I> + 'a>;
|
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 {
|
pub(crate) trait Spendings {
|
||||||
fn recurring(&self) -> BoxIter<(&str, f64)>;
|
fn recurring(&self) -> BoxIter<(&str, f64)>;
|
||||||
fn variable(&self) -> BoxIter<(&str, f64)>;
|
fn variable(&self) -> BoxIter<(&str, f64)>;
|
||||||
fn savings(&self) -> BoxIter<(&str, f64)>;
|
fn savings(&self) -> BoxIter<(&str, f64)>;
|
||||||
fn earnings_1(&self) -> f64;
|
fn earnings(&self) -> ContributionPoint;
|
||||||
fn earnings_2(&self) -> f64;
|
|
||||||
fn average(&self) -> NonZeroU8;
|
fn average(&self) -> NonZeroU8;
|
||||||
fn date(&self) -> Option<ReportDate>;
|
fn date(&self) -> Option<ReportDate>;
|
||||||
|
|
||||||
|
|
@ -791,6 +856,85 @@ pub(crate) trait Spendings {
|
||||||
.map(|(_, v)| v)
|
.map(|(_, v)| v)
|
||||||
.sum()
|
.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 {
|
pub struct EditState {
|
||||||
|
|
@ -822,12 +966,11 @@ impl Spendings for EditState {
|
||||||
Box::new(self.savings.iter().map(|(s, f)| (s.as_str(), *f)))
|
Box::new(self.savings.iter().map(|(s, f)| (s.as_str(), *f)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn earnings_1(&self) -> f64 {
|
fn earnings(&self) -> ContributionPoint {
|
||||||
self.earnings_1
|
ContributionPoint {
|
||||||
}
|
person_1: self.earnings_1,
|
||||||
|
person_2: self.earnings_2,
|
||||||
fn earnings_2(&self) -> f64 {
|
}
|
||||||
self.earnings_2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn average(&self) -> NonZeroU8 {
|
fn average(&self) -> NonZeroU8 {
|
||||||
|
|
@ -994,111 +1137,83 @@ impl EditState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn view(&self, archive: &BTreeMap<ReportDate, Report>, config: &Config) -> crate::Element {
|
pub(super) fn view(
|
||||||
let total_spendings = self.total_spendings();
|
&self,
|
||||||
let total_earnings = self.earnings_1() + self.earnings_2();
|
archive: &BTreeMap<ReportDate, Report>,
|
||||||
|
config: &Config,
|
||||||
let (historic_spendings, count) = std::iter::once(total_spendings)
|
) -> crate::Element {
|
||||||
.chain(
|
let contributions = self.contributions(archive);
|
||||||
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();
|
|
||||||
|
|
||||||
let mut same_percent = column![
|
let mut same_percent = column![
|
||||||
text("Same percent").size(TEXT_H2),
|
text("Same percent").size(TEXT_H2),
|
||||||
text("Main account").size(TEXT_EMPH2),
|
text("Main account").size(TEXT_EMPH2),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{}: {:.2} €",
|
"{}: {:.2} €",
|
||||||
config.person_1,
|
config.person_1, contributions.proportional.main.person_1,
|
||||||
self.earnings_1() * main_factor
|
|
||||||
)),
|
)),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{}: {:.2} €",
|
"{}: {:.2} €",
|
||||||
config.person_2,
|
config.person_2, contributions.proportional.main.person_2,
|
||||||
self.earnings_2() * main_factor
|
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
|
||||||
remaining_1 -= self.earnings_1() * main_factor;
|
for (account, contribution) in &contributions.proportional.savings {
|
||||||
remaining_2 -= self.earnings_2() * main_factor;
|
|
||||||
|
|
||||||
let mut total_depositing = main_account;
|
|
||||||
|
|
||||||
for (account, amount) in self.savings() {
|
|
||||||
let factor = amount / total_earnings;
|
|
||||||
same_percent = same_percent
|
same_percent = same_percent
|
||||||
.push(text(account).size(TEXT_EMPH2))
|
.push(text(account).size(TEXT_EMPH2))
|
||||||
.push(text(format!(
|
.push(text(format!(
|
||||||
"{}: {:.2} €",
|
"{}: {:.2} €",
|
||||||
config.person_1,
|
config.person_1, contribution.person_1,
|
||||||
self.earnings_1() * factor
|
|
||||||
)))
|
)))
|
||||||
.push(text(format!(
|
.push(text(format!(
|
||||||
"{}: {:.2} €",
|
"{}: {:.2} €",
|
||||||
config.person_2,
|
config.person_2, contribution.person_2,
|
||||||
self.earnings_2() * factor
|
|
||||||
)));
|
)));
|
||||||
|
|
||||||
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![
|
let mut same_remaining = column![
|
||||||
text("Same remaining").size(TEXT_H2),
|
text("Same remaining").size(TEXT_H2),
|
||||||
text("Main account").size(TEXT_EMPH2),
|
text("Main account").size(TEXT_EMPH2),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{}: {:.2} €",
|
"{}: {:.2} €",
|
||||||
config.person_1,
|
config.person_1, contributions.same_remaining.main.person_1
|
||||||
self.earnings_1() - remaining_per_person
|
|
||||||
)),
|
)),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{}: {:.2} €",
|
"{}: {:.2} €",
|
||||||
config.person_2,
|
config.person_2, contributions.same_remaining.main.person_2
|
||||||
self.earnings_2() - remaining_per_person
|
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (account, amount) in &self.savings {
|
for (account, contribution) in &contributions.proportional.savings {
|
||||||
same_remaining = same_remaining
|
same_remaining = same_remaining
|
||||||
.push(text(account).size(TEXT_EMPH2))
|
.push(text(account).size(TEXT_EMPH2))
|
||||||
.push(text(format!("{}: {:.2} €", config.person_1, amount / 2.,)))
|
.push(text(format!(
|
||||||
.push(text(format!("{}: {:.2} €", config.person_2, amount / 2.,)));
|
"{}: {:.2} €",
|
||||||
|
config.person_1, contribution.person_1
|
||||||
|
)))
|
||||||
|
.push(text(format!(
|
||||||
|
"{}: {:.2} €",
|
||||||
|
config.person_2, contribution.person_2
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let per_person = row![
|
let per_person = row![
|
||||||
same_percent
|
same_percent
|
||||||
.push(text("Remaining").size(TEXT_EMPH2))
|
.push(text("Remaining").size(TEXT_EMPH2))
|
||||||
.push(text(format!("{}: {remaining_1:.2} €", config.person_1,)))
|
.push(text(format!(
|
||||||
.push(text(format!("{}: {remaining_2:.2} €", config.person_2,)),),
|
"{}: {:.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),
|
horizontal_space(Length::Fill),
|
||||||
same_remaining
|
same_remaining
|
||||||
.push(text("Remaining").size(TEXT_EMPH2))
|
.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);
|
.align_items(Alignment::Start);
|
||||||
|
|
||||||
|
|
@ -1134,13 +1249,9 @@ impl EditState {
|
||||||
.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.average.get() {
|
let missing_warning = match contributions.missing_months {
|
||||||
format!(
|
0 => "".into(),
|
||||||
" (Missing {} months for average)",
|
n => format!(" (Missing {n} months for average)",),
|
||||||
self.average.get() - count
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
"".into()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollable(
|
scrollable(
|
||||||
|
|
@ -1167,7 +1278,7 @@ impl EditState {
|
||||||
let def = rule::StyleSheet::appearance(theme, &theme::Rule::Default);
|
let def = rule::StyleSheet::appearance(theme, &theme::Rule::Default);
|
||||||
rule::Appearance { width: 3, ..def }
|
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),
|
text("Earnings").size(TEXT_H1),
|
||||||
row![
|
row![
|
||||||
text(&config.person_1).size(TEXT_EMPH1),
|
text(&config.person_1).size(TEXT_EMPH1),
|
||||||
|
|
@ -1187,10 +1298,11 @@ impl EditState {
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
.align_items(iced::Alignment::Center),
|
.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("Outputs").size(TEXT_H1),
|
||||||
text(&format!(
|
text(&format!(
|
||||||
"Main account: {main_account:.2} €{missing_warning}"
|
"Main account: {:.2} €{missing_warning}",
|
||||||
|
contributions.same_remaining.main.total(),
|
||||||
))
|
))
|
||||||
.size(TEXT_EMPH2),
|
.size(TEXT_EMPH2),
|
||||||
row![
|
row![
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue