Bläddra i källkod

Minor cleanups, start using cliask.

Kestrel 2 veckor sedan
förälder
incheckning
a3d90b2780
9 ändrade filer med 152 tillägg och 194 borttagningar
  1. 2 1
      Cargo.toml
  2. 14 4
      src/cmd.rs
  3. 32 106
      src/cmd/comb.rs
  4. 35 41
      src/cmd/infer.rs
  5. 37 0
      src/cmd/txnmatch.rs
  6. 30 3
      src/data.rs
  7. 1 5
      src/data/parse.rs
  8. 1 18
      src/import.rs
  9. 0 16
      src/show.rs

+ 2 - 1
Cargo.toml

@@ -11,6 +11,7 @@ stringstore = { version = "0.1.5", features = ["serde"] }
 rust_decimal = { version = "1.37" }
 itertools = { version = "0.14" }
 nanoid = { version = "0.4.0" }
+chrono = { version = "0.4.4", default-features = false, features = ["alloc"] }
 
 # i/o dependencies
 serde = { version = "1.0", features = ["derive"] }
@@ -18,7 +19,6 @@ toml = { version = "0.8", features = [] }
 chumsky = { version = "0.10", features = ["lexical-numbers"] }
 ariadne = { version = "0.5" }
 csv = { version = "1.3" }
-strptime = { version = "1.1" }
 tempfile = { version = "3.20" }
 
 # cli dependencies
@@ -26,3 +26,4 @@ pretty_env_logger = { version = "0.5.0" }
 clap = { version = "4.5", features = ["derive", "env"] }
 console = { version = "0.15" }
 boxy = { version = "0.1.0" }
+cliask = { version = "0.1.0", path = "../cliask/" }

+ 14 - 4
src/cmd.rs

@@ -7,6 +7,7 @@ use crate::prelude::*;
 
 mod infer;
 mod comb;
+mod txnmatch;
 
 #[derive(clap::Parser)]
 pub struct Invocation {
@@ -58,6 +59,10 @@ pub enum Command {
     Match {
         account: data::AccountName,
     },
+    Merge {
+        txn1: String,
+        txn2: String,
+    },
     Comb {
         account: Option<data::AccountName>,
     },
@@ -112,7 +117,7 @@ impl Command {
                 summarize(&data);
             }
             Self::Ledger { account } => {
-                let data = load_data(&mut fsdata, inv, Default::default())?;
+                let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
                 let aname = data::AccountName::new(account.as_str());
 
@@ -212,7 +217,7 @@ impl Command {
             Self::Match { account } => {
                 let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
-                let Some(acc_data) = data.ledger_data_for(*account) else {
+                /*let Some(acc_data) = data.ledger_data_for(*account) else {
                     log::error!("No ledger data available for account {account}");
                     return Ok(());
                 };
@@ -222,12 +227,17 @@ impl Command {
                     .filter(|v| v.borrow().get_annotation("id").is_some())
                     .sorted()
                     .collect_vec();
+                */
+
+                if txnmatch::do_match(&data, *account)? {
+                    
+                }
 
-                show::TransactionTable::default().show_refs(
+                /*show::TransactionTable::default().show_refs(
                     Some(&data),
                     *account,
                     txns.into_iter()
-                );
+                );*/
             },
             Self::Comb { account } => {
                 let data = load_data(&mut fsdata, inv, check::CheckLevel::Consistent)?;

+ 32 - 106
src/cmd/comb.rs

@@ -2,86 +2,15 @@ use std::collections::HashSet;
 
 use crate::prelude::*;
 
-use crate::{show, data::{AccountName, Hoard}};
-
-fn prompt_account(data: &Hoard) -> anyhow::Result<Option<AccountName>> {
-    println!("Enter new account:");
-
-    let mut acc = String::default();
-
-    let stdout = console::Term::stdout();
-
-    println!();
-
-    stdout.move_cursor_up(1)?;
-
-    loop {
-        stdout.clear_line()?;
-        stdout.write_line(format!("{}", console::style(&acc).yellow()).as_str())?;
-        stdout.move_cursor_up(1)?;
-        stdout.move_cursor_right(acc.len())?;
-
-        match console::Term::stdout().read_key()? {
-            console::Key::Backspace => {
-                acc.pop();
-            },
-            console::Key::Enter => {
-                if let Some(r) = data.account_names().find(|an| **an == *acc) {
-                    return Ok(Some(r))
-                }
-            },
-            console::Key::Char(c) => {
-                if !c.is_control() {
-                    acc.push(c);
-                }
-            },
-            console::Key::Tab => {
-                if acc.is_empty() { continue }
-
-                let possibles = data.account_names().filter(|an| an.starts_with(acc.as_str())).collect::<Vec<_>>();
-
-                if possibles.is_empty() { continue }
-
-                let longest = possibles.iter().map(|s| s.len()).min().unwrap();
-
-                let mut longest_prefix = 0;
-                for plen in 1..=longest {
-                    if possibles.iter().all(|p| p.starts_with(&possibles[0][0..plen])) {
-                        longest_prefix = plen;
-                    }
-                }
-
-                acc.clear();
-                acc.push_str(&possibles[0][0..longest_prefix]);
-
-                stdout.clear_to_end_of_screen()?;
-
-                if possibles.len() == 1 {
-                    continue
-                }
-
-                stdout.move_cursor_down(1)?;
-                stdout.clear_line()?;
-
-                let mut line = String::new();
-                let len = possibles.len();
-                for possible in possibles.iter().take(5) {
-                    line.push_str(format!("{} ", console::style(possible).white().dim()).as_str());
-                }
-                if len > 5 {
-                    line.push_str("...");
-                }
-
-                stdout.write_line(line.as_str())?;
-                stdout.move_cursor_up(2)?;
-
-            },
-            console::Key::Escape => {
-                return Ok(None)
-            },
-            _key => (),
-        }
-    }
+use crate::data::{AccountName, Hoard};
+
+#[derive(PartialEq, Clone, Copy, Debug, cliask::ActionEnum)]
+enum Action {
+    Ignore,
+    Skip,
+    Edit,
+    Finish,
+    Abort
 }
 
 pub fn do_comb(data: &Hoard, account: AccountName) -> anyhow::Result<bool> {
@@ -108,33 +37,30 @@ pub fn do_comb(data: &Hoard, account: AccountName) -> anyhow::Result<bool> {
         println!();
         show::show_transaction(Some(&data), &txn);
 
-        println!();
-        println!("(I)gnore (S)kip all (E)dit (F)inish (A)bort: ");
-        let action = loop {
-            match console::Term::stdout().read_char().unwrap().to_lowercase().next() {
-                Some('s') => break 's',
-                Some('e') => break 'e',
-                Some('i') => break 'i',
-                Some('f') => break 'f',
-                Some('a') => break 'a',
-                _ => (),
-            }
-        };
+        let action = cliask::ActionPrompt::new().with_default(Action::Ignore).run()?;
 
-        if action == 'i' { () }
-        else if action == 's' {
-            ignore.insert(txn.title.clone());
-        } else if action == 'a' {
-            aborted = true;
-            break 
-        } else if action == 'e' {
-            let Some(new_account) = prompt_account(data)? else { continue };
-            drop(txn);
-            let mut txn = txn_ref.borrow_mut();
-            txn.changes.iter_mut().find(|e| *e.account == account).iter_mut().for_each(|ch| *ch.account = new_account);
-            changed = true;
-        } else if action == 'f' {
-            break
+        match action {
+            Action::Ignore => (),
+            Action::Skip => {
+                ignore.insert(txn.title.clone());
+            },
+            Action::Edit => {
+                let Some(new_account) = cliask::SelectPrompt::new("Select account:")
+                    .with_items(data.account_names())
+                    .run_cancellable()? else { continue };
+
+                drop(txn);
+                let mut txn = txn_ref.borrow_mut();
+                txn.changes.iter_mut().find(|e| *e.account == account).iter_mut().for_each(|ch| *ch.account = new_account);
+                changed = true;
+            },
+            Action::Finish => {
+                break
+            },
+            Action::Abort => {
+                aborted = true;
+                break
+            },
         }
     }
 

+ 35 - 41
src/cmd/infer.rs

@@ -1,10 +1,10 @@
+use std::collections::HashSet;
+
+use crate::prelude::*;
+
 use crate::data::Hoard;
 use crate::show::show_transaction;
 
-fn input_loop() -> anyhow::Result<bool> {
-    Ok(true)
-}
-
 pub fn do_inference(data: &mut Hoard) -> anyhow::Result<bool> {
     let placeholder = data.spec_root().placeholder_account;
 
@@ -32,6 +32,8 @@ pub fn do_inference(data: &mut Hoard) -> anyhow::Result<bool> {
 
         drop(chg);
 
+        let mut rejected = HashSet::<data::AccountName>::new();
+
         for other_txn_ref in other_txns {
             let other_txn = other_txn_ref.borrow();
             if other_txn.title.is_none() || other_txn.title != txn_title || *other_txn == *txn {
@@ -43,9 +45,12 @@ pub fn do_inference(data: &mut Hoard) -> anyhow::Result<bool> {
             let candidate = chg.next().unwrap();
             if chg.next().is_some() || *candidate.account == placeholder {
                 continue;
+            } else if rejected.contains(&candidate.account) {
+                continue
             }
-            log::debug!(
-                "possible candidate {}, titles are {:?} and {:?}",
+
+            log::trace!(
+                "candidate {}, titles are {:?} and {:?}",
                 candidate.account,
                 other_txn.title,
                 txn.title
@@ -53,41 +58,30 @@ pub fn do_inference(data: &mut Hoard) -> anyhow::Result<bool> {
 
             show_transaction(None, &txn);
 
-            println!(
-                "Candidate account: {} (Y/n)",
-                console::style(candidate.account).red()
-            );
-            let answer = console::Term::stdout().read_char().unwrap();
-            match answer.to_lowercase().next() {
-                Some('y') | Some('\n') | Some('\r') => {
-                    let new_account = *candidate.account;
-                    println!(
-                        "    Changing to account {} ...",
-                        console::style(new_account).green()
-                    );
-
-                    drop(txn);
-
-                    txn_ref.borrow_mut()
-                        .changes
-                        .iter_mut()
-                        .for_each(|c| {
-                            log::info!(
-                                "change account is {}, placeholder is {}",
-                                c.account,
-                                placeholder
-                            );
-                            if *c.account == placeholder {
-                                c.account = new_account.into();
-                                log::info!("    changed account to {new_account}");
-                            }
-                        });
-                    any_changed = true;
-
-                    break;
-                }
-                Some('n') => (),
-                c => println!("unknown {c:?}"),
+            println!("Candidate account is {}. Use?", console::style(candidate.account).red());
+
+            if cliask::ActionPrompt::<cliask::YesNoAction>::new().with_default(cliask::YesNoAction::Yes).run()?.into() {
+                let new_account = *candidate.account;
+                log::info!(
+                    "    Changing to account {} ...",
+                    console::style(new_account).green()
+                );
+
+                drop(txn);
+
+                txn_ref.borrow_mut()
+                    .changes
+                    .iter_mut()
+                    .for_each(|c| {
+                        if *c.account == placeholder {
+                            c.account = new_account.into();
+                            log::trace!("    changed account to {new_account}");
+                        }
+                    });
+                any_changed = true;
+                break
+            } else {
+                rejected.insert(*candidate.account);
             }
         }
     }

+ 37 - 0
src/cmd/txnmatch.rs

@@ -0,0 +1,37 @@
+use crate::prelude::*;
+
+pub fn do_match(data: &data::Hoard, account: data::AccountName) -> anyhow::Result<bool> {
+    let Some(account_data)  = data.ledger_data_for(account) else { 
+        log::warn!("No data for account {account}");
+        return Ok(false)
+    };
+
+    let mut sorted_data : Vec<_> = account_data.into();
+    sorted_data.sort_by_key(|d| d.borrow().datestamp);
+
+    let search_window = chrono::Days::new(7);
+
+    for txn_ref in &sorted_data {
+        let txn = txn_ref.borrow();
+
+        let amount = *txn.change_for(account).unwrap().amount;
+
+
+        // go one week prior and after
+        let range_start = sorted_data.binary_search_by_key(&txn.datestamp.checked_sub_days(search_window).unwrap(), |d| d.borrow().datestamp).unwrap_or_else(|a| a);
+        let range_end = sorted_data.binary_search_by_key(&txn.datestamp.checked_add_days(search_window).unwrap(), |d| d.borrow().datestamp).unwrap_or_else(|a| a);
+
+        for otxn_ref in sorted_data.iter().take(range_end).skip(range_start) {
+            let otxn = otxn_ref.borrow();
+            if *otxn.change_for(account).unwrap().amount == -amount {
+                println!("possible matching transactions:");
+                show::show_transaction(Some(data), &txn);
+                show::show_transaction(Some(data), &otxn);
+
+
+            }
+        }
+    }
+
+    Ok(false)
+}

+ 30 - 3
src/data.rs

@@ -28,6 +28,9 @@ impl stringstore::NamespaceTag for AccountTag {
 }
 pub type AccountName = stringstore::StoredString<AccountTag>;
 
+pub type Datestamp = chrono::NaiveDate;
+
+/*
 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct Datestamp {
     pub year: u16,
@@ -35,9 +38,31 @@ pub struct Datestamp {
     pub day: u8,
 }
 
-impl std::fmt::Display for Datestamp {
+impl Datestamp {
+    pub fn distance_from(&self, other: &Self) -> usize {
+        if self > other {
+            return other.distance_from(self)
+        }
+
+        // at target?
+        if self.year == other.year && self.month == other.month && self.day == other.day {
+            0
+        }
+        // advance days?
+        else if self.year == other.year && self.month == other.month {
+            (other.day - self.day) as usize
+        } else if self.year == other.year {
+            todo!()
+        } else {
+            todo!()
+        }
+    }
+}
+*/
+
+/*impl std::fmt::Display for Datestamp {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
+        write!(f, "{:04}-{:02}-{:02}", self.0.year, self.month, self.day)
     }
 }
 
@@ -45,7 +70,7 @@ impl std::fmt::Debug for Datestamp {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "Datestamp ({self})")
     }
-}
+}*/
 
 #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
 pub struct Change {
@@ -304,6 +329,8 @@ impl Hoard {
                     .push(txn_ref.clone());
             }
         }
+
+        self.account_ledger_data.values_mut().for_each(|v| v.sort_by_key(|t| t.borrow().datestamp));
     }
 
     pub fn all_raw_ledger_data(&self) -> &[RawLedgerEntry] {

+ 1 - 5
src/data/parse.rs

@@ -32,11 +32,7 @@ fn ledger_parser<'a>() -> impl Parser<
 
     let datestamp =
         group((int, just('-').ignored(), int, just('-').ignored(), int)).map(|(y, _, m, _, d)| {
-            Datestamp {
-                year: y as u16,
-                month: m as u8,
-                day: d as u8,
-            }
+            Datestamp::from_ymd_opt(y as i32, m as u32, d as u32).expect("parse error: date format did not match")
         });
 
     let decimal_digit = one_of("0123456789.,");

+ 1 - 18
src/import.rs

@@ -24,12 +24,6 @@ impl From<csv::Error> for ImportError {
     }
 }
 
-impl From<strptime::ParseError> for ImportError {
-    fn from(value: strptime::ParseError) -> Self {
-        Self::InputError(value.to_string())
-    }
-}
-
 impl From<rust_decimal::Error> for ImportError {
     fn from(value: rust_decimal::Error) -> Self {
         Self::InputError(value.to_string())
@@ -58,8 +52,6 @@ fn import_from_csv(
         .trim(csv::Trim::All)
         .from_reader(reader);
 
-    // let mut csv_reader = csv::Reader::from_reader(reader);
-
     // validate CSV spec
     if !csv_spec
         .cols
@@ -79,10 +71,6 @@ fn import_from_csv(
         ));
     }
 
-    // strptime is silly and wants a &'static format string
-    let date_format = Box::leak(csv_spec.date_format.clone().into_boxed_str());
-    let date_parser = strptime::Parser::new(date_format);
-
     let mut txns = vec![];
 
     for record in csv_reader.records().skip(csv_spec.skip_start.unwrap_or(0)) {
@@ -98,12 +86,7 @@ fn import_from_csv(
             match spec {
                 data::spec::CsvColumnSpec::Ignore => (),
                 data::spec::CsvColumnSpec::Datestamp => {
-                    let date = date_parser.parse(record)?.date()?;
-                    txn_datestamp = Some(data::Datestamp {
-                        year: date.year() as u16,
-                        month: date.month(),
-                        day: date.day(),
-                    });
+                    txn_datestamp = data::Datestamp::parse_from_str(record, &csv_spec.date_format).ok();
                 }
                 data::spec::CsvColumnSpec::Title => {
                     txn_title = Some(record.into());

+ 0 - 16
src/show.rs

@@ -159,22 +159,6 @@ impl TransactionTable {
             .into_iter()
             .chain(
                 txns.filter_map(|txn| self.format_txn(root, account, txn))
-                /*txns.filter_map(|txn| txn.change_for(account).map(|chg| (txn, chg)))
-                    .map(|(txn, chg)| {
-                        let precision = root
-                            .and_then(|r| r.unit_spec(*chg.unit))
-                            .and_then(|v| v.precision)
-                            .unwrap_or(2) as usize;
-                        Row::Data(vec![
-                            txn.datestamp.to_string(),
-                            txn.title.clone().unwrap_or_else(String::new),
-                            format!("{:.precision$}", chg.amount),
-                            chg.balance
-                                .as_deref()
-                                .map(|b| format!("{:.precision$}", b))
-                                .unwrap_or(String::new()),
-                        ])
-                    }),*/
             ),
         )
     }