|
@@ -1,18 +1,22 @@
|
|
use std::collections::HashMap;
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
+use itertools::Itertools;
|
|
|
|
+
|
|
use ariadne::Cache;
|
|
use ariadne::Cache;
|
|
use chumsky::span::Span as _;
|
|
use chumsky::span::Span as _;
|
|
|
|
|
|
pub use rust_decimal::Decimal;
|
|
pub use rust_decimal::Decimal;
|
|
|
|
|
|
-use crate::{
|
|
|
|
- check::CheckLevel,
|
|
|
|
- io::{FilesystemData, SourceFile},
|
|
|
|
-};
|
|
|
|
|
|
+use crate::prelude::*;
|
|
|
|
|
|
-pub mod ledger;
|
|
|
|
pub mod spec;
|
|
pub mod spec;
|
|
|
|
|
|
|
|
+mod parse;
|
|
|
|
+pub use parse::parse_ledger;
|
|
|
|
+
|
|
|
|
+mod format;
|
|
|
|
+pub use format::format_ledger;
|
|
|
|
+
|
|
pub struct UnitTag;
|
|
pub struct UnitTag;
|
|
impl stringstore::NamespaceTag for UnitTag {
|
|
impl stringstore::NamespaceTag for UnitTag {
|
|
const PREFIX: &'static str = "unit";
|
|
const PREFIX: &'static str = "unit";
|
|
@@ -25,201 +29,132 @@ impl stringstore::NamespaceTag for AccountTag {
|
|
}
|
|
}
|
|
pub type AccountName = stringstore::StoredString<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 {
|
|
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)]
|
|
#[derive(Debug)]
|
|
-pub struct Root {
|
|
|
|
|
|
+pub struct Hoard {
|
|
path: std::path::PathBuf,
|
|
path: std::path::PathBuf,
|
|
spec_root: spec::SpecRoot,
|
|
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(
|
|
pub fn load(
|
|
- fsdata: &mut FilesystemData,
|
|
|
|
|
|
+ fsdata: &mut io::FilesystemData,
|
|
path: &std::path::Path,
|
|
path: &std::path::Path,
|
|
- check_level: CheckLevel,
|
|
|
|
|
|
+ check_level: check::CheckLevel,
|
|
) -> Result<Self, DataError> {
|
|
) -> 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();
|
|
let root_data = fsdata.fetch(&sf).unwrap();
|
|
|
|
|
|
match toml::from_str::<spec::SpecRoot>(root_data.text()) {
|
|
match toml::from_str::<spec::SpecRoot>(root_data.text()) {
|
|
@@ -245,9 +180,11 @@ impl Root {
|
|
|
|
|
|
let report = ariadne::Report::build(
|
|
let report = ariadne::Report::build(
|
|
ariadne::ReportKind::Error,
|
|
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")
|
|
.with_message("Failed to parse root TOML")
|
|
.finish();
|
|
.finish();
|
|
|
|
|
|
@@ -258,7 +195,7 @@ impl Root {
|
|
|
|
|
|
fn load_ledger(
|
|
fn load_ledger(
|
|
&mut self,
|
|
&mut self,
|
|
- fsdata: &mut FilesystemData,
|
|
|
|
|
|
+ fsdata: &mut io::FilesystemData,
|
|
path: &mut std::path::PathBuf,
|
|
path: &mut std::path::PathBuf,
|
|
) -> Result<(), DataError> {
|
|
) -> Result<(), DataError> {
|
|
log::debug!("Loading ledger data from {}", path.display());
|
|
log::debug!("Loading ledger data from {}", path.display());
|
|
@@ -286,19 +223,22 @@ impl Root {
|
|
return Ok(());
|
|
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) {
|
|
if let Ok(data) = fsdata.fetch(&sf) {
|
|
self.ledger_data
|
|
self.ledger_data
|
|
- .extend(ledger::parse_ledger(sf, &self.spec_root, data.text())?);
|
|
|
|
|
|
+ .extend(parse_ledger(sf, &self.spec_root, data.text())?);
|
|
} else {
|
|
} 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(())
|
|
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())?;
|
|
let mut ledger_path = std::fs::canonicalize(self.path.as_path())?;
|
|
ledger_path.pop();
|
|
ledger_path.pop();
|
|
ledger_path.push(&self.spec_root.ledger_path);
|
|
ledger_path.push(&self.spec_root.ledger_path);
|
|
@@ -311,7 +251,7 @@ impl Root {
|
|
|
|
|
|
fn preprocess_ledger_data(&mut self) {
|
|
fn preprocess_ledger_data(&mut self) {
|
|
for entry in &self.ledger_data {
|
|
for entry in &self.ledger_data {
|
|
- let ledger::LedgerEntry::Transaction(tx) = &entry else {
|
|
|
|
|
|
+ let LedgerEntry::Transaction(tx) = &entry else {
|
|
continue;
|
|
continue;
|
|
};
|
|
};
|
|
for bal in &tx.changes {
|
|
for bal in &tx.changes {
|
|
@@ -321,32 +261,30 @@ impl Root {
|
|
.push(tx.clone());
|
|
.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()
|
|
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()
|
|
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)
|
|
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()
|
|
self.all_ledger_data()
|
|
.iter()
|
|
.iter()
|
|
.filter(move |le| le.span().context == Some(source))
|
|
.filter(move |le| le.span().context == Some(source))
|