Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jcornaz/beancount-parser
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.0.0-alpha.2
Choose a base ref
...
head repository: jcornaz/beancount-parser
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.0.0-alpha.3
Choose a head ref
  • 8 commits
  • 8 files changed
  • 1 contributor

Commits on Sep 16, 2022

  1. docs: Document the Date type

    feat: Getters for the `Date` type
    jcornaz committed Sep 16, 2022
    Copy the full SHA
    18eb0b4 View commit details
  2. Copy the full SHA
    68158c9 View commit details
  3. Copy the full SHA
    743c136 View commit details
  4. docs: Document the Transacion and Posting types

    feat: Getters for the `Transaction` and `Posting` types
    jcornaz committed Sep 16, 2022
    Copy the full SHA
    8b51e47 View commit details
  5. docs: Document the Account type

    feat: Getters for the `Account` type
    jcornaz committed Sep 16, 2022
    Copy the full SHA
    3e22b67 View commit details
  6. refactor: iterate directives instead of tuples (date, directive)

    BREAKING CHANGE: The type of item returned by the `Parser` iterator
    has changed from `Result<(Date, Directive), Error>` to `Result<Directive, Error>`.
    This is because not all directives have a date. So this change will make possible to support
    more directives in the future without breaking the API.
    jcornaz committed Sep 16, 2022
    Copy the full SHA
    3391822 View commit details
  7. Copy the full SHA
    ce9cdf4 View commit details
  8. Copy the full SHA
    7619aa2 View commit details
Showing with 173 additions and 97 deletions.
  1. +27 −0 src/account.rs
  2. +28 −0 src/date.rs
  3. +18 −34 src/directive.rs
  4. +13 −16 src/lib.rs
  5. +3 −26 src/string.rs
  6. +54 −13 src/transaction/mod.rs
  7. +25 −0 src/transaction/posting.rs
  8. +5 −8 tests/spec.rs
27 changes: 27 additions & 0 deletions src/account.rs
Original file line number Diff line number Diff line change
@@ -8,18 +8,33 @@ use nom::{
IResult,
};

/// Account
///
/// An account has a type (`Assets`, `Liabilities`, `Equity`, `Income` or `Expenses`)
/// and components.
///
/// # Examples
///
/// * `Assets:Liquidity:Cash` (type: `Assets`, components: ["Liquidity", "Cash"]
/// * `Expenses:Groceries` (type: `Assets`, components: ["Groceries"]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Account<'a> {
type_: Type,
components: Vec<&'a str>,
}

/// Type of account
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Type {
/// The assets
Assets,
/// The liabilities
Liabilities,
/// The equity
Equity,
/// Income
Income,
/// Expenses
Expenses,
}

@@ -30,6 +45,18 @@ impl<'a> Account<'a> {
components: path.into_iter().collect(),
}
}

/// Returns the type of account
#[must_use]
pub fn type_(&self) -> Type {
self.type_
}

/// Returns the components
#[must_use]
pub fn components(&self) -> &[&str] {
self.components.as_ref()
}
}

pub(crate) fn account(input: &str) -> IResult<&str, Account<'_>> {
28 changes: 28 additions & 0 deletions src/date.rs
Original file line number Diff line number Diff line change
@@ -5,6 +5,12 @@ use nom::{
IResult,
};

/// A date
///
/// The parser has some sanity check to make sure the date remotly makes sense
/// but it doesn't verify it is an actual real date valid date.
///
/// If that is important, you should use a date-time library to verify the validity.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Date {
year: u16,
@@ -21,6 +27,28 @@ impl Date {
day_of_month,
}
}

/// Returns the year
#[must_use]
pub fn year(&self) -> u16 {
self.year
}

/// Returns the number of the month in the year
///
/// The result is between `1` (january) and `12` (december) inclusive.
#[must_use]
pub fn month_of_year(&self) -> u8 {
self.month_of_year
}

/// Returns the number of the day in the month
///
/// The result is between `1` and `31` inclusive
#[must_use]
pub fn day_of_month(&self) -> u8 {
self.day_of_month
}
}

pub(super) fn date(input: &str) -> IResult<&str, Date> {
52 changes: 18 additions & 34 deletions src/directive.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
use nom::{
branch::alt,
character::complete::{line_ending, not_line_ending, space1},
combinator::{map, opt, value},
sequence::{separated_pair, tuple},
IResult,
};
use nom::{combinator::map, IResult};

use crate::{
date::date,
transaction::{transaction, Transaction},
Date,
};
use crate::transaction::{transaction, Transaction};

/// A directive
///
/// A beancount file is made of directives.
///
/// By far the the most common directive is the [`Transaction`].
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Directive<'a> {
/// The transaction directive
Transaction(Transaction<'a>),
}

impl<'a> Directive<'a> {
/// Reterns the [`Transaction`] if this directive is a transaction
#[must_use]
pub fn as_transaction(&self) -> Option<&Transaction<'a>> {
match self {
@@ -27,15 +24,8 @@ impl<'a> Directive<'a> {
}
}

pub(crate) fn directive(input: &str) -> IResult<&str, (Date, Option<Directive<'_>>)> {
separated_pair(
date,
space1,
alt((
map(map(transaction, Directive::Transaction), Some),
value(None, tuple((not_line_ending, opt(line_ending)))),
)),
)(input)
pub(crate) fn directive(input: &str) -> IResult<&str, Directive<'_>> {
map(transaction, Directive::Transaction)(input)
}

#[cfg(test)]
@@ -46,24 +36,13 @@ mod tests {
#[test]
fn transaction() {
let input = r#"2022-09-10 txn "My transaction""#;
let (_, (date, directive)) = directive(input).expect("should successfully parse directive");
assert_eq!(date, Date::new(2022, 9, 10));
let (_, directive) = directive(input).expect("should successfully parse directive");
let transaction = directive
.as_ref()
.expect("should recognize the directive")
.as_transaction()
.expect("the directive should be a transaction");
assert_eq!(transaction.narration(), Some("My transaction"));
}

#[rstest]
#[case("2022-09-11 whatisthis \"hello\"", "")]
#[case("2022-09-11 whatisthis \"hello\"\ntest", "test")]
fn unkown_directive(#[case] input: &str, #[case] expected_rest: &str) {
let (rest, _) = directive(input).expect("should successfully parse the directive");
assert_eq!(rest, expected_rest);
}

#[rstest]
fn invalid(
#[values(
@@ -73,6 +52,11 @@ mod tests {
)]
input: &str,
) {
assert!(directive(input).is_err());
assert!(matches!(directive(input), Err(nom::Err::Failure(_))));
}

#[rstest]
fn not_matching(#[values(" ")] input: &str) {
assert!(matches!(directive(input), Err(nom::Err::Error(_))));
}
}
29 changes: 13 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -18,45 +18,42 @@
//! Assets:Bank
//! "#;
//!
//! let directives: Vec<(Date, Directive<'_>)> = Parser::new(beancount).collect::<Result<_, _>>()?;
//! assert_eq!(directives[0].1.as_transaction().unwrap().narration(), Some("Coffee beans"));
//! let directives: Vec<Directive<'_>> = Parser::new(beancount).collect::<Result<_, _>>()?;
//! let transaction = directives[0].as_transaction().unwrap();
//! assert_eq!(transaction.narration(), Some("Coffee beans"));
//!
//! let postings = directives[0].1.as_transaction().unwrap().postings();
//! let postings = transaction.postings();
//! assert_eq!(postings[0].amount().unwrap().currency(), "CHF");
//! assert_eq!(postings[0].amount().unwrap().value().try_into_f64()?, 10.0);
//! # Ok(()) }
//! ```
#[allow(missing_docs)]
mod account;
mod amount;
#[allow(missing_docs)]
mod date;
#[allow(missing_docs)]
mod directive;
mod error;
#[allow(missing_docs)]
mod string;
#[allow(missing_docs)]
mod transaction;

use crate::directive::directive;

pub use crate::{
account::Account,
account::{Account, Type},
amount::{Amount, ConversionError, Expression, Value},
date::Date,
directive::Directive,
error::Error,
transaction::{Posting, Transaction},
transaction::{Flag, Posting, PriceType, Transaction},
};

use nom::{
branch::alt,
combinator::{map, value},
character::complete::{line_ending, not_line_ending},
combinator::{map, opt, value},
sequence::tuple,
IResult,
};
use string::comment_line;

/// Parser of a beancount document
///
@@ -76,7 +73,7 @@ impl<'a> Parser<'a> {
}

impl<'a> Iterator for Parser<'a> {
type Item = Result<(Date, Directive<'a>), Error>;
type Item = Result<Directive<'a>, Error>;

fn next(&mut self) -> Option<Self::Item> {
while !self.rest.is_empty() {
@@ -94,9 +91,9 @@ impl<'a> Iterator for Parser<'a> {
}
}

fn next(input: &str) -> IResult<&str, Option<(Date, Directive<'_>)>> {
fn next(input: &str) -> IResult<&str, Option<Directive<'_>>> {
alt((
map(directive, |(date, directive)| directive.map(|d| (date, d))),
value(None, comment_line),
map(directive, Some),
value(None, tuple((not_line_ending, opt(line_ending)))),
))(input)
}
29 changes: 3 additions & 26 deletions src/string.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use nom::{
branch::alt,
bytes::complete::{escaped_transform, tag, take_till1, take_while1},
character::complete::{char, digit1, line_ending, not_line_ending},
combinator::{map, not, opt, recognize, value},
sequence::{delimited, preceded, tuple},
character::complete::{char, not_line_ending},
combinator::{map, value},
sequence::{delimited, preceded},
IResult,
};

@@ -31,11 +31,6 @@ pub(crate) fn comment(input: &str) -> IResult<&str, &str> {
preceded(take_while1(|c| c == ';'), map(not_line_ending, str::trim))(input)
}

pub(crate) fn comment_line(input: &str) -> IResult<&str, &str> {
let date_like = tuple((digit1, char('-'), digit1, char('-'), digit1));
recognize(tuple((not(date_like), not_line_ending, opt(line_ending))))(input)
}

#[cfg(test)]
mod tests {
use super::*;
@@ -68,22 +63,4 @@ mod tests {
assert_eq!(comment, "This is a comment");
assert_eq!(rest, "\n This is not a comment");
}

#[rstest]
#[case("* Banking", "")]
#[case("* Banking\n2022-01-01", "2022-01-01")]
#[case("\n", "")]
#[case("\ntest", "test")]
#[case("test", "")]
fn recognize_comment_line(#[case] input: &str, #[case] expected_rest: &str) {
let (rest, _) =
comment_line(input).expect("should succesfully parse the input as a comment line");
assert_eq!(rest, expected_rest);
}

#[rstest]
fn recognize_a_non_comment_line(#[values("2022-01-01", "0000-00-00")] input: &str) {
let result = comment_line(input);
assert!(result.is_err(), "{:?}", result);
}
}
Loading