Bläddra i källkod

Refactoring to remove ledger module from data.

Kestrel 2 veckor sedan
förälder
incheckning
6e1840f8bb
14 ändrade filer med 474 tillägg och 453 borttagningar
  1. 10 9
      src/check.rs
  2. 41 28
      src/cmd.rs
  3. 36 18
      src/cmd/infer.rs
  4. 113 175
      src/data.rs
  5. 0 2
      src/data/account.rs
  6. 9 8
      src/data/format.rs
  7. 0 99
      src/data/ledger.rs
  8. 6 6
      src/data/parse.rs
  9. 0 38
      src/data/root.rs
  10. 27 0
      src/error.rs
  11. 67 50
      src/import.rs
  12. 133 0
      src/io.rs
  13. 13 6
      src/main.rs
  14. 19 14
      src/show.rs

+ 10 - 9
src/check.rs

@@ -3,7 +3,8 @@ use std::collections::BTreeMap;
 use ariadne::Span as ASpan;
 use chumsky::span::Span as CSpan;
 
-use crate::data::{DataError, Decimal, Root, Span, Spanned, UnitName};
+// use crate::data::{DataError, Decimal, Hoard, Span, Spanned, UnitName};
+use crate::prelude::*;
 
 #[derive(Default, PartialEq, PartialOrd)]
 pub enum CheckLevel {
@@ -14,7 +15,7 @@ pub enum CheckLevel {
     Consistent,
 }
 
-fn check_equal_sum(root: &Root) -> Result<(), DataError> {
+fn check_equal_sum(root: &data::Hoard) -> Result<(), DataError> {
     log::trace!("Checking for equal sums in monounit ledger entries...");
 
     for le in root.all_ledger_data() {
@@ -26,8 +27,8 @@ fn check_equal_sum(root: &Root) -> Result<(), DataError> {
         let net = tx
             .changes
             .iter()
-            .try_fold(Decimal::ZERO, |acc, b| acc.checked_add(*b.amount));
-        if net != Some(Decimal::ZERO) {
+            .try_fold(data::Decimal::ZERO, |acc, b| acc.checked_add(*b.amount));
+        if net != Some(data::Decimal::ZERO) {
             let report = ariadne::Report::build(ariadne::ReportKind::Error, tx.span()).with_labels(
                 tx.changes.iter().map(|v| {
                     let span = v.amount.span().union(v.unit.span());
@@ -50,7 +51,7 @@ fn check_equal_sum(root: &Root) -> Result<(), DataError> {
     Ok(())
 }
 
-fn check_precision(root: &Root) -> Result<(), DataError> {
+fn check_precision(root: &data::Hoard) -> Result<(), DataError> {
     log::trace!("Checking for precision errors in ledger entries...");
 
     for le in root.all_ledger_data() {
@@ -104,7 +105,7 @@ fn check_precision(root: &Root) -> Result<(), DataError> {
     Ok(())
 }
 
-fn check_balances(root: &Root) -> Result<(), DataError> {
+fn check_balances(root: &data::Hoard) -> Result<(), DataError> {
     log::trace!("Checking balance consistency...");
 
     for account in root.account_names() {
@@ -112,12 +113,12 @@ fn check_balances(root: &Root) -> Result<(), DataError> {
             continue;
         };
 
-        let mut running_balance = BTreeMap::<UnitName, Spanned<Decimal>>::new();
+        let mut running_balance = BTreeMap::<data::UnitName, Spanned<data::Decimal>>::new();
         for txn in ledger {
             let change = txn.change_for(account).unwrap();
             let bal = running_balance
                 .entry(*change.unit)
-                .or_insert_with(|| Spanned::new(Decimal::default(), Span::default()));
+                .or_insert_with(|| Spanned::new(data::Decimal::default(), io::Span::default()));
             let last_span = bal.span();
             bal.0 = bal.checked_add(*change.amount).unwrap();
             bal.1 = change.1;
@@ -146,7 +147,7 @@ fn check_balances(root: &Root) -> Result<(), DataError> {
     Ok(())
 }
 
-pub fn run_checks(root: &mut Root, level: CheckLevel) -> Result<(), DataError> {
+pub fn run_checks(root: &mut data::Hoard, level: CheckLevel) -> Result<(), DataError> {
     if level >= CheckLevel::WellFormed {
         log::debug!("Running transaction well-formedness checks...");
         check_precision(root)?;

+ 41 - 28
src/cmd.rs

@@ -2,7 +2,8 @@ use std::collections::HashSet;
 
 use itertools::Itertools;
 
-use crate::{check::CheckLevel, data, import::import_from, io, show};
+// use crate::{check::CheckLevel, data, import::import_from, io, show};
+use crate::prelude::*;
 
 mod infer;
 
@@ -23,17 +24,16 @@ pub struct Invocation {
 fn load_data(
     fsdata: &mut io::FilesystemData,
     invocation: &Invocation,
-    level: CheckLevel,
-) -> anyhow::Result<data::Root> {
+    level: check::CheckLevel,
+) -> anyhow::Result<data::Hoard> {
     let path = std::fs::canonicalize(&invocation.file)?;
-    match data::Root::load(fsdata, &path, level) {
+    match data::Hoard::load(fsdata, &path, level) {
         Ok(data) => Ok(data),
-        Err(data::DataError::IOError(ioerror)) => Err(ioerror.into()),
-        Err(data::DataError::Report(report)) => {
+        Err(DataError::IOError(ioerror)) => Err(ioerror.into()),
+        Err(DataError::Report(report)) => {
             report.eprint(fsdata)?;
             Err(anyhow::anyhow!("Error reported"))
         }
-        Err(data::DataError::Validation(verr)) => Err(anyhow::anyhow!("Validation error: {verr}")),
     }
 }
 
@@ -55,11 +55,11 @@ pub enum Command {
     },
     Infer,
     Match {
-        account: data::AccountName
+        account: data::AccountName,
     },
 }
 
-fn summarize(data: &data::Root) {
+fn summarize(data: &data::Hoard) {
     let positive = console::Style::new().green();
     let negative = console::Style::new().red();
     let neutral = console::Style::new();
@@ -114,25 +114,31 @@ impl Command {
 
                 let tt = show::TransactionTable::default();
                 if let Some(ld) = data.ledger_data_for(aname) {
-                    tt.show(Some(&data), aname, ld.iter().map(data::Spanned::as_ref));
+                    tt.show(Some(&data), aname, ld.iter().map(io::Spanned::as_ref));
                 } else {
                     log::error!("account not found!");
                 }
             }
             Self::Reformat { skip_ids } => {
-                let mut data = load_data(&mut fsdata, inv, CheckLevel::WellFormed)?;
+                let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
                 if !skip_ids {
                     let mut used_ids = HashSet::new();
                     for le in data.all_ledger_data() {
-                        let Some(txn) = le.as_transaction() else { continue };
-                        let Some(id) = txn.get_annotation("id") else { continue };
+                        let Some(txn) = le.as_transaction() else {
+                            continue;
+                        };
+                        let Some(id) = txn.get_annotation("id") else {
+                            continue;
+                        };
                         used_ids.insert(id.to_owned());
                     }
 
                     // assign IDs if to transactions lacking them
                     for le in data.all_ledger_data_mut() {
-                        let Some(txn) = le.as_transaction_mut() else { continue };
+                        let Some(txn) = le.as_transaction_mut() else {
+                            continue;
+                        };
                         if !txn.get_annotation("id").is_some() {
                             // generated unique ID
                             let mut id = nanoid::nanoid!(10);
@@ -145,7 +151,7 @@ impl Command {
                     }
                 }
 
-                data::ledger::print_ledger(&mut fsdata, &data, data.all_ledger_data().iter())?;
+                data::format_ledger(&mut fsdata, &data, data.all_ledger_data().iter())?;
             }
             Self::Import {
                 account,
@@ -162,21 +168,22 @@ impl Command {
 
                 let default_destination = data.spec_root().placeholder_account;
 
-                let imported = import_from(aspec, aname, default_destination, from.as_path()).unwrap();
+                let imported =
+                    import::import_from(aspec, aname, default_destination, from.as_path()).unwrap();
                 log::info!("Imported {} transaction(s)", imported.len());
 
                 if let Some(target) = target {
                     let new_source = std::fs::canonicalize(target.as_os_str())?
                         .into_os_string()
                         .into();
-                    let span = data::Span::null_for_file(new_source);
+                    let span = io::Span::null_for_file(new_source);
 
                     let new_data = imported
                         .into_iter()
-                        .map(|txn| data::ledger::LedgerEntry::Transaction(data::Spanned(txn, span)))
+                        .map(|txn| data::LedgerEntry::Transaction(io::Spanned(txn, span)))
                         .collect_vec();
 
-                    data::ledger::print_ledger(
+                    data::format_ledger(
                         &mut fsdata,
                         &data,
                         data.ledger_data_from(new_source).chain(new_data.iter()),
@@ -188,25 +195,31 @@ impl Command {
                 }
             }
             Self::Infer => {
-                let mut data = load_data(&mut fsdata, inv, CheckLevel::WellFormed)?;
+                let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
                 if infer::do_inference(&mut data)? {
-                    data::ledger::print_ledger(&mut fsdata, &data, data.all_ledger_data().iter())?;
+                    data::format_ledger(&mut fsdata, &data, data.all_ledger_data().iter())?;
                 }
             }
             Self::Match { account } => {
-                let data = load_data(&mut fsdata, inv, CheckLevel::WellFormed)?;
+                let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
                 let Some(acc_data) = data.ledger_data_for(*account) else {
                     log::error!("No ledger data available for account {account}");
-                    return Ok(())
+                    return Ok(());
                 };
 
-                let txs = acc_data.iter().filter(|v| v.get_annotation("id").is_some()).sorted().collect_vec();
-
-                
-
-                show::TransactionTable::default().show(Some(&data), *account, txs.iter().map(|v| &v.0));
+                let txs = acc_data
+                    .iter()
+                    .filter(|v| v.get_annotation("id").is_some())
+                    .sorted()
+                    .collect_vec();
+
+                show::TransactionTable::default().show(
+                    Some(&data),
+                    *account,
+                    txs.iter().map(|v| &v.0),
+                );
             }
         }
         Ok(())

+ 36 - 18
src/cmd/infer.rs

@@ -1,7 +1,7 @@
-use crate::data::Root;
+use crate::data::Hoard;
 use crate::show::show_transaction;
 
-pub fn do_inference(data: &mut Root) -> anyhow::Result<bool> {
+pub fn do_inference(data: &mut Hoard) -> anyhow::Result<bool> {
     let placeholder = data.spec_root().placeholder_account;
 
     let num_txns = data.ledger_data_for(placeholder).unwrap().len();
@@ -20,7 +20,7 @@ pub fn do_inference(data: &mut Root) -> anyhow::Result<bool> {
 
         if chg.next().is_some() {
             log::debug!("Skipping transaction with more than two changes");
-            continue
+            continue;
         }
 
         let other_account = *complementary_change.account;
@@ -33,40 +33,58 @@ pub fn do_inference(data: &mut Root) -> anyhow::Result<bool> {
 
         for other_txn in other_txns {
             if other_txn.title.is_none() || other_txn.title != txn_title || other_txn == txn {
-                continue
+                continue;
             }
 
             // get other_txn's other
             let (_, mut chg) = other_txn.split_changes(other_account).unwrap();
             let candidate = chg.next().unwrap();
             if chg.next().is_some() || *candidate.account == placeholder {
-                continue
+                continue;
             }
-            log::debug!("possible candidate {}, titles are {:?} and {:?}", candidate.account, other_txn.title, txn.title);
+            log::debug!(
+                "possible candidate {}, titles are {:?} and {:?}",
+                candidate.account,
+                other_txn.title,
+                txn.title
+            );
 
             show_transaction(None, txn.as_ref());
 
-            println!("Candidate account: {} (Y/n)", console::style(candidate.account).red());
+            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.0.clone();
-                    println!("    Changing to account {} ...", console::style(new_account).green());
+                    println!(
+                        "    Changing to account {} ...",
+                        console::style(new_account).green()
+                    );
 
                     drop(chg);
 
-                    data.ledger_data_for_mut(placeholder).unwrap()[idx].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}");
-                        }
-                    });
+                    data.ledger_data_for_mut(placeholder).unwrap()[idx]
+                        .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
+
+                    break;
                 }
-                | Some('n') => (),
+                Some('n') => (),
                 c => println!("unknown {c:?}"),
             }
         }

+ 113 - 175
src/data.rs

@@ -1,18 +1,22 @@
 use std::collections::HashMap;
 
+use itertools::Itertools;
+
 use ariadne::Cache;
 use chumsky::span::Span as _;
 
 pub use rust_decimal::Decimal;
 
-use crate::{
-    check::CheckLevel,
-    io::{FilesystemData, SourceFile},
-};
+use crate::prelude::*;
 
-pub mod ledger;
 pub mod spec;
 
+mod parse;
+pub use parse::parse_ledger;
+
+mod format;
+pub use format::format_ledger;
+
 pub struct UnitTag;
 impl stringstore::NamespaceTag for UnitTag {
     const PREFIX: &'static str = "unit";
@@ -25,201 +29,132 @@ impl stringstore::NamespaceTag for AccountTag {
 }
 pub type AccountName = stringstore::StoredString<AccountTag>;
 
-#[derive(Debug)]
-pub enum DataError {
-    IOError(std::io::Error),
-    Report(Box<ariadne::Report<'static, Span>>),
-    Validation(String),
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Datestamp {
+    pub year: u16,
+    pub month: u8,
+    pub day: u8,
 }
 
-impl std::fmt::Display for DataError {
+impl std::fmt::Display for Datestamp {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        <Self as std::fmt::Debug>::fmt(self, f)
+        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
     }
 }
 
-impl From<std::io::Error> for DataError {
-    fn from(value: std::io::Error) -> Self {
-        Self::IOError(value)
+impl std::fmt::Debug for Datestamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Datestamp ({self})")
     }
 }
 
-impl From<ariadne::Report<'static, Span>> for DataError {
-    fn from(value: ariadne::Report<'static, Span>) -> Self {
-        Self::Report(value.into())
-    }
+#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
+pub struct Change {
+    pub account: Spanned<AccountName>,
+    pub amount: Spanned<Decimal>,
+    pub balance: Option<Spanned<Decimal>>,
+    pub unit: Spanned<UnitName>,
 }
 
-impl std::error::Error for DataError {}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Span {
-    range: (usize, usize),
-    context: Option<SourceFile>,
+#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
+pub struct Transaction {
+    pub datestamp: Datestamp,
+    pub title: Option<String>,
+    pub annotations: Vec<String>,
+    pub changes: Vec<Spanned<Change>>,
 }
 
-impl Span {
-    pub fn null_for_file(source: SourceFile) -> Self {
-        Self {
-            range: (usize::MAX, usize::MAX),
-            context: Some(source),
-        }
+impl Transaction {
+    pub fn modifies(&self, account: AccountName) -> bool {
+        self.changes.iter().any(|b| b.account.as_ref() == &account)
     }
-}
 
-impl Default for Span {
-    fn default() -> Self {
-        Self {
-            range: (0, 0),
-            context: None,
-        }
+    pub fn change_for(&self, account: AccountName) -> Option<&Spanned<Change>> {
+        self.changes.iter().find(|b| b.account.as_ref() == &account)
     }
-}
-
-impl chumsky::span::Span for Span {
-    type Offset = usize;
-    type Context = SourceFile;
 
-    fn new(context: Self::Context, range: std::ops::Range<Self::Offset>) -> Self {
-        Self {
-            context: Some(context),
-            range: (range.start, range.end),
-        }
-    }
-
-    fn start(&self) -> Self::Offset {
-        self.range.0
+    pub fn split_changes(
+        &self,
+        account: AccountName,
+    ) -> Option<(&Spanned<Change>, impl Iterator<Item = &Spanned<Change>>)> {
+        let index = self
+            .changes
+            .iter()
+            .position(|b| b.account.as_ref() == &account)?;
+        Some((
+            &self.changes[index],
+            self.changes[0..index]
+                .iter()
+                .chain(self.changes[index + 1..].iter()),
+        ))
     }
 
-    fn end(&self) -> Self::Offset {
-        self.range.1
+    pub fn is_mono_unit(&self) -> bool {
+        self.changes.iter().unique_by(|b| *b.unit).count() == 1
     }
 
-    fn context(&self) -> Self::Context {
-        self.context.unwrap()
+    pub fn mono_unit(&self) -> Option<UnitName> {
+        let mut it = self.changes.iter().unique_by(|b| *b.unit);
+        let uniq = it.next()?;
+        it.next().is_none().then_some(*uniq.unit)
     }
 
-    fn to_end(&self) -> Self {
-        Self {
-            context: self.context,
-            range: (self.range.1, self.range.1),
+    pub fn get_annotation(&self, label: &str) -> Option<&str> {
+        for anno in self.annotations.iter() {
+            if let Some(body) = anno.strip_prefix(label) {
+                return Some(body);
+            }
         }
+        None
     }
 }
 
-impl ariadne::Span for Span {
-    type SourceId = SourceFile;
-    fn source(&self) -> &Self::SourceId {
-        self.context.as_ref().unwrap()
-    }
-    fn start(&self) -> usize {
-        self.range.0
-    }
-    fn end(&self) -> usize {
-        self.range.1
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct Spanned<T>(pub T, pub Span);
-
-impl<T> Spanned<T> {
-    pub fn new(t: T, span: Span) -> Self {
-        Self(t, span)
-    }
-
-    pub fn span(&self) -> Span {
-        self.1
-    }
-}
-
-impl<T> std::ops::Deref for Spanned<T> {
-    type Target = T;
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum LedgerEntry {
+    Transaction(Spanned<Transaction>),
+    Comment(Spanned<String>),
 }
 
-impl<T> std::ops::DerefMut for Spanned<T> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.0
-    }
-}
-
-impl<T> AsRef<T> for Spanned<T> {
-    fn as_ref(&self) -> &T {
-        &self.0
-    }
-}
-
-impl<T: PartialEq> PartialEq for Spanned<T> {
-    fn eq(&self, other: &Self) -> bool {
-        self.0.eq(&other.0)
-    }
-}
-
-impl<T: Eq> Eq for Spanned<T> {}
-
-impl<T> From<T> for Spanned<T> {
-    fn from(value: T) -> Self {
-        Self(value, Span::default())
-    }
-}
-
-impl<T: PartialOrd> PartialOrd for Spanned<T> {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        self.0.partial_cmp(&other.0)
-    }
-}
-
-impl<T: Ord> Ord for Spanned<T> {
-    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-        self.0.cmp(&other.0)
-    }
-}
-
-impl<T: std::fmt::Display> std::fmt::Display for Spanned<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
+impl LedgerEntry {
+    pub fn as_transaction(&self) -> Option<&Spanned<Transaction>> {
+        match self {
+            Self::Transaction(tx) => Some(tx),
+            _ => None,
+        }
     }
-}
 
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Datestamp {
-    pub year: u16,
-    pub month: u8,
-    pub day: u8,
-}
-
-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)
+    pub fn as_transaction_mut(&mut self) -> Option<&mut Spanned<Transaction>> {
+        match self {
+            Self::Transaction(tx) => Some(tx),
+            _ => None,
+        }
     }
-}
 
-impl std::fmt::Debug for Datestamp {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "Datestamp ({self})")
+    pub fn span(&self) -> io::Span {
+        match self {
+            Self::Transaction(ts) => ts.span(),
+            Self::Comment(c) => c.span(),
+        }
     }
 }
 
 #[derive(Debug)]
-pub struct Root {
+pub struct Hoard {
     path: std::path::PathBuf,
     spec_root: spec::SpecRoot,
 
-    ledger_data: Vec<ledger::LedgerEntry>,
+    ledger_data: Vec<LedgerEntry>,
 
-    account_ledger_data: HashMap<AccountName, Vec<Spanned<ledger::Transaction>>>,
+    account_ledger_data: HashMap<AccountName, Vec<Spanned<Transaction>>>,
 }
 
-impl Root {
+impl Hoard {
     pub fn load(
-        fsdata: &mut FilesystemData,
+        fsdata: &mut io::FilesystemData,
         path: &std::path::Path,
-        check_level: CheckLevel,
+        check_level: check::CheckLevel,
     ) -> Result<Self, DataError> {
-        let sf = SourceFile::new(path.as_os_str());
+        let sf = io::SourceFile::new(path.as_os_str());
         let root_data = fsdata.fetch(&sf).unwrap();
 
         match toml::from_str::<spec::SpecRoot>(root_data.text()) {
@@ -245,9 +180,11 @@ impl Root {
 
                 let report = ariadne::Report::build(
                     ariadne::ReportKind::Error,
-                    Span::new(sf, range.clone()),
+                    io::Span::new(sf, range.clone()),
+                )
+                .with_label(
+                    ariadne::Label::new(io::Span::new(sf, range)).with_message(te.message()),
                 )
-                .with_label(ariadne::Label::new(Span::new(sf, range)).with_message(te.message()))
                 .with_message("Failed to parse root TOML")
                 .finish();
 
@@ -258,7 +195,7 @@ impl Root {
 
     fn load_ledger(
         &mut self,
-        fsdata: &mut FilesystemData,
+        fsdata: &mut io::FilesystemData,
         path: &mut std::path::PathBuf,
     ) -> Result<(), DataError> {
         log::debug!("Loading ledger data from {}", path.display());
@@ -286,19 +223,22 @@ impl Root {
                 return Ok(());
             }
 
-            let sf = SourceFile::new_from_string(path.into_os_string());
+            let sf = io::SourceFile::new_from_string(path.into_os_string());
             if let Ok(data) = fsdata.fetch(&sf) {
                 self.ledger_data
-                    .extend(ledger::parse_ledger(sf, &self.spec_root, data.text())?);
+                    .extend(parse_ledger(sf, &self.spec_root, data.text())?);
             } else {
-                log::error!("Failed to load data from {}", std::path::Path::new(sf.as_str()).display());
+                log::error!(
+                    "Failed to load data from {}",
+                    std::path::Path::new(sf.as_str()).display()
+                );
             }
         }
 
         Ok(())
     }
 
-    fn load_ledgers(&mut self, fsdata: &mut FilesystemData) -> Result<(), DataError> {
+    fn load_ledgers(&mut self, fsdata: &mut io::FilesystemData) -> Result<(), DataError> {
         let mut ledger_path = std::fs::canonicalize(self.path.as_path())?;
         ledger_path.pop();
         ledger_path.push(&self.spec_root.ledger_path);
@@ -311,7 +251,7 @@ impl Root {
 
     fn preprocess_ledger_data(&mut self) {
         for entry in &self.ledger_data {
-            let ledger::LedgerEntry::Transaction(tx) = &entry else {
+            let LedgerEntry::Transaction(tx) = &entry else {
                 continue;
             };
             for bal in &tx.changes {
@@ -321,32 +261,30 @@ impl Root {
                     .push(tx.clone());
             }
         }
-
-        /*for txns in self.account_ledger_data.values_mut() {
-            txns.sort_by_key(|txn| txn.datestamp);
-        }*/
     }
 
-    pub fn all_ledger_data(&self) -> &[ledger::LedgerEntry] {
+    pub fn all_ledger_data(&self) -> &[LedgerEntry] {
         self.ledger_data.as_slice()
     }
 
-    pub fn all_ledger_data_mut(&mut self) -> &mut [ledger::LedgerEntry] {
+    pub fn all_ledger_data_mut(&mut self) -> &mut [LedgerEntry] {
         self.ledger_data.as_mut_slice()
     }
 
-    pub fn ledger_data_for(&self, aname: AccountName) -> Option<&[Spanned<ledger::Transaction>]> {
+    pub fn ledger_data_for(&self, aname: AccountName) -> Option<&[Spanned<Transaction>]> {
         self.account_ledger_data.get(&aname).map(Vec::as_slice)
     }
 
-    pub fn ledger_data_for_mut(&mut self, aname: AccountName) -> Option<&mut [Spanned<ledger::Transaction>]> {
-        self.account_ledger_data.get_mut(&aname).map(Vec::as_mut_slice)
+    pub fn ledger_data_for_mut(
+        &mut self,
+        aname: AccountName,
+    ) -> Option<&mut [Spanned<Transaction>]> {
+        self.account_ledger_data
+            .get_mut(&aname)
+            .map(Vec::as_mut_slice)
     }
 
-    pub fn ledger_data_from(
-        &self,
-        source: SourceFile,
-    ) -> impl Iterator<Item = &ledger::LedgerEntry> {
+    pub fn ledger_data_from(&self, source: io::SourceFile) -> impl Iterator<Item = &LedgerEntry> {
         self.all_ledger_data()
             .iter()
             .filter(move |le| le.span().context == Some(source))

+ 0 - 2
src/data/account.rs

@@ -1,2 +0,0 @@
-#[derive(Debug)]
-pub struct AccountData {}

+ 9 - 8
src/data/ledger/print.rs → src/data/format.rs

@@ -2,13 +2,14 @@ use std::collections::BTreeMap;
 
 use itertools::Itertools;
 
-use crate::data::{Root, SourceFile, Span, Spanned};
+use crate::data::Hoard;
+use crate::io::{SourceFile, Span, Spanned};
 
 use super::{LedgerEntry, Transaction};
 
-fn print_transaction(
+fn format_transaction(
     target: &mut dyn std::io::Write,
-    root: &Root,
+    root: &Hoard,
     tx: &Transaction,
     padding: usize,
 ) -> std::io::Result<()> {
@@ -62,13 +63,13 @@ fn print_transaction(
     writeln!(target)
 }
 
-fn print_comment(target: &mut dyn std::io::Write, c: &Spanned<String>) -> std::io::Result<()> {
+fn format_comment(target: &mut dyn std::io::Write, c: &Spanned<String>) -> std::io::Result<()> {
     writeln!(target, "{c}")
 }
 
-pub fn print_ledger<'l>(
+pub fn format_ledger<'l>(
     fsdata: &mut crate::io::FilesystemData,
-    root: &Root,
+    root: &Hoard,
     entries: impl Iterator<Item = &'l LedgerEntry>,
 ) -> std::io::Result<()> {
     let mut ordering = BTreeMap::<Option<SourceFile>, BTreeMap<Span, Vec<&LedgerEntry>>>::new();
@@ -104,9 +105,9 @@ pub fn print_ledger<'l>(
             for le in les {
                 match le {
                     LedgerEntry::Transaction(tx) => {
-                        print_transaction(&mut outfile, root, tx, padding)?
+                        format_transaction(&mut outfile, root, tx, padding)?
                     }
-                    LedgerEntry::Comment(c) => print_comment(&mut outfile, c)?,
+                    LedgerEntry::Comment(c) => format_comment(&mut outfile, c)?,
                 }
             }
         }

+ 0 - 99
src/data/ledger.rs

@@ -1,99 +0,0 @@
-use itertools::Itertools;
-
-use super::{AccountName, Datestamp, Decimal, Spanned, UnitName};
-
-mod parse;
-pub use parse::parse_ledger;
-
-mod print;
-pub use print::print_ledger;
-
-#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
-pub struct Change {
-    pub account: Spanned<AccountName>,
-    pub amount: Spanned<Decimal>,
-    pub balance: Option<Spanned<Decimal>>,
-    pub unit: Spanned<UnitName>,
-}
-
-#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
-pub struct Transaction {
-    pub datestamp: Datestamp,
-    pub title: Option<String>,
-    pub annotations: Vec<String>,
-    pub changes: Vec<Spanned<Change>>,
-}
-
-impl Transaction {
-    pub fn modifies(&self, account: AccountName) -> bool {
-        self.changes.iter().any(|b| b.account.as_ref() == &account)
-    }
-
-    pub fn change_for(&self, account: AccountName) -> Option<&Spanned<Change>> {
-        self.changes.iter().find(|b| b.account.as_ref() == &account)
-    }
-
-    pub fn split_changes(
-        &self,
-        account: AccountName,
-    ) -> Option<(&Spanned<Change>, impl Iterator<Item = &Spanned<Change>>)> {
-        let index = self
-            .changes
-            .iter()
-            .position(|b| b.account.as_ref() == &account)?;
-        Some((
-            &self.changes[index],
-            self.changes[0..index]
-                .iter()
-                .chain(self.changes[index + 1..].iter()),
-        ))
-    }
-
-    pub fn is_mono_unit(&self) -> bool {
-        self.changes.iter().unique_by(|b| *b.unit).count() == 1
-    }
-
-    pub fn mono_unit(&self) -> Option<UnitName> {
-        let mut it = self.changes.iter().unique_by(|b| *b.unit);
-        let uniq = it.next()?;
-        it.next().is_none().then_some(*uniq.unit)
-    }
-
-    pub fn get_annotation(&self, label: &str) -> Option<&str> {
-        for anno in self.annotations.iter() {
-            if let Some(body) = anno.strip_prefix(label) {
-                return Some(body)
-            }
-        }
-        None
-    }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub enum LedgerEntry {
-    Transaction(Spanned<Transaction>),
-    Comment(Spanned<String>),
-}
-
-impl LedgerEntry {
-    pub fn as_transaction(&self) -> Option<&Spanned<Transaction>> {
-        match self {
-            Self::Transaction(tx) => Some(tx),
-            _ => None,
-        }
-    }
-
-    pub fn as_transaction_mut(&mut self) -> Option<&mut Spanned<Transaction>> {
-        match self {
-            Self::Transaction(tx) => Some(tx),
-            _ => None,
-        }
-    }
-
-    pub fn span(&self) -> super::Span {
-        match self {
-            Self::Transaction(ts) => ts.span(),
-            Self::Comment(c) => c.span(),
-        }
-    }
-}

+ 6 - 6
src/data/ledger/parse.rs → src/data/parse.rs

@@ -1,19 +1,19 @@
-use crate::data::{
-    AccountName, DataError, Datestamp, Decimal, SourceFile, Span, Spanned, UnitName, spec::SpecRoot,
-};
+use crate::prelude::*;
+
+use data::{AccountName, DataError, Datestamp, Decimal, Spanned, UnitName, spec::SpecRoot};
 
 use super::{Change, LedgerEntry, Transaction};
 
 use chumsky::{prelude::*, text::inline_whitespace};
 
-type InputWithContext<'a> = chumsky::input::WithContext<Span, &'a str>;
+type InputWithContext<'a> = chumsky::input::WithContext<io::Span, &'a str>;
 
 fn ledger_parser<'a>() -> impl Parser<
     'a,
     InputWithContext<'a>,
     Vec<LedgerEntry>,
     chumsky::extra::Full<
-        chumsky::error::Rich<'a, char, Span>,
+        chumsky::error::Rich<'a, char, io::Span>,
         chumsky::extra::SimpleState<&'a SpecRoot>,
         (),
     >,
@@ -155,7 +155,7 @@ fn ledger_parser<'a>() -> impl Parser<
 }
 
 pub fn parse_ledger(
-    source: SourceFile,
+    source: io::SourceFile,
     spec: &SpecRoot,
     data: &str,
 ) -> Result<Vec<LedgerEntry>, DataError> {

+ 0 - 38
src/data/root.rs

@@ -1,38 +0,0 @@
-use std::collections::HashMap;
-
-#[derive(Debug, serde::Deserialize)]
-#[serde(deny_unknown_fields)]
-struct AccountSpec {
-    title: Option<String>,
-    description: Option<String>,
-
-    annotations: Option<HashMap<String, String>>,
-}
-
-#[derive(Debug, serde::Deserialize)]
-#[serde(deny_unknown_fields)]
-struct RootSpec {
-    accounts: HashMap<String, AccountSpec>,
-
-    account_data: HashMap<String, AccountData>,
-}
-
-pub struct Root {
-    path: std::path::PathBuf,
-    data: RootSpec,
-}
-
-impl Root {
-    pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
-        let content = std::fs::read_to_string(path)?;
-        let data = toml::from_str::<RootSpec>(&content).expect("could not parse root");
-        Ok(Self {
-            path: path.into(),
-            data,
-        })
-    }
-
-    pub fn iter_accounts(&self) -> impl Iterator<Item = (&str, &AccountSpec)> {
-        self.data.accounts.iter().map(|v| (v.0.as_str(), v.1))
-    }
-}

+ 27 - 0
src/error.rs

@@ -0,0 +1,27 @@
+use crate::prelude::*;
+
+#[derive(Debug)]
+pub enum DataError {
+    IOError(std::io::Error),
+    Report(Box<ariadne::Report<'static, io::Span>>),
+}
+
+impl std::fmt::Display for DataError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        <Self as std::fmt::Debug>::fmt(self, f)
+    }
+}
+
+impl From<std::io::Error> for DataError {
+    fn from(value: std::io::Error) -> Self {
+        Self::IOError(value)
+    }
+}
+
+impl From<ariadne::Report<'static, io::Span>> for DataError {
+    fn from(value: ariadne::Report<'static, io::Span>) -> Self {
+        Self::Report(value.into())
+    }
+}
+
+impl std::error::Error for DataError {}

+ 67 - 50
src/import.rs

@@ -1,10 +1,12 @@
-use std::collections::{HashMap,BTreeSet};
+use std::collections::BTreeSet;
 
-use crate::data::{
+use crate::prelude::*;
+
+/*use crate::data::{
     AccountName, Datestamp, Decimal, UnitName,
     ledger::{Change, Transaction},
     spec::{AccountSpec, CsvColumnSpec, CsvImportSpec, ImportFileFormat},
-};
+};*/
 
 #[derive(Debug)]
 pub enum ImportError {
@@ -38,35 +40,38 @@ impl From<rust_decimal::Error> for ImportError {
     }
 }
 
-fn try_parse_decimal(from: &str) -> Result<Decimal, ImportError> {
+fn try_parse_decimal(from: &str) -> Result<data::Decimal, ImportError> {
     // remove any '$' or units from the string
     let filtered = from
         .chars()
         .filter(|f| char::is_digit(*f, 10) || *f == '.' || *f == '-')
         .collect::<String>();
 
-    Decimal::from_str_radix(filtered.as_str(), 10).map_err(Into::into)
+    data::Decimal::from_str_radix(filtered.as_str(), 10).map_err(Into::into)
 }
 
 fn import_from_csv(
-    csv_spec: &CsvImportSpec,
-    aspec: &AccountSpec,
-    account: AccountName,
-    target: AccountName,
+    csv_spec: &data::spec::CsvImportSpec,
+    aspec: &data::spec::AccountSpec,
+    account: data::AccountName,
+    target: data::AccountName,
     reader: impl std::io::Read,
-) -> Result<Vec<Transaction>, ImportError> {
+) -> Result<Vec<data::Transaction>, ImportError> {
     let mut csv_reader = csv::Reader::from_reader(reader);
 
     // validate CSV spec
-    if !csv_spec.cols.contains(&CsvColumnSpec::Datestamp) {
+    if !csv_spec
+        .cols
+        .contains(&data::spec::CsvColumnSpec::Datestamp)
+    {
         return Err(ImportError::ConfigError(
             "CSV config does not have a datestamp column".into(),
         ));
     }
 
-    if !csv_spec.cols.contains(&CsvColumnSpec::Change)
-        && (!csv_spec.cols.contains(&CsvColumnSpec::Withdraw)
-            || !csv_spec.cols.contains(&CsvColumnSpec::Deposit))
+    if !csv_spec.cols.contains(&data::spec::CsvColumnSpec::Change)
+        && (!csv_spec.cols.contains(&data::spec::CsvColumnSpec::Withdraw)
+            || !csv_spec.cols.contains(&data::spec::CsvColumnSpec::Deposit))
     {
         return Err(ImportError::ConfigError(
             "CSV config needs either a change column or both withdraw and deposit columns!".into(),
@@ -82,34 +87,34 @@ fn import_from_csv(
     for record in csv_reader.records() {
         let record = record?;
 
-        let mut txn_datestamp: Option<Datestamp> = None;
+        let mut txn_datestamp: Option<data::Datestamp> = None;
         let mut txn_title: Option<String> = None;
-        let mut txn_change: Option<Decimal> = None;
-        let mut txn_balance: Option<Decimal> = None;
-        let mut txn_unit: Option<UnitName> = None;
+        let mut txn_change: Option<data::Decimal> = None;
+        let mut txn_balance: Option<data::Decimal> = None;
+        let mut txn_unit: Option<data::UnitName> = None;
 
         for (record, spec) in record.iter().zip(csv_spec.cols.iter()) {
             match spec {
-                CsvColumnSpec::Ignore => (),
-                CsvColumnSpec::Datestamp => {
+                data::spec::CsvColumnSpec::Ignore => (),
+                data::spec::CsvColumnSpec::Datestamp => {
                     let date = date_parser.parse(record)?.date()?;
-                    txn_datestamp = Some(Datestamp {
+                    txn_datestamp = Some(data::Datestamp {
                         year: date.year() as u16,
                         month: date.month(),
                         day: date.day(),
                     });
                 }
-                CsvColumnSpec::Title => {
+                data::spec::CsvColumnSpec::Title => {
                     txn_title = Some(record.into());
                 }
-                CsvColumnSpec::Deposit => {
+                data::spec::CsvColumnSpec::Deposit => {
                     if record.trim().is_empty() {
                         continue;
                     }
 
                     txn_change = Some(try_parse_decimal(record)?);
                 }
-                CsvColumnSpec::Withdraw => {
+                data::spec::CsvColumnSpec::Withdraw => {
                     if record.trim().is_empty() {
                         continue;
                     }
@@ -117,41 +122,41 @@ fn import_from_csv(
                     dec.set_sign_negative(true);
                     txn_change = Some(dec);
                 }
-                CsvColumnSpec::Change => {
+                data::spec::CsvColumnSpec::Change => {
                     if record.trim().is_empty() {
                         continue;
                     }
 
                     txn_change = Some(try_parse_decimal(record)?);
                 }
-                CsvColumnSpec::Balance => {
+                data::spec::CsvColumnSpec::Balance => {
                     if record.trim().is_empty() {
                         continue;
                     }
 
                     txn_balance = Some(try_parse_decimal(record)?);
                 }
-                CsvColumnSpec::Unit => {
-                    txn_unit = Some(UnitName::new(record));
+                data::spec::CsvColumnSpec::Unit => {
+                    txn_unit = Some(data::UnitName::new(record));
                 }
             }
         }
 
-        txns.push(Transaction {
+        txns.push(data::Transaction {
             datestamp: txn_datestamp.unwrap(),
             title: txn_title,
             annotations: vec![],
             changes: vec![
-                Change {
+                data::Change {
                     account: account.into(),
                     amount: txn_change.unwrap().into(),
                     balance: txn_balance.map(Into::into),
                     unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
                 }
                 .into(),
-                Change {
+                data::Change {
                     account: target.into(),
-                    amount: Decimal::ZERO
+                    amount: data::Decimal::ZERO
                         .checked_sub(txn_change.unwrap())
                         .unwrap()
                         .into(),
@@ -166,32 +171,35 @@ fn import_from_csv(
     Ok(txns)
 }
 
-fn recursive_order_search(txns: &[Transaction], account: AccountName, order: &mut Vec<usize>, remaining: &mut BTreeSet<usize>) -> bool {
+fn recursive_order_search(
+    txns: &[data::Transaction],
+    account: data::AccountName,
+    order: &mut Vec<usize>,
+    remaining: &mut BTreeSet<usize>,
+) -> bool {
     if remaining.is_empty() {
-        return true
+        return true;
     }
 
     let possibles = remaining.iter().cloned().collect::<Vec<_>>();
 
     if let Some(last) = order.last().as_deref() {
         let Some(last_balance) = txns[*last].change_for(account).unwrap().balance.as_deref() else {
-            return false
+            return false;
         };
         for possible in possibles.iter() {
             // check if balances line up
             let change = txns[*possible].change_for(account).unwrap();
-            let Some(nbal) = change.balance else {
-                continue
-            };
+            let Some(nbal) = change.balance else { continue };
 
             if last_balance.checked_add(*change.amount) != Some(*nbal) {
-                continue
+                continue;
             }
 
             remaining.remove(possible);
             order.push(*possible);
             if recursive_order_search(txns, account, order, remaining) {
-                return true
+                return true;
             }
             order.pop();
             remaining.insert(*possible);
@@ -201,7 +209,7 @@ fn recursive_order_search(txns: &[Transaction], account: AccountName, order: &mu
             remaining.remove(&possible);
             order.push(possible);
             if recursive_order_search(txns, account, order, remaining) {
-                return true
+                return true;
             }
             order.pop();
             remaining.insert(possible);
@@ -211,11 +219,15 @@ fn recursive_order_search(txns: &[Transaction], account: AccountName, order: &mu
     false
 }
 
-fn postprocess(account: AccountName, transactions: &mut Vec<Transaction>) {
+fn postprocess(account: data::AccountName, transactions: &mut Vec<data::Transaction>) {
     // check if we're sorted by datestamp already
     if transactions.is_sorted_by_key(|tx| tx.datestamp) {
         // already vaguely in the right order
-    } else if transactions.iter().rev().is_sorted_by_key(|tx| tx.datestamp) {
+    } else if transactions
+        .iter()
+        .rev()
+        .is_sorted_by_key(|tx| tx.datestamp)
+    {
         // reverse everything
         transactions.reverse();
     } else {
@@ -228,24 +240,29 @@ fn postprocess(account: AccountName, transactions: &mut Vec<Transaction>) {
 
     if !recursive_order_search(transactions, account, &mut order, &mut to_assign) {
         log::warn!("Unable to determine transaction ordering!");
-        return
+        return;
     }
 
-    let mut ntransact = order.iter().map(|v| transactions[*v].clone()).collect::<Vec<_>>();
+    let mut ntransact = order
+        .iter()
+        .map(|v| transactions[*v].clone())
+        .collect::<Vec<_>>();
 
     std::mem::swap(&mut ntransact, transactions);
 }
 
 pub fn import_from(
-    aspec: &AccountSpec,
-    account: AccountName,
-    target: AccountName,
+    aspec: &data::spec::AccountSpec,
+    account: data::AccountName,
+    target: data::AccountName,
     path: &std::path::Path,
-) -> Result<Vec<Transaction>, ImportError> {
+) -> Result<Vec<data::Transaction>, ImportError> {
     let reader = std::fs::File::open(path)?;
 
     let mut output = match &aspec.import {
-        Some(ImportFileFormat::Csv(csv)) => import_from_csv(csv, aspec, account, target, reader),
+        Some(data::spec::ImportFileFormat::Csv(csv)) => {
+            import_from_csv(csv, aspec, account, target, reader)
+        }
         None => Err(ImportError::ConfigError(format!(
             "no import configuration for {account}"
         ))),

+ 133 - 0
src/io.rs

@@ -7,6 +7,139 @@ impl stringstore::NamespaceTag for SourceTag {
 /// Canonicalized path to a file
 pub type SourceFile = stringstore::StoredOsString<SourceTag>;
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Span {
+    pub range: (usize, usize),
+    pub context: Option<SourceFile>,
+}
+
+impl Span {
+    pub fn null_for_file(source: SourceFile) -> Self {
+        Self {
+            range: (usize::MAX, usize::MAX),
+            context: Some(source),
+        }
+    }
+}
+
+impl Default for Span {
+    fn default() -> Self {
+        Self {
+            range: (0, 0),
+            context: None,
+        }
+    }
+}
+
+impl chumsky::span::Span for Span {
+    type Offset = usize;
+    type Context = SourceFile;
+
+    fn new(context: Self::Context, range: std::ops::Range<Self::Offset>) -> Self {
+        Self {
+            context: Some(context),
+            range: (range.start, range.end),
+        }
+    }
+
+    fn start(&self) -> Self::Offset {
+        self.range.0
+    }
+
+    fn end(&self) -> Self::Offset {
+        self.range.1
+    }
+
+    fn context(&self) -> Self::Context {
+        self.context.unwrap()
+    }
+
+    fn to_end(&self) -> Self {
+        Self {
+            context: self.context,
+            range: (self.range.1, self.range.1),
+        }
+    }
+}
+
+impl ariadne::Span for Span {
+    type SourceId = SourceFile;
+    fn source(&self) -> &Self::SourceId {
+        self.context.as_ref().unwrap()
+    }
+    fn start(&self) -> usize {
+        self.range.0
+    }
+    fn end(&self) -> usize {
+        self.range.1
+    }
+}
+
+/// Data structure with a [Span] describing location information
+#[derive(Debug, Clone, Copy)]
+pub struct Spanned<T>(pub T, pub Span);
+
+impl<T> Spanned<T> {
+    pub fn new(t: T, span: Span) -> Self {
+        Self(t, span)
+    }
+
+    pub fn span(&self) -> Span {
+        self.1
+    }
+}
+
+impl<T> std::ops::Deref for Spanned<T> {
+    type Target = T;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<T> std::ops::DerefMut for Spanned<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<T> AsRef<T> for Spanned<T> {
+    fn as_ref(&self) -> &T {
+        &self.0
+    }
+}
+
+impl<T: PartialEq> PartialEq for Spanned<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.0.eq(&other.0)
+    }
+}
+
+impl<T: Eq> Eq for Spanned<T> {}
+
+impl<T> From<T> for Spanned<T> {
+    fn from(value: T) -> Self {
+        Self(value, Span::default())
+    }
+}
+
+impl<T: PartialOrd> PartialOrd for Spanned<T> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.0.partial_cmp(&other.0)
+    }
+}
+
+impl<T: Ord> Ord for Spanned<T> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.0.cmp(&other.0)
+    }
+}
+
+impl<T: std::fmt::Display> std::fmt::Display for Spanned<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
 /// Helper for accessing data on the filesystem
 #[derive(Default)]
 pub struct FilesystemData {

+ 13 - 6
src/main.rs

@@ -1,9 +1,16 @@
-mod check;
-mod cmd;
-mod data;
-mod import;
-mod io;
-mod show;
+pub mod check;
+pub mod cmd;
+pub mod data;
+pub mod error;
+pub mod import;
+pub mod io;
+pub mod show;
+
+mod prelude {
+    pub use crate::error::DataError;
+    pub use crate::io::Spanned;
+    pub use crate::{check, data, import, io, show};
+}
 
 fn main() -> anyhow::Result<()> {
     use clap::Parser;

+ 19 - 14
src/show.rs

@@ -1,12 +1,7 @@
-use console::Style;
-
-use crate::data::{
-    AccountName, Decimal, Root,
-    ledger::{Change, Transaction},
-    spec::AccountSpec,
-};
+use crate::data::{AccountName, Hoard, Transaction};
 
 #[derive(Clone, Copy, Default)]
+#[allow(unused)]
 enum Align {
     #[default]
     Left,
@@ -19,7 +14,6 @@ struct Column {
     pub align: Align,
     pub left_border: bool,
     pub right_border: bool,
-    // pub contents: Box<dyn Iterator<Item = dyn Display>>,
 }
 
 enum Row {
@@ -73,14 +67,23 @@ fn show_table(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
     }
 }
 
-pub fn show_transaction(_root: Option<&Root>, txn: &Transaction) {
+pub fn show_transaction(_root: Option<&Hoard>, txn: &Transaction) {
     let bluestyle = console::Style::new().blue();
     // let greenstyle = console::Style::new().green();
     let yellowstyle = console::Style::new().yellow();
     let graystyle = console::Style::new().white().dim();
-    println!("{}: {}", bluestyle.apply_to(txn.datestamp), txn.title.as_deref().unwrap_or(""));
+    println!(
+        "{}: {}",
+        bluestyle.apply_to(txn.datestamp),
+        txn.title.as_deref().unwrap_or("")
+    );
     for change in &txn.changes {
-        println!(" - {}: {} {}", yellowstyle.apply_to(change.account), change.amount, graystyle.apply_to(change.unit));
+        println!(
+            " - {}: {} {}",
+            yellowstyle.apply_to(change.account),
+            change.amount,
+            graystyle.apply_to(change.unit)
+        );
     }
 }
 
@@ -90,7 +93,7 @@ pub struct TransactionTable {}
 impl TransactionTable {
     pub fn show<'d>(
         self,
-        root: Option<&Root>,
+        root: Option<&Hoard>,
         account: AccountName,
         txns: impl Iterator<Item = &'d Transaction>,
     ) {
@@ -132,8 +135,10 @@ impl TransactionTable {
             .chain(
                 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;
+                        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),