1use std::{fmt,fmt::{Display, Formatter}};
2use std::collections::BTreeMap;
3use std::ops::Neg;
4
5use serde::{Deserialize, Serialize};
6use chrono::{DateTime, Utc, NaiveDate};
7
8use crate::currency::{Currency, CurrencyConverter, CurrencyError};
9
10#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
12pub struct CashAmount {
13 pub amount: f64,
14 pub currency: Currency,
15}
16
17pub fn round2digits(x: f64, digits: i32) -> f64 {
18 (x * 10.0_f64.powi(digits)).round() / 10.0_f64.powi(digits)
19}
20
21impl CashAmount {
22 pub async fn add(
23 &mut self,
24 cash_amount: CashAmount,
25 time: DateTime<Utc>,
26 currency_converter: &(dyn CurrencyConverter+Send+Sync),
27 with_rounding: bool,
28 ) -> Result<&mut Self, CurrencyError> {
29 if self.currency == cash_amount.currency {
30 self.amount += cash_amount.amount;
31 } else {
32 let fx_rate = currency_converter.fx_rate(cash_amount.currency, self.currency, time).await?;
33 self.amount += fx_rate * cash_amount.amount;
34 if with_rounding {
35 let digits = self.currency.rounding_digits();
36 self.amount = round2digits(self.amount, digits);
37 }
38 }
39 Ok(self)
40 }
41
42 pub async fn add_opt(
43 &mut self,
44 cash_amount: Option<CashAmount>,
45 time: DateTime<Utc>,
46 currency_converter: &(dyn CurrencyConverter+Send+Sync),
47 with_rounding: bool,
48 ) -> Result<&mut Self, CurrencyError> {
49 match cash_amount {
50 None => Ok(self),
51 Some(cash_amount) => self.add(cash_amount, time, currency_converter, with_rounding).await,
52 }
53 }
54
55 pub async fn sub(
56 &mut self,
57 cash_amount: CashAmount,
58 time: DateTime<Utc>,
59 currency_converter: &(dyn CurrencyConverter+Send+Sync),
60 with_rounding: bool,
61 ) -> Result<&mut Self, CurrencyError> {
62 if self.currency == cash_amount.currency {
63 self.amount -= cash_amount.amount;
64 } else {
65 let fx_rate = currency_converter.fx_rate(cash_amount.currency, self.currency, time).await?;
66 self.amount -= fx_rate * cash_amount.amount;
67 if with_rounding {
68 let digits = self.currency.rounding_digits();
69 self.amount = round2digits(self.amount, digits);
70 }
71 }
72 Ok(self)
73 }
74
75 pub async fn sub_opt(
76 &mut self,
77 cash_amount: Option<CashAmount>,
78 time: DateTime<Utc>,
79 currency_converter: &(dyn CurrencyConverter+Send+Sync),
80 with_rounding: bool,
81 ) -> Result<&mut Self, CurrencyError> {
82 match cash_amount {
83 None => Ok(self),
84 Some(cash_amount) => self.sub(cash_amount, time, currency_converter, with_rounding).await,
85 }
86 }
87
88 pub fn round(&self, digits: i32) -> CashAmount {
90 CashAmount {
91 amount: round2digits(self.amount, digits),
92 currency: self.currency,
93 }
94 }
95
96 pub fn round_by_convention(&self, rounding_conventions: &BTreeMap<String, i32>) -> CashAmount {
100 match rounding_conventions.get_key_value(&self.currency.to_string()) {
101 Some((_, digits)) => self.round(*digits),
102 None => self.round(2),
103 }
104 }
105}
106
107impl Display for CashAmount {
108 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
109 write!(f, "{:16.4} {}", self.amount, self.currency)
110 }
111}
112
113impl Neg for CashAmount {
114 type Output = CashAmount;
115
116 fn neg(self) -> Self::Output {
117 CashAmount {
118 amount: -self.amount,
119 currency: self.currency,
120 }
121 }
122}
123
124#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
126pub struct CashFlow {
127 pub amount: CashAmount,
128 pub date: NaiveDate,
129}
130
131impl CashFlow {
132 pub fn new(amount: f64, currency: Currency, date: NaiveDate) -> CashFlow {
134 CashFlow {
135 amount: CashAmount { amount, currency },
136 date,
137 }
138 }
139 pub fn aggregatable(&self, cf: &CashFlow) -> bool {
141 self.amount.currency == cf.amount.currency && self.date == cf.date
142 }
143
144 pub fn fuzzy_cash_flows_cmp_eq(&self, cf: &CashFlow, tol: f64) -> bool {
146 self.aggregatable(cf)
147 && !self.amount.amount.is_nan()
148 && !cf.amount.amount.is_nan()
149 && (self.amount.amount - cf.amount.amount).abs() <= tol
150 }
151}
152
153impl Display for CashFlow {
154 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155 write!(f, "{} {}", self.date, self.amount)
156 }
157}
158
159impl Neg for CashFlow {
160 type Output = CashFlow;
161
162 fn neg(self) -> Self::Output {
163 CashFlow {
164 amount: -self.amount,
165 date: self.date,
166 }
167 }
168}