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