show.rs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. use console::Style;
  2. use crate::data::{
  3. AccountName, Decimal, Root,
  4. ledger::{Change, Transaction},
  5. spec::AccountSpec,
  6. };
  7. #[derive(Clone, Copy, Default)]
  8. enum Align {
  9. #[default]
  10. Left,
  11. Centre,
  12. Right,
  13. }
  14. #[derive(Default)]
  15. struct Column {
  16. pub align: Align,
  17. pub left_border: bool,
  18. pub right_border: bool,
  19. // pub contents: Box<dyn Iterator<Item = dyn Display>>,
  20. }
  21. enum Row {
  22. Line,
  23. Data(Vec<String>),
  24. }
  25. fn show_table(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
  26. let mut min_widths = vec![0usize; cols.len()];
  27. let table_data = rows.collect::<Vec<_>>();
  28. for row in &table_data {
  29. let Row::Data(row) = &row else { continue };
  30. for (idx, rc) in row.iter().enumerate() {
  31. min_widths[idx] = min_widths[idx].max(console::measure_text_width(rc.as_str()));
  32. }
  33. }
  34. for row in table_data {
  35. match row {
  36. Row::Line => {
  37. for (idx, col) in min_widths.iter().enumerate() {
  38. if cols[idx].left_border {
  39. print!("{}", boxy::Char::cross(boxy::Weight::Normal));
  40. }
  41. for _ in 0..=*col {
  42. print!("{}", boxy::Char::horizontal(boxy::Weight::Normal));
  43. }
  44. if cols[idx].right_border {
  45. print!("{}", boxy::Char::cross(boxy::Weight::Normal));
  46. }
  47. }
  48. }
  49. Row::Data(row) => {
  50. for (idx, col) in row.into_iter().enumerate() {
  51. if cols[idx].left_border {
  52. print!("{}", boxy::Char::vertical(boxy::Weight::Normal));
  53. }
  54. match cols[idx].align {
  55. Align::Left => print!("{col:<width$}", width = min_widths[idx] + 1),
  56. Align::Centre => print!("{col:^width$}", width = min_widths[idx] + 1),
  57. Align::Right => print!("{col:>width$}", width = min_widths[idx] + 1),
  58. }
  59. if cols[idx].right_border {
  60. print!("{}", boxy::Char::vertical(boxy::Weight::Normal));
  61. }
  62. }
  63. }
  64. }
  65. println!();
  66. }
  67. }
  68. #[derive(Clone, Copy, Default)]
  69. pub struct TransactionTable {}
  70. impl TransactionTable {
  71. pub fn show<'d>(
  72. self,
  73. root: Option<&Root>,
  74. account: AccountName,
  75. txns: impl Iterator<Item = &'d Transaction>,
  76. ) {
  77. show_table(
  78. vec![
  79. // datestamp
  80. Column {
  81. align: Align::Left,
  82. right_border: true,
  83. ..Default::default()
  84. },
  85. // Memo
  86. Column {
  87. align: Align::Left,
  88. ..Default::default()
  89. },
  90. // Amount
  91. Column {
  92. align: Align::Right,
  93. ..Default::default()
  94. },
  95. // Balance
  96. Column {
  97. align: Align::Right,
  98. left_border: true,
  99. ..Default::default()
  100. },
  101. ],
  102. vec![
  103. Row::Data(vec![
  104. "Date".into(),
  105. "Memo".into(),
  106. "Amount".into(),
  107. "Balance".into(),
  108. ]),
  109. Row::Line,
  110. ]
  111. .into_iter()
  112. .chain(
  113. txns.filter_map(|txn| txn.change_for(account).map(|chg| (txn, chg)))
  114. .map(|(txn, chg)| {
  115. let precision =
  116. root.and_then(|r| r.unit_spec(*chg.unit)).and_then(|v| v.precision).unwrap_or(2) as usize;
  117. Row::Data(vec![
  118. txn.datestamp.to_string(),
  119. txn.title.clone().unwrap_or_else(String::new),
  120. format!("{:.precision$}", chg.amount),
  121. chg.balance
  122. .as_deref()
  123. .map(|b| format!("{:.precision$}", b))
  124. .unwrap_or(String::new()),
  125. ])
  126. }),
  127. ),
  128. )
  129. }
  130. }