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 } ); } }