mod codec; /// simple typed helper enums mod simple_enums; mod transaction; fn main() { let mut args: Vec<_> = std::env::args().skip(1).collect(); if args.is_empty() { return; } use { chrono::Datelike, itertools::Itertools, prettytable::{format, row, Table}, rayon::prelude::*, std::{ collections::{BTreeMap, HashSet}, io::Write, }, }; let mut highlight = HashSet::new(); let hl_only = args[0] == "--highlight-only"; if args[0] == "--highlight" || hl_only { args.remove(0); let eo_selst = args.iter().take_while(|i| *i != ";").count(); highlight = args .drain(..std::cmp::min(eo_selst + 1, args.len())) .collect(); highlight.remove(";"); } let i_skipped = std::sync::atomic::AtomicUsize::new(0); let mut dat: Vec<_> = args .into_par_iter() .flat_map(|i| { let mut rdr = csv::ReaderBuilder::new() .delimiter(b';') .flexible(true) .has_headers(false) .from_reader(codec::Latin12Utf8Reader::new( std::fs::File::open(i).expect("unable to open file"), )); let mut recsit = rdr.records().skip(8); assert_eq!( recsit.next().unwrap().unwrap(), vec![ "Buchungstag", "Valuta", "Auftraggeber/Zahlungsempfänger", "Empfänger/Zahlungspflichtiger", "Konto-Nr.", "IBAN", "BLZ", "BIC", "Vorgang/Verwendungszweck", "Kundenreferenz", "Währung", "Umsatz", " " ] ); let mut idat = Vec::new(); let mut iisk = 0usize; for result in recsit { let record = result.expect("got invalid line"); let record_bak = record.clone(); let pres: Result = std::convert::TryInto::try_into(record); match pres { Ok(tl) => { if tl.direction == simple_enums::TransactionDirection::Haben && tl.waehrung == simple_enums::Waehrung::Eur && !tl.p_other.is_empty() && !tl.p_other.contains(" ZINS BIS ") { idat.push(tl) } else { iisk += 1; } } Err(transaction::ParseError::Finalizer) => break, Err(x) => panic!("got error '{}' @ {:?}", x, record_bak), } } i_skipped.fetch_add(iisk, std::sync::atomic::Ordering::SeqCst); idat }) .collect(); dat.par_sort(); let oldlen = dat.len(); let mut newlen = 0usize; let mut stdout = term::stdout().unwrap(); let mut accu = BTreeMap::< (i32, bool, String), (usize, transaction::TransactionValue, String, usize), >::new(); for i in dat.into_iter().dedup() { newlen += 1; let mut ent = accu .entry((i.d_buchungs.year(), i.d_buchungs.month() > 6, i.p_other)) .or_default(); ent.0 += 1; ent.1 += i.umsatz; } writeln!( &mut stdout, "skipped {} duplicates and {} misc entries", oldlen - newlen, i_skipped.into_inner(), ) .unwrap(); if accu.is_empty() { return; } let mut pdsp = 0; for i in accu.values_mut() { i.2 = i.1.to_string(); i.3 = i.2.find('.').map(|x| i.2.len() - x).unwrap_or(0); pdsp = std::cmp::max(pdsp, i.3); } let mut table = Table::new(); table.set_format( format::FormatBuilder::new() .column_separator('│') .borders('│') .separator( format::LinePosition::Top, format::LineSeparator::new('─', '┬', '┌', '┐'), ) .separator( format::LinePosition::Title, format::LineSeparator::new('─', '┼', '├', '┤'), ) .separator( format::LinePosition::Bottom, format::LineSeparator::new('─', '┴', '└', '┘'), ) .padding(1, 1) .build(), ); table.set_titles(row!["Jahr", "Einzahler", "Zahlungen", "Summe"]); let mut prev_yuhj = Option::::None; for (k, v) in accu.iter_mut() { let is_hl = highlight.contains(&*k.2); if !hl_only || is_hl { let mut yearuhj = format!("{} {}I", k.0, if k.1 { "I" } else { "" }); let is_ny = prev_yuhj.as_ref().map(|i| i != &yearuhj); v.2 += &" ".repeat(pdsp - v.3); if is_ny.unwrap_or(false) { table.add_row(row!["", "", "", ""]); } if is_ny.unwrap_or(true) { prev_yuhj = Some(yearuhj.clone()); } else { yearuhj.clear(); } table.add_row(if !hl_only && is_hl { row![yearuhj, FYBdb-> k.2, r-> v.0.to_string(), Fgbr-> v.2] } else { row![yearuhj, k.2, r-> v.0.to_string(), r-> v.2] }); } } table .print_term(&mut *stdout) .expect("unable to print table"); }