use crate::parser::{is_valid_identifier, is_valid_name};
use crate::tackler;
use std::cmp::Ordering;
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 Display for Commodity {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl Commodity {
pub fn is_any(&self) -> bool {
!self.name.is_empty()
}
pub fn is_void(&self) -> bool {
self.name.is_empty()
}
}
impl Commodity {
pub fn from(name: &str) -> Result<Commodity, tackler::Error> {
match is_valid_identifier(name) {
Err(e) => {
let msg = format!("This is not a valid commodity: '{name}', error was: {e}");
Err(msg.into())
}
Ok(_) => Ok(Self::unchecked_from(name)),
}
}
pub fn unchecked_from(name: &str) -> Commodity {
Commodity {
name: name.to_string(),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Eq)]
pub(crate) struct AccountTreeNode {
pub(crate) depth: usize,
root: String,
pub(crate) parent: String,
parts: Vec<String>,
pub(crate) account: String,
name: String,
}
impl AccountTreeNode {
pub(crate) fn is_root(&self) -> bool {
self.depth == 1
}
}
#[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.cmp(&other.atn),
}
}
}
impl PartialOrd for TxnAccount {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl TxnAccount {
pub(crate) fn is_root(&self) -> bool {
self.atn.is_root()
}
}
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);
}
}
impl PartialEq for AccountTreeNode {
fn eq(&self, other: &Self) -> bool {
self.depth == other.depth && self.account == other.account
}
}
impl PartialOrd for AccountTreeNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for AccountTreeNode {
fn cmp(&self, other: &Self) -> Ordering {
for (acc_a, acc_b) in self.parts.iter().zip(other.parts.iter()) {
match acc_a.cmp(acc_b) {
Ordering::Equal => {}
ordering => return ordering,
}
}
self.depth.cmp(&other.depth)
}
}
#[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(crate) fn from(account: &str) -> Result<AccountTreeNode, tackler::Error> {
if let Err(e) = is_valid_name(account) {
let msg = format!("This is not a valid account name: '{account}', error was: {e}");
return Err(msg.into());
}
Ok(Self::unchecked_from(account))
}
pub(crate) fn make_parent(child: &AccountTreeNode) -> Result<AccountTreeNode, tackler::Error> {
if child.depth < 2 {
let msg = format!("can't create parent from root account: '{child}'");
return Err(msg.into());
}
let parts = child.parts[..child.depth - 1].to_vec();
let depth = parts.len();
assert_eq!(depth, child.depth - 1);
let root = child.root.clone();
let parent = parts[..depth - 1].join(":");
let account = child.parent.clone();
let name = parts.last().unwrap().clone();
Ok(AccountTreeNode {
depth,
root,
parent,
parts,
account,
name,
})
}
pub(crate) fn unchecked_from(account: &str) -> AccountTreeNode {
let parts: Vec<&str> = account.split(':').collect();
let depth = parts.len();
assert!(depth > 0);
let root = String::from(parts[0]);
let acc_parts = parts.iter().map(ToString::to_string).collect();
let name = parts.last().unwrap().to_string();
let parent = parts[..depth - 1].join(":");
AccountTreeNode {
depth,
root,
parent,
parts: acc_parts,
account: account.to_string(),
name,
}
}
}
#[cfg(test)]
#[allow(clippy::bool_assert_comparison)]
mod tests {
use super::*;
#[test]
fn commodity_ok() {
let res = Commodity::from("He·bar");
assert!(res.is_ok());
let c = res.unwrap();
assert_eq!(c.name, "He·bar");
assert!(Commodity::from("$").is_ok());
assert!(Commodity::from("¢").is_ok());
assert!(Commodity::from("£").is_ok());
assert!(Commodity::from("¤").is_ok());
assert!(Commodity::from("¥").is_ok());
}
#[test]
fn commodity_err() {
assert!(Commodity::from("123").is_err());
assert!(Commodity::from("-USD").is_err());
assert!(Commodity::from("_USD").is_err());
assert!(Commodity::from("·USD").is_err());
assert!(Commodity::from("He:bar").is_err());
assert!(Commodity::from("He bar").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_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 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 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 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 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 err_empty() {
let atn = AccountTreeNode::from("");
assert!(atn.is_err());
}
#[test]
fn 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());
}
#[test]
fn err_invalid_ids_sep_chars() {
assert!(AccountTreeNode::from("-Err").is_err());
assert!(AccountTreeNode::from("_Err").is_err());
assert!(AccountTreeNode::from("·Err").is_err());
assert!(AccountTreeNode::from("A:-Err").is_err());
assert!(AccountTreeNode::from("A:_Err").is_err());
assert!(AccountTreeNode::from("A:·Err").is_err());
}
#[test]
fn err_invalid_ids_spaces() {
assert!(AccountTreeNode::from(" a").is_err());
assert!(AccountTreeNode::from("a ").is_err());
assert!(AccountTreeNode::from(" a ").is_err());
assert!(AccountTreeNode::from(" a:b:c").is_err());
assert!(AccountTreeNode::from("a:b:c ").is_err());
assert!(AccountTreeNode::from(" a:b:c ").is_err());
assert!(AccountTreeNode::from("a:b c:d").is_err());
}
#[test]
fn err_invalid_ids_numerical() {
assert!(AccountTreeNode::from("123:Err").is_err());
assert!(AccountTreeNode::from("-123:Err").is_err());
assert!(AccountTreeNode::from("1ABC:Err").is_err());
assert!(AccountTreeNode::from("-1ABC:Err").is_err());
}
}