use crate::parser;
use std::cmp::Ordering;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[derive(Debug, Clone, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct Commodity {
pub name: String,
}
impl Commodity {
pub fn is_some(&self) -> bool {
!self.name.is_empty()
}
pub fn is_none(&self) -> bool {
self.name.is_empty()
}
}
impl Commodity {
pub fn from(name: String) -> Result<Commodity, Box<dyn Error>> {
if !parser::is_valid_id(&name) {
let msg = format!("This is not a valid commodity: '{name}'");
return Err(msg.into());
}
Ok(Commodity { name })
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Eq)]
pub struct AccountTreeNode {
pub(crate) depth: usize,
root: String,
pub(crate) parent: String,
pub(crate) account: String,
name: String,
}
impl AccountTreeNode {
pub(crate) fn is_root(&self) -> bool {
self.depth == 1
}
pub(crate) fn my_parent_is_root(&self) -> bool {
self.depth == 2
}
}
#[derive(Debug, Clone, Eq)]
pub struct TxnAccount {
pub(crate) atn: Arc<AccountTreeNode>,
pub(crate) comm: Arc<Commodity>,
}
impl Hash for TxnAccount {
fn hash<H: Hasher>(&self, state: &mut H) {
self.atn.account.hash(state);
self.comm.name.hash(state);
}
}
impl PartialEq for TxnAccount {
fn eq(&self, other: &Self) -> bool {
self.atn == other.atn && self.comm == other.comm
}
}
impl Ord for TxnAccount {
fn cmp(&self, other: &Self) -> Ordering {
match self.comm.cmp(&other.comm) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => self.atn.account.cmp(&other.atn.account),
}
}
}
impl PartialOrd for TxnAccount {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl TxnAccount {
pub(crate) fn is_parent_of(&self, atn: &TxnAccount) -> bool {
self.atn.account == atn.atn.parent && self.comm.name == atn.comm.name
}
pub(crate) fn is_root(&self) -> bool {
self.atn.is_root()
}
pub(crate) fn my_parent_is_root(&self) -> bool {
self.atn.my_parent_is_root()
}
}
impl Display for AccountTreeNode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.account)
}
}
impl PartialEq for AccountTreeNode {
fn eq(&self, other: &Self) -> bool {
self.account == other.account
}
}
#[cfg(test)]
impl AccountTreeNode {
pub(crate) fn get_root(&self) -> &str {
self.root.as_str()
}
pub(crate) fn get_name(&self) -> &str {
self.name.as_str()
}
}
impl AccountTreeNode {
pub fn from(account: &str) -> Result<AccountTreeNode, Box<dyn Error>> {
{
let acc = account.trim();
if acc.len() != account.len() {
let msg = format!("Account name contains whitespaces '{account}'");
return Err(msg.into());
}
}
let parts: Vec<&str> = account.split(':').collect();
if parts.is_empty() {
let msg = format!(
"Empty account names are not allowed (all sub-components are empty): '{account}'"
);
return Err(msg.into());
}
if parts
.iter()
.map(|subpath| parser::is_valid_sub_id(subpath.trim()))
.any(|valid| !valid)
{
let msg = format!("This is not a valid account name: '{account}'");
return Err(msg.into());
}
let depth = parts.len();
let root = String::from(parts[0]);
let mut rev_parts = parts;
rev_parts.reverse();
let name = String::from(rev_parts.remove(0));
rev_parts.reverse();
let parent = rev_parts.join(":");
Ok(AccountTreeNode {
depth,
root,
parent,
account: account.to_string(),
name,
})
}
}
#[cfg(test)]
#[allow(non_snake_case)]
#[allow(clippy::bool_assert_comparison)]
mod tests {
use super::*;
#[test]
fn id_270d505b_76f6_4e49_a24c_2fbdfb6e5adf__commodity_ok() {
let res = Commodity::from("He·bar".to_string());
assert!(res.is_ok());
let c = res.unwrap();
assert_eq!(c.name, "He·bar".to_string());
assert!(Commodity::from("$".to_string()).is_ok());
assert!(Commodity::from("¢".to_string()).is_ok());
assert!(Commodity::from("£".to_string()).is_ok());
assert!(Commodity::from("¤".to_string()).is_ok());
assert!(Commodity::from("Â¥".to_string()).is_ok());
}
#[test]
fn id_699aadb1_d1ba_44b6_ae6a_158cf5be13e5__commodity_err() {
assert!(Commodity::from("123".to_string()).is_err());
assert!(Commodity::from("-USD".to_string()).is_err());
assert!(Commodity::from("_USD".to_string()).is_err());
assert!(Commodity::from("·USD".to_string()).is_err());
assert!(Commodity::from("He:bar".to_string()).is_err());
assert!(Commodity::from("He bar".to_string()).is_err());
}
#[test]
fn atn_is_root() {
let atn_a = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
let atn_ab = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a:b")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
let atn_abc = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a:b:c")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
assert_eq!(atn_a.is_root(), true);
assert_eq!(atn_ab.is_root(), false);
assert_eq!(atn_abc.is_root(), false);
}
#[test]
fn atn_my_parent_is_root() {
let atn_a = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
let atn_ab = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a:b")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
let atn_abc = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a:b:c")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
assert_eq!(atn_a.my_parent_is_root(), false);
assert_eq!(atn_ab.my_parent_is_root(), true);
assert_eq!(atn_abc.my_parent_is_root(), false);
}
#[test]
fn atn_is_parent() {
let parent = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a:b")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
let leaf = TxnAccount {
atn: Arc::new(AccountTreeNode::from("a:b:c")
.unwrap()),
comm: Arc::new(Commodity::default()),
};
assert!(parent.is_parent_of(&leaf));
assert!(!parent.is_parent_of(&parent));
}
#[test]
fn atn_ok_display() {
let atn = AccountTreeNode::from("a:b:c")
.unwrap();
let atn_str = format!("{}", atn);
assert_eq!(atn_str, String::from("a:b:c"));
}
#[test]
fn id_88c5cb23_5995_4b93_8c26_a3f7374e96d9__atn_root() {
let atn = AccountTreeNode::from("a").unwrap();
assert_eq!(atn.depth, 1);
assert_eq!(atn.root, "a");
assert_eq!(atn.parent, "");
assert_eq!(atn.account, "a");
assert_eq!(atn.name, "a");
}
#[test]
fn id_fc69f9b2_1faf_425c_87d3_aed63d66171b__atn_two() {
let atn = AccountTreeNode::from("a:b").unwrap();
assert_eq!(atn.depth, 2);
assert_eq!(atn.root, "a");
assert_eq!(atn.parent, "a");
assert_eq!(atn.account, "a:b");
assert_eq!(atn.name, "b");
}
#[test]
fn id_38c103d3_4cc7_4af7_86cd_bf24ca37d026__atn_three() {
let atn = AccountTreeNode::from("a:b:c").unwrap();
assert_eq!(atn.depth, 3);
assert_eq!(atn.root, "a");
assert_eq!(atn.parent, "a:b");
assert_eq!(atn.account, "a:b:c");
assert_eq!(atn.name, "c");
}
#[test]
fn id_76a6c300_5569_4e1d_a0a1_ae2ee31d919a__atn_more() {
let atn = AccountTreeNode::from("a:b:c:leaf").unwrap();
assert_eq!(atn.depth, 4);
assert_eq!(atn.root, "a");
assert_eq!(atn.parent, "a:b:c");
assert_eq!(atn.account, "a:b:c:leaf");
assert_eq!(atn.name, "leaf");
}
#[test]
fn id_55407835_34d8_4de9_a362_4172f0e4d54b__err_empty() {
let atn = AccountTreeNode::from("");
assert!(atn.is_err());
}
#[test]
fn id_0609e72f_c509_4b62_950e_fce432122d10__err_empty_sub() {
assert!(AccountTreeNode::from("a:").is_err());
assert!(AccountTreeNode::from(":a").is_err());
assert!(AccountTreeNode::from("a::b").is_err());
assert!(AccountTreeNode::from("::").is_err());
assert!(AccountTreeNode::from(":").is_err());
assert!(AccountTreeNode::from(": :").is_err());
}
}