1use std::borrow::Borrow;
2use std::collections::HashSet;
3use std::fmt::{Display, Formatter};
4use std::sync::Arc;
5
6use nom::{
7 branch::alt,
8 bytes::complete::{tag, take_while},
9 character::complete::satisfy,
10 character::complete::{char as char_tag, space0, space1},
11 combinator::{cut, iterator, map, opt, success, value},
12 sequence::{delimited, preceded, separated_pair, terminated, tuple},
13 Parser,
14};
15
16use crate::{
17 account, account::Account, amount, amount::Amount, date, empty_line, end_of_line, metadata,
18 string, Date, Decimal, IResult, Span,
19};
20
21#[derive(Debug, Clone)]
45#[non_exhaustive]
46pub struct Transaction<D> {
47 pub flag: Option<char>,
49 pub payee: Option<String>,
51 pub narration: Option<String>,
53 pub tags: HashSet<Tag>,
55 pub links: HashSet<Link>,
57 pub postings: Vec<Posting<D>>,
59}
60
61#[derive(Debug, Clone)]
89#[non_exhaustive]
90pub struct Posting<D> {
91 pub flag: Option<char>,
93 pub account: Account,
95 pub amount: Option<Amount<D>>,
97 pub cost: Option<Cost<D>>,
99 pub price: Option<PostingPrice<D>>,
101 pub metadata: metadata::Map<D>,
103}
104
105#[derive(Debug, Clone)]
109#[non_exhaustive]
110pub struct Cost<D> {
111 pub amount: Option<Amount<D>>,
113 pub date: Option<Date>,
115}
116
117#[derive(Debug, Clone)]
121pub enum PostingPrice<D> {
122 Unit(Amount<D>),
124 Total(Amount<D>),
126}
127
128#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
146pub struct Tag(Arc<str>);
147
148impl Tag {
149 #[must_use]
151 pub fn as_str(&self) -> &str {
152 &self.0
153 }
154}
155
156impl Display for Tag {
157 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
158 Display::fmt(&self.0, f)
159 }
160}
161
162impl AsRef<str> for Tag {
163 fn as_ref(&self) -> &str {
164 self.0.as_ref()
165 }
166}
167
168impl Borrow<str> for Tag {
169 fn borrow(&self) -> &str {
170 self.0.borrow()
171 }
172}
173
174#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
192pub struct Link(Arc<str>);
193
194impl Link {
195 #[must_use]
197 pub fn as_str(&self) -> &str {
198 &self.0
199 }
200}
201
202impl Display for Link {
203 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
204 Display::fmt(&self.0, f)
205 }
206}
207
208impl AsRef<str> for Link {
209 fn as_ref(&self) -> &str {
210 self.0.as_ref()
211 }
212}
213
214impl Borrow<str> for Link {
215 fn borrow(&self) -> &str {
216 self.0.borrow()
217 }
218}
219
220#[allow(clippy::type_complexity)]
221pub(crate) fn parse<D: Decimal>(
222 input: Span<'_>,
223) -> IResult<'_, (Transaction<D>, metadata::Map<D>)> {
224 let (input, flag) = alt((map(flag, Some), value(None, tag("txn"))))(input)?;
225 cut(do_parse(flag))(input)
226}
227
228fn flag(input: Span<'_>) -> IResult<'_, char> {
229 satisfy(|c: char| !c.is_ascii_lowercase())(input)
230}
231
232fn do_parse<D: Decimal>(
233 flag: Option<char>,
234) -> impl Fn(Span<'_>) -> IResult<'_, (Transaction<D>, metadata::Map<D>)> {
235 move |input| {
236 let (input, payee_and_narration) = opt(preceded(space1, payee_and_narration))(input)?;
237 let (input, (tags, links)) = tags_and_links(input)?;
238 let (input, _) = end_of_line(input)?;
239 let (input, metadata) = metadata::parse(input)?;
240 let mut iter = iterator(input, alt((posting.map(Some), empty_line.map(|_| None))));
241 let postings = iter.flatten().collect();
242 let (input, _) = iter.finish()?;
243 let narration = payee_and_narration.map(|(_, n)| n).map(ToOwned::to_owned);
244 let payee = payee_and_narration
245 .and_then(|(p, _)| p)
246 .map(ToOwned::to_owned);
247 Ok((
248 input,
249 (
250 Transaction {
251 flag,
252 payee,
253 narration,
254 tags,
255 links,
256 postings,
257 },
258 metadata,
259 ),
260 ))
261 }
262}
263
264pub(super) enum TagOrLink {
265 Tag(Tag),
266 Link(Link),
267}
268
269pub(super) fn parse_tag(input: Span<'_>) -> IResult<'_, Tag> {
270 map(
271 preceded(
272 char_tag('#'),
273 take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
274 ),
275 |s: Span<'_>| Tag((*s.fragment()).into()),
276 )(input)
277}
278
279pub(super) fn parse_link(input: Span<'_>) -> IResult<'_, Link> {
280 map(
281 preceded(
282 char_tag('^'),
283 take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
284 ),
285 |s: Span<'_>| Link((*s.fragment()).into()),
286 )(input)
287}
288
289pub(super) fn parse_tag_or_link(input: Span<'_>) -> IResult<'_, TagOrLink> {
290 alt((
291 map(parse_tag, TagOrLink::Tag),
292 map(parse_link, TagOrLink::Link),
293 ))(input)
294}
295
296fn tags_and_links(input: Span<'_>) -> IResult<'_, (HashSet<Tag>, HashSet<Link>)> {
297 let mut tags_and_links_iter = iterator(input, preceded(space1, parse_tag_or_link));
298 let (tags, links) = tags_and_links_iter.fold(
299 (HashSet::new(), HashSet::new()),
300 |(mut tags, mut links), x| {
301 match x {
302 TagOrLink::Tag(tag) => tags.insert(tag),
303 TagOrLink::Link(link) => links.insert(link),
304 };
305 (tags, links)
306 },
307 );
308 let (input, _) = tags_and_links_iter.finish()?;
309 Ok((input, (tags, links)))
310}
311
312fn payee_and_narration(input: Span<'_>) -> IResult<'_, (Option<&str>, &str)> {
313 let (input, s1) = string(input)?;
314 let (input, s2) = opt(preceded(space1, string))(input)?;
315 Ok((
316 input,
317 match s2 {
318 Some(narration) => (Some(s1), narration),
319 None => (None, s1),
320 },
321 ))
322}
323
324fn posting<D: Decimal>(input: Span<'_>) -> IResult<'_, Posting<D>> {
325 let (input, _) = space1(input)?;
326 let (input, flag) = opt(terminated(flag, space1))(input)?;
327 let (input, account) = account::parse(input)?;
328 let (input, amounts) = opt(tuple((
329 preceded(space1, amount::parse),
330 opt(preceded(space1, cost)),
331 opt(preceded(
332 space1,
333 alt((
334 map(
335 preceded(tuple((char_tag('@'), space1)), amount::parse),
336 PostingPrice::Unit,
337 ),
338 map(
339 preceded(tuple((tag("@@"), space1)), amount::parse),
340 PostingPrice::Total,
341 ),
342 )),
343 )),
344 )))(input)?;
345 let (input, _) = end_of_line(input)?;
346 let (input, metadata) = metadata::parse(input)?;
347 let (amount, cost, price) = match amounts {
348 Some((a, l, p)) => (Some(a), l, p),
349 None => (None, None, None),
350 };
351 Ok((
352 input,
353 Posting {
354 flag,
355 account,
356 amount,
357 cost,
358 price,
359 metadata,
360 },
361 ))
362}
363
364fn cost<D: Decimal>(input: Span<'_>) -> IResult<'_, Cost<D>> {
365 let (input, _) = terminated(char_tag('{'), space0)(input)?;
366 let (input, (cost, date)) = alt((
367 map(
368 separated_pair(
369 amount::parse,
370 delimited(space0, char_tag(','), space0),
371 date::parse,
372 ),
373 |(a, d)| (Some(a), Some(d)),
374 ),
375 map(
376 separated_pair(
377 date::parse,
378 delimited(space0, char_tag(','), space0),
379 amount::parse,
380 ),
381 |(d, a)| (Some(a), Some(d)),
382 ),
383 map(amount::parse, |a| (Some(a), None)),
384 map(date::parse, |d| (None, Some(d))),
385 map(success(true), |_| (None, None)),
386 ))(input)?;
387 let (input, _) = preceded(space0, char_tag('}'))(input)?;
388 Ok((input, Cost { amount: cost, date }))
389}