1use crate::config::BalanceType;
7use crate::kernel::Settings;
8use crate::kernel::price_lookup::PriceLookupCtx;
9use crate::kernel::report_item_selector::BalanceSelector;
10use crate::model::balance_tree_node::ord_by_btn;
11use crate::model::{BalanceTreeNode, Commodity, Transaction, TxnAccount, TxnSet};
12use crate::tackler;
13use itertools::Itertools;
14use rust_decimal::Decimal;
15use std::collections::{BTreeMap, HashMap};
16use std::sync::Arc;
17
18pub type Deltas = BTreeMap<Option<Arc<Commodity>>, Decimal>;
20pub type BTNs = Vec<BalanceTreeNode>;
21#[derive(Debug)]
22pub struct Balance {
23 pub(crate) title: String,
24 pub(crate) bal: BTNs,
25 pub(crate) deltas: Deltas,
26}
27
28impl Balance {
29 pub(crate) fn is_empty(&self) -> bool {
30 self.bal.is_empty()
31 }
32}
33
34impl Balance {
35 fn bubble_up_acctn(
47 acc_sums: &mut HashMap<TxnAccount, Decimal>,
48 my_acctn_sum: &(TxnAccount, Decimal),
49 settings: &Settings,
50 ) -> Result<(), tackler::Error> {
51 let my_acctn = &my_acctn_sum.0;
52 if my_acctn.is_root() {
53 Ok(())
56 } else {
57 let new_parent_atn =
59 settings.get_txn_account(my_acctn.atn.parent.as_str(), &my_acctn.comm)?;
60
61 let parent = acc_sums.get_key_value(&new_parent_atn);
62 if parent.is_some() {
63 Ok(())
65 } else if new_parent_atn.is_root() {
66 acc_sums.insert(new_parent_atn, Decimal::ZERO);
67 Ok(())
69 } else {
70 acc_sums.insert(new_parent_atn.clone(), Decimal::ZERO);
71 Balance::bubble_up_acctn(acc_sums, &(new_parent_atn, Decimal::ZERO), settings)
72 }
73 }
74 }
75
76 fn calculate_account_sums<'a, I>(
81 txns: I,
82 price_lookup_ctx: &PriceLookupCtx<'_>,
83 inverted: bool,
84 ) -> impl Iterator<Item = (TxnAccount, Decimal)>
85 where
86 I: Iterator<Item = &'a &'a Transaction>,
87 {
88 let mut account_sums: HashMap<TxnAccount, Decimal> = HashMap::new();
89
90 txns.for_each(|txn| {
91 price_lookup_ctx.convert_prices(txn).for_each(|p| {
92 let val = if inverted { -p.1 } else { p.1 };
93 account_sums
94 .entry(p.0)
95 .and_modify(|v| {
96 *v += val;
97 })
98 .or_insert(val);
99 });
100 });
101 account_sums.into_iter()
102 }
103
104 fn balance_tree<'a, I>(
112 txns: I,
113 price_lookup_ctx: &PriceLookupCtx<'_>,
114 settings: &Settings,
115 ) -> Result<Vec<BalanceTreeNode>, tackler::Error>
116 where
117 I: Iterator<Item = &'a &'a Transaction>,
118 {
119 let account_sums: Vec<(TxnAccount, Decimal)> =
124 Self::calculate_account_sums(txns, price_lookup_ctx, settings.inverted).collect();
125
126 let mut complete_acctn_sums: HashMap<TxnAccount, Decimal> =
131 HashMap::from_iter(account_sums.iter().cloned());
132
133 account_sums
134 .iter()
135 .try_for_each(|acc| -> Result<(), tackler::Error> {
136 Balance::bubble_up_acctn(&mut complete_acctn_sums, acc, settings)
137 })?;
138
139 let mut bal_tns: HashMap<TxnAccount, BalanceTreeNode> = HashMap::new();
140 complete_acctn_sums
141 .iter()
142 .sorted_by(|a, b| a.0.atn.depth.cmp(&b.0.atn.depth).reverse())
143 .try_for_each(|atn_val| -> Result<(), tackler::Error> {
144 let me = bal_tns
146 .entry(atn_val.0.clone())
147 .and_modify(|v| {
148 v.account_sum += *atn_val.1;
149 v.sub_acc_tree_sum += *atn_val.1;
150 })
151 .or_insert({
152 BalanceTreeNode {
153 acctn: atn_val.0.clone(),
154 sub_acc_tree_sum: *atn_val.1,
155 account_sum: *atn_val.1,
156 }
157 });
158
159 let tree_sum = me.sub_acc_tree_sum;
160
161 if !atn_val.0.is_root() {
163 let parent_atn =
164 settings.get_txn_account(atn_val.0.atn.parent.as_str(), &atn_val.0.comm)?;
165
166 bal_tns
167 .entry(parent_atn.clone())
168 .and_modify(|v| {
169 v.sub_acc_tree_sum += tree_sum;
170 })
171 .or_insert({
172 BalanceTreeNode {
173 acctn: parent_atn,
174 sub_acc_tree_sum: tree_sum,
175 account_sum: Decimal::ZERO,
176 }
177 });
178 }
179 Ok(())
180 })?;
181
182 let bal: Vec<BalanceTreeNode> = bal_tns
183 .into_iter()
184 .map(|c| c.1)
185 .sorted_by(ord_by_btn)
186 .collect();
187
188 Ok(bal)
189 }
190
191 pub fn from<T>(
196 title: &str,
197 txn_set: &TxnSet<'_>,
198 price_lookup_ctx: &PriceLookupCtx<'_>,
199 accounts: &T,
200 settings: &Settings,
201 ) -> Result<Balance, tackler::Error>
202 where
203 T: BalanceSelector + ?Sized,
204 {
205 Self::from_iter(
206 title,
207 &txn_set.txns,
208 price_lookup_ctx,
209 accounts,
210 settings,
211 settings.report.balance.bal_type.clone(),
212 )
213 }
214
215 #[allow(clippy::needless_pass_by_value)]
216 pub(crate) fn from_iter<'a, I, T>(
217 title: &str,
218 txns: I,
219 price_lookup_ctx: &PriceLookupCtx<'_>,
220 accounts: &T,
221 settings: &Settings,
222 bal_type: BalanceType,
223 ) -> Result<Balance, tackler::Error>
224 where
225 T: BalanceSelector + ?Sized,
226 I: IntoIterator<Item = &'a &'a Transaction>,
227 {
228 let bal = match bal_type {
229 BalanceType::Tree => {
230 Balance::balance_tree(txns.into_iter(), price_lookup_ctx, settings)?
231 }
232 BalanceType::Flat => {
233 Balance::balance_flat(txns.into_iter(), price_lookup_ctx, settings)
234 }
235 };
236
237 let filt_bal: Vec<_> = bal.into_iter().filter(|b| accounts.eval(b)).collect();
238
239 if filt_bal.is_empty() {
240 Ok(Balance {
241 title: title.to_string(),
242 bal: Vec::default(),
243 deltas: BTreeMap::default(),
244 })
245 } else {
246 let deltas = filt_bal
247 .iter()
248 .chunk_by(|btn| btn.acctn.comm.clone())
249 .into_iter()
250 .map(|(c, bs)| {
251 let dsum = bs.map(|b| b.account_sum).sum();
252 (c.is_any().then_some(c), dsum)
253 })
254 .collect();
255
256 Ok(Balance {
257 title: title.to_string(),
258 bal: filt_bal,
259 deltas,
260 })
261 }
262 }
263
264 fn balance_flat<'a, I>(
265 txns: I,
266 price_lookup_ctx: &PriceLookupCtx<'_>,
267 settings: &Settings,
268 ) -> Vec<BalanceTreeNode>
269 where
270 I: Iterator<Item = &'a &'a Transaction>,
271 {
272 let account_sums = Self::calculate_account_sums(txns, price_lookup_ctx, settings.inverted);
273
274 let mut v: Vec<BalanceTreeNode> = account_sums
275 .map(|(acctn, acc_sum)| BalanceTreeNode {
276 acctn: acctn.clone(),
277 sub_acc_tree_sum: Decimal::ZERO,
278 account_sum: acc_sum,
279 })
280 .collect();
281
282 v.sort_by(ord_by_btn);
283 v
284 }
285}