use crate::{
account_name::account_name,
comment::{comment, Comment},
posting_type::PostingType,
posting_value::{posting_value, PostingValue},
};
use nom::{
branch::alt,
character::complete::{char, space0, space1},
combinator::{map, opt},
sequence::{delimited, preceded, tuple},
IResult,
};
#[derive(Debug, PartialEq, Eq)]
pub struct Posting<'a> {
pub account_name: &'a str,
pub posting_type: PostingType,
pub posting_value: Option<PostingValue<'a>>,
pub comment: Option<Comment<'a>>,
}
pub(crate) fn posting(i: &str) -> IResult<&str, Posting> {
map(
tuple((
account_name_with_posting_type,
opt(preceded(space1, posting_value)),
space0,
opt(comment),
)),
|((account_name, posting_type), posting_value, _, comment)| Posting {
account_name,
posting_type,
posting_value,
comment,
},
)(i)
}
fn account_name_with_posting_type(i: &str) -> IResult<&str, (&str, PostingType)> {
alt((
map(account_name, |a| (a, PostingType::RegularPosting)),
map(delimited(char('('), account_name, char(')')), |a| {
(a, PostingType::VirtualPosting)
}),
))(i)
}
#[test]
fn it_should_parse_when_detail_omitted() {
assert_eq!(
posting("a"),
Ok((
"",
Posting {
account_name: "a",
posting_type: PostingType::RegularPosting,
posting_value: None,
comment: None
}
))
);
assert_eq!(
posting("(a)"),
Ok((
"",
Posting {
account_name: "a",
posting_type: PostingType::VirtualPosting,
posting_value: None,
comment: None
}
))
);
}
#[test]
fn it_should_parse_given_detail() {
use crate::position::Position;
assert_eq!(
posting("Expenses:Ice·Cream 200 SEK"),
Ok((
"",
Posting {
account_name: "Expenses:Ice·Cream",
posting_type: PostingType::RegularPosting,
posting_value: Some(PostingValue {
value_position: Position {
amount: "200",
commodity: Some("SEK")
},
closing_position: None,
}),
comment: None
}
))
);
assert_eq!(
posting("(Expenses:Ice·Cream) 200 SEK"),
Ok((
"",
Posting {
account_name: "Expenses:Ice·Cream",
posting_type: PostingType::VirtualPosting,
posting_value: Some(PostingValue {
value_position: Position {
amount: "200",
commodity: Some("SEK")
},
closing_position: None,
}),
comment: None
}
))
);
}
#[test]
fn it_should_parse_given_detail_and_value_position() {
use crate::{
closing_position::{ClosingPosition, ClosingPositionType},
position::Position,
};
assert_eq!(
posting("Expenses:Ice·Cream 200 SEK @ 0.1039 EUR"),
Ok((
"",
Posting {
account_name: "Expenses:Ice·Cream",
posting_type: PostingType::RegularPosting,
posting_value: Some(PostingValue {
value_position: Position {
amount: "200",
commodity: Some("SEK")
},
closing_position: Some(ClosingPosition {
closing_position_type: ClosingPositionType::UnitPrice,
position: Position {
amount: "0.1039",
commodity: Some("EUR")
}
})
}),
comment: None
}
))
);
}
#[test]
fn it_should_parse_given_detail_and_attribute_comment() {
use crate::position::Position;
assert_eq!(
posting("Expenses:Ice_cream 2.12 SEK # date: 2017-01-03"),
Ok((
"",
Posting {
account_name: "Expenses:Ice_cream",
posting_type: PostingType::RegularPosting,
posting_value: Some(PostingValue {
value_position: Position {
amount: "2.12",
commodity: Some("SEK")
},
closing_position: None
}),
comment: Some(Comment::Attribute("date: 2017-01-03"))
}
))
);
}
#[test]
fn it_should_parse_given_no_commodity_but_comment() {
use crate::position::Position;
assert_eq!(
posting("Expenses:Jäätelö 2.12 ; Strawberry ice cream!"),
Ok((
"",
Posting {
account_name: "Expenses:Jäätelö",
posting_type: PostingType::RegularPosting,
posting_value: Some(PostingValue {
value_position: Position {
amount: "2.12",
commodity: None
},
closing_position: None
}),
comment: Some(Comment::UserComment("Strawberry ice cream!"))
}
))
);
}
#[test]
fn it_should_fail_to_parse_given_leading_spaces() {
assert!(posting(" Expenses:Jäätelö 2.12").is_err());
}