use crate::parser;
use std::cmp::Ordering;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Hash, PartialOrd, PartialEq, Eq)]
pub struct Commodity {
pub name: String,
}
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/currency: [{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,
pub(crate) commodity: Option<Commodity>,
pub(crate) commodity_str: String,
}
impl Display for AccountTreeNode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.account)
}
}
impl Hash for AccountTreeNode {
fn hash<H: Hasher>(&self, state: &mut H) {
self.account.hash(state);
self.commodity.hash(state);
}
}
impl Ord for AccountTreeNode {
fn cmp(&self, other: &Self) -> Ordering {
self.get_full().cmp(&other.get_full())
}
}
impl PartialOrd for AccountTreeNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for AccountTreeNode {
fn eq(&self, other: &Self) -> bool {
self.get_full() == other.get_full()
}
}
impl AccountTreeNode {
pub fn from(
account: String,
comm: Option<Commodity>,
) -> 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 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(":");
let commodity_str = comm
.as_ref()
.map(|c| String::from(&c.name))
.unwrap_or_default();
Ok(AccountTreeNode {
depth,
root,
parent,
account,
name,
commodity: comm,
commodity_str,
})
}
pub(crate) fn is_parent_of(&self, atn: &AccountTreeNode) -> bool {
self.account == atn.parent && self.commodity_str == atn.commodity_str
}
pub fn get_full(&self) -> String {
match &self.commodity {
Some(c) => String::from(&c.name) + "@" + &self.account,
None => String::from("@") + &self.account,
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
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 id_e11d5d26_c149_4d8c_b150_5cb2e2f80608__atn_commodity() {
let atn = AccountTreeNode::from(
"a".to_string(),
Some(Commodity::from("He·bar".to_string()).unwrap()),
)
.unwrap();
assert!(atn.commodity.is_some());
assert_eq!(atn.commodity.unwrap().name, "He·bar".to_string());
assert_eq!(atn.commodity_str, String::from("He·bar"));
}
#[test]
fn atn_ok_display() {
let atn = AccountTreeNode::from(
"a:b:c".to_string(),
Some(Commodity::from("He·bar".to_string()).unwrap()),
)
.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".to_string(), None).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");
assert!(atn.commodity.is_none());
assert_eq!(atn.commodity_str, String::from(""));
}
#[test]
fn id_fc69f9b2_1faf_425c_87d3_aed63d66171b__atn_two() {
let atn = AccountTreeNode::from("a:b".to_string(), None).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");
assert!(atn.commodity.is_none());
assert_eq!(atn.commodity_str, String::from(""));
}
#[test]
fn id_38c103d3_4cc7_4af7_86cd_bf24ca37d026__atn_three() {
let atn = AccountTreeNode::from("a:b:c".to_string(), None).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");
assert!(atn.commodity.is_none());
assert_eq!(atn.commodity_str, String::from(""));
}
#[test]
fn id_76a6c300_5569_4e1d_a0a1_ae2ee31d919a__atn_more() {
let atn = AccountTreeNode::from("a:b:c:leaf".to_string(), None).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");
assert!(atn.commodity.is_none());
assert_eq!(atn.commodity_str, String::from(""));
}
#[test]
fn id_55407835_34d8_4de9_a362_4172f0e4d54b__err_empty() {
let atn = AccountTreeNode::from("".to_string(), None);
assert!(atn.is_err());
}
#[test]
fn id_0609e72f_c509_4b62_950e_fce432122d10__err_empty_sub() {
assert!(AccountTreeNode::from("a:".to_string(), None).is_err());
assert!(AccountTreeNode::from(":a".to_string(), None).is_err());
assert!(AccountTreeNode::from("a::b".to_string(), None).is_err());
assert!(AccountTreeNode::from("::".to_string(), None).is_err());
assert!(AccountTreeNode::from(":".to_string(), None).is_err());
assert!(AccountTreeNode::from(": :".to_string(), None).is_err());
}
}