1use crate::kernel::balance::{BTNs, Balance, Deltas};
7use crate::kernel::report_item_selector::{
8 BalanceAllSelector, BalanceByAccountSelector, BalanceSelector,
9};
10use crate::kernel::{BalanceSettings, Settings};
11use crate::model::{BalanceTreeNode, TxnSet};
12use crate::report::{Report, write_acc_sel_checksum, write_price_metadata, write_report_timezone};
13use crate::tackler;
14use itertools::Itertools;
15use rust_decimal::prelude::Zero;
16use rust_decimal::{Decimal, RoundingStrategy};
17use std::cmp::max;
18use std::io;
19
20#[derive(Debug, Clone)]
21pub struct BalanceReporter {
22 pub report_settings: BalanceSettings,
23}
24
25impl TryFrom<&Settings> for BalanceReporter {
26 type Error = tackler::Error;
27
28 fn try_from(settings: &Settings) -> Result<Self, Self::Error> {
29 Ok(BalanceReporter {
30 report_settings: BalanceSettings::try_from(settings)?,
31 })
32 }
33}
34
35impl BalanceReporter {
36 pub(crate) fn acc_selector(ras: &[String]) -> Result<Box<dyn BalanceSelector>, tackler::Error> {
37 if ras.is_empty() {
38 Ok(Box::<BalanceAllSelector>::default())
39 } else {
40 let s: Vec<_> = ras.iter().map(|s| s.as_str()).collect();
41 let ras = BalanceByAccountSelector::from(&s)?;
42 Ok(Box::new(ras))
43 }
44 }
45
46 fn get_acc_selector(&self) -> Result<Box<dyn BalanceSelector>, tackler::Error> {
47 BalanceReporter::acc_selector(&self.report_settings.ras)
48 }
49}
50
51impl BalanceReporter {
52 pub(crate) fn txt_report<W: io::Write + ?Sized>(
53 writer: &mut W,
54 bal_report: &Balance,
55 bal_settings: &BalanceSettings,
56 ) -> Result<(), tackler::Error> {
57 let get_max_sum_len = |bal: &BTNs, f: fn(&BalanceTreeNode) -> Decimal| -> usize {
58 bal.iter()
59 .map(|btn| {
60 let d = f(btn);
61 format!("{:+.prec$}", d, prec = bal_settings.scale.get_precision(&d))
63 .chars()
64 .count()
65 })
66 .fold(0, max)
67 };
68 fn get_max_delta_len(deltas: &Deltas) -> usize {
69 deltas
70 .iter()
71 .map(|(_, d)| format!("{}", d).chars().count())
72 .fold(0, max)
73 }
74 fn get_max_commodity_len(deltas: &Deltas) -> usize {
77 deltas
78 .iter()
79 .map(|(opt_comm, _)| {
80 opt_comm
81 .as_ref()
82 .map_or(0, |comm| comm.name.chars().count())
83 })
84 .fold(0, max)
85 }
86
87 let delta_max_len = get_max_delta_len(&bal_report.deltas);
88 let comm_max_len = get_max_commodity_len(&bal_report.deltas);
89
90 let left_sum_len = max(
92 12,
93 max(
94 get_max_sum_len(&bal_report.bal, |btn| btn.account_sum),
95 delta_max_len,
96 ),
97 );
98
99 let sub_acc_tree_sum_len = get_max_sum_len(&bal_report.bal, |btn| btn.sub_acc_tree_sum);
100
101 let filler_field = if comm_max_len.is_zero() {
105 " ".repeat(3)
106 } else {
107 " ".repeat(4 + comm_max_len)
108 };
109 let filler_field_len = filler_field.chars().count();
110
111 fn make_commodity_field(comm_max_len: usize, btn: &BalanceTreeNode) -> String {
112 if comm_max_len.is_zero() {
113 " ".repeat(2)
115 } else {
116 let comm = &btn.acctn.comm;
117 match &comm.is_any() {
118 true => {
119 format!(" {: <cl$} ", comm.name, cl = comm_max_len)
120 }
121 false => format!(" {} ", " ".repeat(comm_max_len)),
122 }
123 }
124 }
125
126 let left_ruler = " ".repeat(9);
127
128 writeln!(writer, "{}", bal_report.title)?;
129 writeln!(writer, "{}", "-".repeat(bal_report.title.chars().count()))?;
130
131 if !bal_report.is_empty() {
132 for btn in &bal_report.bal {
133 let prec_1 = bal_settings.scale.get_precision(&btn.account_sum);
134 let prec_2 = bal_settings.scale.get_precision(&btn.sub_acc_tree_sum);
135
136 writeln!(
137 writer,
138 "{left_ruler}{:>asl$.prec_1$}{:>width$}{:>satsl$.prec_2$}{}{}",
139 btn.account_sum.round_dp_with_strategy(
140 prec_1 as u32,
141 RoundingStrategy::MidpointAwayFromZero
142 ),
143 "",
144 btn.sub_acc_tree_sum.round_dp_with_strategy(
145 prec_2 as u32,
146 RoundingStrategy::MidpointAwayFromZero
147 ),
148 make_commodity_field(comm_max_len, btn),
149 btn.acctn.atn,
150 asl = left_sum_len,
151 satsl = sub_acc_tree_sum_len,
152 width = filler_field_len,
153 )?;
154 }
155
156 writeln!(
157 writer,
158 "{}",
159 "=".repeat(
160 left_ruler.chars().count()
161 + left_sum_len
162 + (if comm_max_len.is_zero() {
163 0
164 } else {
165 comm_max_len + 1
166 })
167 )
168 )?;
169
170 let deltas = bal_report.deltas.iter().sorted_by_key(|i| {
171 i.0.as_ref()
172 .map_or(String::default(), |comm| comm.name.clone())
173 });
174 for delta in deltas {
175 let prec = bal_settings.scale.get_precision(delta.1);
176 writeln!(
177 writer,
178 "{left_ruler}{:>width$.prec$}{}",
179 delta.1.round_dp_with_strategy(
180 prec as u32,
181 RoundingStrategy::MidpointAwayFromZero
182 ),
183 delta
184 .0
185 .as_ref()
186 .map_or(String::default(), |c| format!(" {}", &c.name)),
187 width = left_sum_len,
188 )?;
189 }
190 }
191 Ok(())
192 }
193}
194
195impl Report for BalanceReporter {
196 fn write_txt_report<W: io::Write + ?Sized>(
197 &self,
198 cfg: &Settings,
199 writer: &mut W,
200 txn_data: &TxnSet<'_>,
201 ) -> Result<(), tackler::Error> {
202 let bal_acc_sel = self.get_acc_selector()?;
203
204 let price_lookup_ctx = self.report_settings.price_lookup.make_ctx(
205 &txn_data.txns,
206 self.report_settings.report_commodity.clone(),
207 &cfg.price.price_db,
208 );
209
210 write_acc_sel_checksum(cfg, writer, bal_acc_sel.as_ref())?;
211
212 if !price_lookup_ctx.is_empty() {
213 write_report_timezone(cfg, writer)?;
214 }
215
216 write_price_metadata(cfg, writer, &price_lookup_ctx)?;
217
218 writeln!(writer)?;
219
220 let bal_report = Balance::from(
221 &self.report_settings.title,
222 txn_data,
223 &price_lookup_ctx,
224 bal_acc_sel.as_ref(),
225 cfg,
226 )?;
227
228 BalanceReporter::txt_report(writer, &bal_report, &self.report_settings)?;
229 Ok(())
230 }
231}