accounting/src/document/parser/amounts.rs

156 lines
4.2 KiB
Rust

use nom::{
branch::alt,
character::complete::{char, none_of, one_of, space0},
combinator::{opt, recognize},
error::{Error, ErrorKind},
multi::{many0, many1},
sequence::{preceded, terminated, tuple},
Err, IResult, InputTakeAtPosition, Parser,
};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use crate::document::DirectiveAmount;
pub fn account(input: &str) -> IResult<&str, &str> {
input.split_at_position1_complete(|item| item == ' ' || item == '\t', ErrorKind::AlphaNumeric)
}
pub fn amount(input: &str) -> IResult<&str, DirectiveAmount> {
alt((suffix_amount, prefix_amount)).parse(input)
}
pub fn decimal(input: &str) -> IResult<&str, Decimal> {
let (new_input, decimal_str) = recognize(tuple((
opt(one_of("+-")),
opt(number_int),
opt(char('.')),
opt(number_int),
)))
.parse(input)?;
if decimal_str.contains(',') {
match Decimal::from_str_exact(&decimal_str.replace(",", "")) {
Ok(decimal) => Ok((new_input, decimal)),
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
}
} else {
match Decimal::from_str_exact(decimal_str) {
Ok(decimal) => Ok((new_input, decimal)),
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
}
}
}
///////////////
// Private //
///////////////
fn prefix_amount(input: &str) -> IResult<&str, DirectiveAmount> {
tuple((
opt(one_of("+-")),
unit,
preceded(space0, decimal),
))
.map(|(sign, unit_symbol, mut value)| {
if let Some(s) = sign {
if s == '-' {
value = value * dec!(-1);
}
}
DirectiveAmount {
value,
unit_symbol: unit_symbol.to_string(),
is_unit_prefix: true,
}
})
.parse(input)
}
fn suffix_amount(input: &str) -> IResult<&str, DirectiveAmount> {
tuple((
decimal,
preceded(space0, unit),
))
.map(|(value, unit_symbol)| DirectiveAmount {
value,
unit_symbol: unit_symbol.to_string(),
is_unit_prefix: false,
})
.parse(input)
}
fn unit(input: &str) -> IResult<&str, &str> {
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input)
}
fn number_int(input: &str) -> IResult<&str, &str> {
recognize(many1(terminated(one_of("0123456789"), many0(one_of("_,")))))(input)
}
#[cfg(test)]
mod tests {
use rust_decimal_macros::dec;
use super::*;
#[test]
fn parse_decimal_good() {
assert_eq!(decimal("1").unwrap().1, dec!(1));
assert_eq!(decimal("+10").unwrap().1, dec!(10));
assert_eq!(decimal("-10").unwrap().1, dec!(-10));
assert_eq!(decimal("10.1").unwrap().1, dec!(10.1));
assert_eq!(decimal("100_000.01").unwrap().1, dec!(100000.01));
assert_eq!(decimal(".1").unwrap().1, dec!(0.1));
assert_eq!(decimal("-.1").unwrap().1, dec!(-0.1));
assert_eq!(decimal("2.").unwrap().1, dec!(2.));
assert_eq!(decimal("1,000").unwrap().1, dec!(1000));
}
#[test]
fn amount_good() {
assert_eq!(
amount("$10").unwrap().1,
DirectiveAmount {
value: dec!(10),
unit_symbol: "$".into(),
is_unit_prefix: true
}
);
assert_eq!(
amount("10 USD").unwrap().1,
DirectiveAmount {
value: dec!(10),
unit_symbol: "USD".into(),
is_unit_prefix: false
}
);
assert_eq!(
amount("-$10.01").unwrap().1,
DirectiveAmount {
value: dec!(-10.01),
unit_symbol: "$".into(),
is_unit_prefix: true
}
);
assert_eq!(
amount("-10€").unwrap().1,
DirectiveAmount {
value: dec!(-10),
unit_symbol: "".into(),
is_unit_prefix: false
}
);
assert_eq!(
amount("-€10").unwrap().1,
DirectiveAmount {
value: dec!(-10),
unit_symbol: "".into(),
is_unit_prefix: true
}
);
}
}