use gregorian::Date;
use super::types::Account;
use super::types::Cents;
use super::types::Mutation;
use super::types::Tag;
use super::types::Transaction;
impl<'a> Transaction<'a> {
pub fn parse_from_str(data: &'a str) -> Result<Vec<Self>, ParseError<'a>> {
let mut lines = data.lines();
let mut output = Vec::new();
output.reserve((lines.size_hint().0 + 3) / 4);
while let Some(transaction) = Self::parse_from_lines(&mut lines)? {
output.push(transaction);
}
Ok(output)
}
pub fn parse_from_lines(lines: &mut std::str::Lines<'a>) -> Result<Option<Self>, ParseError<'a>> {
let header = loop {
let line = match lines.next() {
Some(x) => x.trim(),
None => return Ok(None),
};
if !line.starts_with('#') && !line.is_empty() {
break line;
}
};
let (date, description) = partition(header, ':')
.ok_or_else(|| MissingDescription.for_token(header))?;
let date = date.trim();
let description = description.trim();
if description.is_empty() {
return Err(MissingDescription.for_token(header));
}
let date: Date = date.parse().map_err(|_| InvalidTransactionHeaderDetails::InvalidDate.for_token(date))?;
let mut tags = Vec::new();
let mut mutations = Vec::new();
while let Some(line) = lines.next() {
let line = line.trim();
if line.is_empty() {
break;
} else if line.starts_with('#') {
continue;
} else if let Some(tag) = Tag::parse_from_str(line) {
if mutations.is_empty() {
tags.push(tag?);
} else {
return Err(InvalidTagDetails::TagAfterMutation.for_token(line));
}
} else {
mutations.push(Mutation::parse_from_str(line)?);
}
}
Ok(Some(Self { date, description, tags, mutations }))
}
}
impl<'a> Tag<'a> {
fn parse_from_str(data: &'a str) -> Option<Result<Self, ParseError<'a>>> {
let data = data.trim();
let (label, value) = partition(data, ':')?;
let label = label.trim();
let value = value.trim();
if label.chars().find(|c| !c.is_ascii_alphanumeric() && *c != '-').is_some() {
return Some(Err(InvalidLabel.for_token(label).into()));
}
Some(Ok(Self { label, value }))
}
}
impl<'a> Mutation<'a> {
fn parse_from_str(data: &'a str) -> Result<Self, ParseError<'a>> {
let data = data.trim();
let (amount, account) = partition(data, ' ').ok_or(MissingAccount.for_token(data))?;
let amount = amount.trim();
let account = account.trim();
let sign = match &amount[0..1] {
"-" => -1,
"+" => 1,
_ => return Err(MissingSign.for_token(amount)),
};
let Cents(amount) = Cents::parse_from_str(&amount[1..])
.map_err(|_| InvalidAmount.for_token(amount))?;
let amount = Cents(amount * sign);
Ok(Self { amount, account: Account::from_raw(account) })
}
}
impl Cents {
fn parse_from_str(data: &str) -> Result<Self, ()> {
if let Some((whole, decimals)) = partition(data, '.') {
if decimals.len() != 2 {
Err(())
} else {
let whole : i32 = whole.parse().map_err(|_| ())?;
let decimals : i32 = decimals.parse().map_err(|_| ())?;
Ok(Self(whole * 100 + decimals))
}
} else {
let whole : i32 = data.parse().map_err(|_| ())?;
Ok(Self(whole * 100))
}
}
}
#[derive(Clone, Debug)]
pub struct ParseError<'a> {
pub details: ParseErrorDetails,
pub token: &'a str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ParseErrorDetails {
InvalidTransactionHeader(InvalidTransactionHeaderDetails),
InvalidMutation(InvalidMutationDetails),
InvalidTag(InvalidTagDetails),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InvalidTransactionHeaderDetails {
MissingHeader,
MissingDescription,
InvalidDate,
}
use InvalidTransactionHeaderDetails::*;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InvalidTagDetails {
InvalidLabel,
TagAfterMutation,
}
use InvalidTagDetails::*;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InvalidMutationDetails {
MissingSign,
MissingAccount,
InvalidAmount,
}
use InvalidMutationDetails::*;
impl From<InvalidTransactionHeaderDetails> for ParseErrorDetails {
fn from(other: InvalidTransactionHeaderDetails) -> Self {
Self::InvalidTransactionHeader(other)
}
}
impl From<InvalidTagDetails> for ParseErrorDetails {
fn from(other: InvalidTagDetails) -> Self {
Self::InvalidTag(other)
}
}
impl From<InvalidMutationDetails> for ParseErrorDetails {
fn from(other: InvalidMutationDetails) -> Self {
Self::InvalidMutation(other)
}
}
impl InvalidTransactionHeaderDetails {
fn for_token(self, token: &str) -> ParseError {
ParseError { details: self.into(), token }
}
}
impl InvalidTagDetails {
fn for_token(self, token: &str) -> ParseError {
ParseError { details: self.into(), token }
}
}
impl InvalidMutationDetails {
fn for_token(self, token: &str) -> ParseError {
ParseError { details: self.into(), token }
}
}
fn partition(data: &str, seperator: char) -> Option<(&str, &str)> {
let mut split = data.splitn(2, seperator);
Some((split.next()?, split.next()?))
}
impl std::fmt::Display for ParseError<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "parse error at token: {:?}: {}", self.token, self.details)
}
}
impl std::fmt::Display for ParseErrorDetails {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::InvalidTransactionHeader(e) => write!(f, "{}", e),
Self::InvalidTag(e) => write!(f, "{}", e),
Self::InvalidMutation(e) => write!(f, "{}", e),
}
}
}
impl std::fmt::Display for InvalidTransactionHeaderDetails {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::MissingHeader => write!(f, "missing transaction header"),
Self::MissingDescription => write!(f, "missing transaction description"),
Self::InvalidDate => write!(f, "invalid date"),
}
}
}
impl std::fmt::Display for InvalidTagDetails {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::InvalidLabel => write!(f, "invalid tag label"),
Self::TagAfterMutation => write!(f, "tags are only allowed before the first mutation"),
}
}
}
impl std::fmt::Display for InvalidMutationDetails {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::MissingSign => write!(f, "missing sign (+/-)"),
Self::MissingAccount => write!(f, "missing account for mutation"),
Self::InvalidAmount => write!(f, "invalid mutation amount"),
}
}
}
impl std::error::Error for ParseError<'_> {}
impl std::error::Error for ParseErrorDetails {}
impl std::error::Error for InvalidTransactionHeaderDetails {}
impl std::error::Error for InvalidTagDetails {}
impl std::error::Error for InvalidMutationDetails {}