use std::borrow::Borrow;
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use nom::{
branch::alt,
bytes::complete::{tag, take_while},
character::complete::satisfy,
character::complete::{char as char_tag, space0, space1},
combinator::{cut, iterator, map, opt, success, value},
sequence::{delimited, preceded, separated_pair, terminated},
Parser,
};
use crate::string;
use crate::{
account, account::Account, amount, amount::Amount, date, empty_line, end_of_line, metadata,
Date, Decimal, IResult, Span,
};
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct Transaction<D> {
pub flag: Option<char>,
pub payee: Option<String>,
pub narration: Option<String>,
pub tags: HashSet<Tag>,
pub links: HashSet<Link>,
pub postings: Vec<Posting<D>>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Posting<D> {
pub flag: Option<char>,
pub account: Account,
pub amount: Option<Amount<D>>,
pub cost: Option<Cost<D>>,
pub price: Option<PostingPrice<D>>,
pub metadata: metadata::Map<D>,
}
impl<D> Posting<D> {
#[must_use]
pub fn from_account(account: Account) -> Posting<D> {
Posting {
flag: None,
account,
amount: None,
cost: None,
price: None,
metadata: metadata::Map::new(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[non_exhaustive]
pub struct Cost<D> {
pub amount: Option<Amount<D>>,
pub date: Option<Date>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PostingPrice<D> {
Unit(Amount<D>),
Total(Amount<D>),
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Tag(Arc<str>);
impl Tag {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl AsRef<str> for Tag {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl Borrow<str> for Tag {
fn borrow(&self) -> &str {
self.0.borrow()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Link(Arc<str>);
impl Link {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for Link {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl AsRef<str> for Link {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl Borrow<str> for Link {
fn borrow(&self) -> &str {
self.0.borrow()
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn parse<D: Decimal>(
input: Span<'_>,
) -> IResult<'_, (Transaction<D>, metadata::Map<D>)> {
let (input, flag) = alt((map(flag, Some), value(None, tag("txn")))).parse(input)?;
cut(do_parse(flag)).parse(input)
}
fn flag(input: Span<'_>) -> IResult<'_, char> {
satisfy(|c: char| !c.is_ascii_lowercase())(input)
}
fn do_parse<D: Decimal>(
flag: Option<char>,
) -> impl Fn(Span<'_>) -> IResult<'_, (Transaction<D>, metadata::Map<D>)> {
move |input| {
let (input, payee_and_narration) =
opt(preceded(space1, payee_and_narration)).parse(input)?;
let (input, (tags, links)) = tags_and_links(input)?;
let (input, ()) = end_of_line(input)?;
let (input, metadata) = metadata::parse(input)?;
let mut iter = iterator(input, alt((posting.map(Some), empty_line.map(|()| None))));
let postings = iter.by_ref().flatten().collect();
let (input, ()) = iter.finish()?;
let (payee, narration) = match payee_and_narration {
Some((payee, narration)) => (payee, Some(narration)),
None => (None, None),
};
Ok((
input,
(
Transaction {
flag,
payee,
narration,
tags,
links,
postings,
},
metadata,
),
))
}
}
pub(super) enum TagOrLink {
Tag(Tag),
Link(Link),
}
pub(super) fn parse_tag(input: Span<'_>) -> IResult<'_, Tag> {
map(
preceded(
char_tag('#'),
take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
),
|s: Span<'_>| Tag((*s.fragment()).into()),
)
.parse(input)
}
pub(super) fn parse_link(input: Span<'_>) -> IResult<'_, Link> {
map(
preceded(
char_tag('^'),
take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
),
|s: Span<'_>| Link((*s.fragment()).into()),
)
.parse(input)
}
pub(super) fn parse_tag_or_link(input: Span<'_>) -> IResult<'_, TagOrLink> {
alt((
map(parse_tag, TagOrLink::Tag),
map(parse_link, TagOrLink::Link),
))
.parse(input)
}
fn tags_and_links(input: Span<'_>) -> IResult<'_, (HashSet<Tag>, HashSet<Link>)> {
let mut tags_and_links_iter = iterator(input, preceded(space0, parse_tag_or_link));
let (tags, links) = tags_and_links_iter.by_ref().fold(
(HashSet::new(), HashSet::new()),
|(mut tags, mut links), x| {
match x {
TagOrLink::Tag(tag) => tags.insert(tag),
TagOrLink::Link(link) => links.insert(link),
};
(tags, links)
},
);
let (input, ()) = tags_and_links_iter.finish()?;
Ok((input, (tags, links)))
}
fn payee_and_narration(input: Span<'_>) -> IResult<'_, (Option<String>, String)> {
let (input, s1) = string(input)?;
let (input, s2) = opt(preceded(space1, string)).parse(input)?;
Ok((
input,
match s2 {
Some(narration) => (Some(s1), narration),
None => (None, s1),
},
))
}
fn posting<D: Decimal>(input: Span<'_>) -> IResult<'_, Posting<D>> {
let (input, _) = space1(input)?;
let (input, flag) = opt(terminated(flag, space1)).parse(input)?;
let (input, account) = account::parse(input)?;
let (input, amounts) = opt((
preceded(space1, amount::parse),
opt(preceded(space1, cost)),
opt(preceded(
space1,
alt((
map(
preceded((char_tag('@'), space1), amount::parse),
PostingPrice::Unit,
),
map(
preceded((tag("@@"), space1), amount::parse),
PostingPrice::Total,
),
)),
)),
))
.parse(input)?;
let (input, ()) = end_of_line(input)?;
let (input, metadata) = metadata::parse(input)?;
let (amount, cost, price) = match amounts {
Some((a, l, p)) => (Some(a), l, p),
None => (None, None, None),
};
Ok((
input,
Posting {
flag,
account,
amount,
cost,
price,
metadata,
},
))
}
fn cost<D: Decimal>(input: Span<'_>) -> IResult<'_, Cost<D>> {
let (input, _) = terminated(char_tag('{'), space0).parse(input)?;
let (input, (cost, date)) = alt((
map(
separated_pair(
amount::parse,
delimited(space0, char_tag(','), space0),
date::parse,
),
|(a, d)| (Some(a), Some(d)),
),
map(
separated_pair(
date::parse,
delimited(space0, char_tag(','), space0),
amount::parse,
),
|(d, a)| (Some(a), Some(d)),
),
map(amount::parse, |a| (Some(a), None)),
map(date::parse, |d| (None, Some(d))),
map(success(true), |_| (None, None)),
))
.parse(input)?;
let (input, _) = preceded(space0, char_tag('}')).parse(input)?;
Ok((input, Cost { amount: cost, date }))
}