use chrono::NaiveDate;
use pest::Parser;
use pest_derive::Parser;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GymDataParserError {
#[error("IO error: {0}")]
IOError(#[from] io::Error),
#[error("Parse error: {0}")]
ParseError(#[from] Box<pest::error::Error<Rule>>),
#[error("Date parsing error: {0}")]
DateParseError(#[from] chrono::ParseError),
#[error("File content parse error")]
FileContentParseError,
#[error("Exercise name parse error")]
ExerciseNameParseError,
#[error("Target parse error")]
TargetParseError,
#[error("Set group parse error")]
SetGroupParseError,
#[error("Missing data error")]
MissingDateError,
#[error("Invalid number format: {0}")]
InvalidNumberFormat(#[from] std::num::ParseIntError),
}
#[derive(Debug)]
pub struct ExerciseRecord {
pub date: NaiveDate,
pub exercise_name: String,
pub target: TargetReps,
pub sets: Vec<Set>,
}
#[derive(Debug)]
pub struct TargetReps {
pub sets_count: u32,
pub min_reps: u32,
pub max_reps: u32,
}
#[derive(Debug)]
pub struct Set {
pub attempts: Vec<Attempt>,
}
#[derive(Debug)]
pub struct Attempt {
pub weight: u32,
pub reps: u32,
}
#[derive(Parser)]
#[grammar = "grammar.pest"]
pub struct Grammar;
pub fn parse_exercise_log(input: &str) -> Result<Vec<ExerciseRecord>, GymDataParserError> {
let mut records = Vec::new();
let mut parsed = Grammar::parse(Rule::file, input)
.map_err(|e| GymDataParserError::ParseError(Box::from(e)))?;
let file_rule = parsed
.next()
.ok_or(GymDataParserError::FileContentParseError)?;
for record in file_rule.into_inner() {
if record.as_rule() == Rule::record {
let mut date: Option<NaiveDate> = None;
let mut exercise_name: Option<String> = None;
let mut target: Option<TargetReps> = None;
let mut sets: Vec<Set> = Vec::new();
for field in record.into_inner() {
match field.as_rule() {
Rule::date => {
let date_str = field.as_str();
let parsed_date = NaiveDate::parse_from_str(date_str, "%d.%m.%Y")?;
date = Some(parsed_date);
}
Rule::exercise_name => {
let name = field.as_str().trim().to_string();
exercise_name = Some(name);
}
Rule::target => {
let mut parts = field.into_inner();
let sets_count_str = parts
.next()
.ok_or(GymDataParserError::TargetParseError)?
.as_str()
.trim();
let min_reps_str = parts
.next()
.ok_or(GymDataParserError::TargetParseError)?
.as_str()
.trim();
let max_reps_str = parts
.next()
.ok_or(GymDataParserError::TargetParseError)?
.as_str()
.trim();
let sets_count = sets_count_str.parse::<u32>()?;
let min_reps = min_reps_str.parse::<u32>()?;
let max_reps = max_reps_str.parse::<u32>()?;
target = Some(TargetReps {
sets_count,
min_reps,
max_reps,
});
}
Rule::set_group => {
for set_group in field.into_inner() {
let mut attempts = Vec::new();
for set in set_group.into_inner() {
let mut set_parts = set.into_inner();
let weight_str = set_parts
.next()
.ok_or(GymDataParserError::SetGroupParseError)?
.as_str()
.trim();
let reps_str = set_parts
.next()
.ok_or(GymDataParserError::SetGroupParseError)?
.as_str()
.trim();
let weight = weight_str.parse::<u32>()?;
let reps = reps_str.parse::<u32>()?;
attempts.push(Attempt { weight, reps });
}
sets.push(Set { attempts });
}
}
_ => {}
}
}
let date = date.ok_or(GymDataParserError::MissingDateError)?;
let exercise_name = exercise_name.ok_or(GymDataParserError::ExerciseNameParseError)?;
let target = target.ok_or(GymDataParserError::TargetParseError)?;
records.push(ExerciseRecord {
date,
exercise_name,
target,
sets,
});
}
}
Ok(records)
}