finql_data/
cash_flow.rs

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/// Container for an amount of money in some currency
11#[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    /// Round a cash amount to that number of decimals
89    pub fn round(&self, digits: i32) -> CashAmount {
90        CashAmount {
91            amount: round2digits(self.amount, digits),
92            currency: self.currency,
93        }
94    }
95
96    /// Round Cash amount according to rounding conventions
97    /// Lookup currency in rounding_conventions. If found, use the number of digits found for
98    /// rounding to that number of decimals, otherwise round to two decimals.
99    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/// Container for a single cash flow
125#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
126pub struct CashFlow {
127    pub amount: CashAmount,
128    pub date: NaiveDate,
129}
130
131impl CashFlow {
132    /// Construct new cash flow
133    pub fn new(amount: f64, currency: Currency, date: NaiveDate) -> CashFlow {
134        CashFlow {
135            amount: CashAmount { amount, currency },
136            date,
137        }
138    }
139    /// Check, whether cash flows could be aggregated
140    pub fn aggregatable(&self, cf: &CashFlow) -> bool {
141        self.amount.currency == cf.amount.currency && self.date == cf.date
142    }
143
144    /// Compare to cash flows for equality within a given absolute tolerance
145    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}