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},
13 Parser,
14};
15
16use crate::string;
17use crate::{
18 account, account::Account, amount, amount::Amount, date, empty_line, end_of_line, metadata,
19 Date, Decimal, IResult, Span,
20};
21
22#[derive(Debug, Clone, PartialEq, Default)]
46#[non_exhaustive]
47pub struct Transaction<D> {
48 pub flag: Option<char>,
50 pub payee: Option<String>,
52 pub narration: Option<String>,
54 pub tags: HashSet<Tag>,
56 pub links: HashSet<Link>,
58 pub postings: Vec<Posting<D>>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
90#[non_exhaustive]
91pub struct Posting<D> {
92 pub flag: Option<char>,
94 pub account: Account,
96 pub amount: Option<Amount<D>>,
98 pub cost: Option<Cost<D>>,
100 pub price: Option<PostingPrice<D>>,
102 pub metadata: metadata::Map<D>,
104}
105
106impl<D> Posting<D> {
107 #[must_use]
109 pub fn from_account(account: Account) -> Posting<D> {
110 Posting {
111 flag: None,
112 account,
113 amount: None,
114 cost: None,
115 price: None,
116 metadata: metadata::Map::new(),
117 }
118 }
119}
120
121#[derive(Debug, Default, Clone, PartialEq)]
125#[non_exhaustive]
126pub struct Cost<D> {
127 pub amount: Option<Amount<D>>,
129 pub date: Option<Date>,
131}
132
133#[derive(Debug, Clone, PartialEq)]
137pub enum PostingPrice<D> {
138 Unit(Amount<D>),
140 Total(Amount<D>),
142}
143
144#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
162pub struct Tag(Arc<str>);
163
164impl Tag {
165 #[must_use]
167 pub fn as_str(&self) -> &str {
168 &self.0
169 }
170}
171
172impl Display for Tag {
173 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
174 Display::fmt(&self.0, f)
175 }
176}
177
178impl AsRef<str> for Tag {
179 fn as_ref(&self) -> &str {
180 self.0.as_ref()
181 }
182}
183
184impl Borrow<str> for Tag {
185 fn borrow(&self) -> &str {
186 self.0.borrow()
187 }
188}
189
190#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
208pub struct Link(Arc<str>);
209
210impl Link {
211 #[must_use]
213 pub fn as_str(&self) -> &str {
214 &self.0
215 }
216}
217
218impl Display for Link {
219 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
220 Display::fmt(&self.0, f)
221 }
222}
223
224impl AsRef<str> for Link {
225 fn as_ref(&self) -> &str {
226 self.0.as_ref()
227 }
228}
229
230impl Borrow<str> for Link {
231 fn borrow(&self) -> &str {
232 self.0.borrow()
233 }
234}
235
236#[allow(clippy::type_complexity)]
237pub(crate) fn parse<D: Decimal>(
238 input: Span<'_>,
239) -> IResult<'_, (Transaction<D>, metadata::Map<D>)> {
240 let (input, flag) = alt((map(flag, Some), value(None, tag("txn")))).parse(input)?;
241 cut(do_parse(flag)).parse(input)
242}
243
244fn flag(input: Span<'_>) -> IResult<'_, char> {
245 satisfy(|c: char| !c.is_ascii_lowercase())(input)
246}
247
248fn do_parse<D: Decimal>(
249 flag: Option<char>,
250) -> impl Fn(Span<'_>) -> IResult<'_, (Transaction<D>, metadata::Map<D>)> {
251 move |input| {
252 let (input, payee_and_narration) =
253 opt(preceded(space1, payee_and_narration)).parse(input)?;
254 let (input, (tags, links)) = tags_and_links(input)?;
255 let (input, ()) = end_of_line(input)?;
256 let (input, metadata) = metadata::parse(input)?;
257 let mut iter = iterator(input, alt((posting.map(Some), empty_line.map(|()| None))));
258 let postings = iter.by_ref().flatten().collect();
259 let (input, ()) = iter.finish()?;
260 let (payee, narration) = match payee_and_narration {
261 Some((payee, narration)) => (payee, Some(narration)),
262 None => (None, None),
263 };
264 Ok((
265 input,
266 (
267 Transaction {
268 flag,
269 payee,
270 narration,
271 tags,
272 links,
273 postings,
274 },
275 metadata,
276 ),
277 ))
278 }
279}
280
281pub(super) enum TagOrLink {
282 Tag(Tag),
283 Link(Link),
284}
285
286pub(super) fn parse_tag(input: Span<'_>) -> IResult<'_, Tag> {
287 map(
288 preceded(
289 char_tag('#'),
290 take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
291 ),
292 |s: Span<'_>| Tag((*s.fragment()).into()),
293 )
294 .parse(input)
295}
296
297pub(super) fn parse_link(input: Span<'_>) -> IResult<'_, Link> {
298 map(
299 preceded(
300 char_tag('^'),
301 take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.'),
302 ),
303 |s: Span<'_>| Link((*s.fragment()).into()),
304 )
305 .parse(input)
306}
307
308pub(super) fn parse_tag_or_link(input: Span<'_>) -> IResult<'_, TagOrLink> {
309 alt((
310 map(parse_tag, TagOrLink::Tag),
311 map(parse_link, TagOrLink::Link),
312 ))
313 .parse(input)
314}
315
316fn tags_and_links(input: Span<'_>) -> IResult<'_, (HashSet<Tag>, HashSet<Link>)> {
317 let mut tags_and_links_iter = iterator(input, preceded(space0, parse_tag_or_link));
318 let (tags, links) = tags_and_links_iter.by_ref().fold(
319 (HashSet::new(), HashSet::new()),
320 |(mut tags, mut links), x| {
321 match x {
322 TagOrLink::Tag(tag) => tags.insert(tag),
323 TagOrLink::Link(link) => links.insert(link),
324 };
325 (tags, links)
326 },
327 );
328 let (input, ()) = tags_and_links_iter.finish()?;
329 Ok((input, (tags, links)))
330}
331
332fn payee_and_narration(input: Span<'_>) -> IResult<'_, (Option<String>, String)> {
333 let (input, s1) = string(input)?;
334 let (input, s2) = opt(preceded(space1, string)).parse(input)?;
335 Ok((
336 input,
337 match s2 {
338 Some(narration) => (Some(s1), narration),
339 None => (None, s1),
340 },
341 ))
342}
343
344fn posting<D: Decimal>(input: Span<'_>) -> IResult<'_, Posting<D>> {
345 let (input, _) = space1(input)?;
346 let (input, flag) = opt(terminated(flag, space1)).parse(input)?;
347 let (input, account) = account::parse(input)?;
348 let (input, amounts) = opt((
349 preceded(space1, amount::parse),
350 opt(preceded(space1, cost)),
351 opt(preceded(
352 space1,
353 alt((
354 map(
355 preceded((char_tag('@'), space1), amount::parse),
356 PostingPrice::Unit,
357 ),
358 map(
359 preceded((tag("@@"), space1), amount::parse),
360 PostingPrice::Total,
361 ),
362 )),
363 )),
364 ))
365 .parse(input)?;
366 let (input, ()) = end_of_line(input)?;
367 let (input, metadata) = metadata::parse(input)?;
368 let (amount, cost, price) = match amounts {
369 Some((a, l, p)) => (Some(a), l, p),
370 None => (None, None, None),
371 };
372 Ok((
373 input,
374 Posting {
375 flag,
376 account,
377 amount,
378 cost,
379 price,
380 metadata,
381 },
382 ))
383}
384
385fn cost<D: Decimal>(input: Span<'_>) -> IResult<'_, Cost<D>> {
386 let (input, _) = terminated(char_tag('{'), space0).parse(input)?;
387 let (input, (cost, date)) = alt((
388 map(
389 separated_pair(
390 amount::parse,
391 delimited(space0, char_tag(','), space0),
392 date::parse,
393 ),
394 |(a, d)| (Some(a), Some(d)),
395 ),
396 map(
397 separated_pair(
398 date::parse,
399 delimited(space0, char_tag(','), space0),
400 amount::parse,
401 ),
402 |(d, a)| (Some(a), Some(d)),
403 ),
404 map(amount::parse, |a| (Some(a), None)),
405 map(date::parse, |d| (None, Some(d))),
406 map(success(true), |_| (None, None)),
407 ))
408 .parse(input)?;
409 let (input, _) = preceded(space0, char_tag('}')).parse(input)?;
410 Ok((input, Cost { amount: cost, date }))
411}