Skip to main content

my_gym_data_rust_parser/
lib.rs

1use chrono::NaiveDate;
2use pest::Parser;
3use pest_derive::Parser;
4use std::io;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum GymDataParserError {
9    #[error("IO error: {0}")]
10    IOError(#[from] io::Error),
11
12    #[error("Parse error: {0}")]
13    ParseError(#[from] Box<pest::error::Error<Rule>>),
14
15    #[error("Date parsing error: {0}")]
16    DateParseError(#[from] chrono::ParseError),
17
18    #[error("File content parse error")]
19    FileContentParseError,
20
21    #[error("Exercise name parse error")]
22    ExerciseNameParseError,
23
24    #[error("Target parse error")]
25    TargetParseError,
26
27    #[error("Set group parse error")]
28    SetGroupParseError,
29
30    #[error("Missing data error")]
31    MissingDateError,
32
33    #[error("Invalid number format: {0}")]
34    InvalidNumberFormat(#[from] std::num::ParseIntError),
35}
36
37/// Represents a record of a single exercise session, including the date, exercise name,
38/// target repetition range, and the sets completed.
39#[derive(Debug)]
40pub struct ExerciseRecord {
41    /// The date the exercise was completed, formatted as `DD.MM.YYYY`.
42    pub date: NaiveDate,
43
44    /// The name of the exercise performed (e.g., "bench press").
45    pub exercise_name: String,
46
47    /// The target repetitions, including the total sets and the min-max rep range.
48    pub target: TargetReps,
49
50    /// A vector of `Set` instances, each containing the details of completed sets.
51    pub sets: Vec<Set>,
52}
53
54/// Represents the target repetition range for an exercise, specifying the number of sets
55/// and the min-max range of repetitions for each set.
56///
57/// An example of a valid target token: "3 x 10-15 reps", where `sets_count` is 3,
58/// `min_reps` is 10, and `max_reps` is 15.
59#[derive(Debug)]
60pub struct TargetReps {
61    /// The number of sets targeted for the exercise.
62    pub sets_count: u32,
63
64    /// The minimum number of repetitions targeted for each set.
65    pub min_reps: u32,
66
67    /// The maximum number of repetitions targeted for each set.
68    /// Can be set to the same value as `min_reps`.
69    pub max_reps: u32,
70}
71
72/// Represents a single set performed within an exercise session, which may contain
73/// multiple attempts if the weight or reps were modified mid-set.
74#[derive(Debug)]
75pub struct Set {
76    /// A vector of `Attempt` instances, representing individual attempts within the set.
77    ///
78    /// Multiple attempts can occur if adjustments are made within the set due to difficulty.
79    pub attempts: Vec<Attempt>,
80}
81
82/// Represents an attempt within a set, specifying the weight used and the repetitions completed.
83///
84/// Part of a `Set` instance.
85#[derive(Debug)]
86pub struct Attempt {
87    /// The weight lifted in this attempt, in kilograms.
88    pub weight: u32,
89
90    /// The number of repetitions completed in this attempt.
91    pub reps: u32,
92}
93
94#[derive(Parser)]
95#[grammar = "grammar.pest"]
96pub struct Grammar;
97
98pub fn parse_exercise_log(input: &str) -> Result<Vec<ExerciseRecord>, GymDataParserError> {
99    let mut records = Vec::new();
100
101    let mut parsed = Grammar::parse(Rule::file, input)
102        .map_err(|e| GymDataParserError::ParseError(Box::from(e)))?;
103
104    let file_rule = parsed
105        .next()
106        .ok_or(GymDataParserError::FileContentParseError)?;
107
108    for record in file_rule.into_inner() {
109        if record.as_rule() == Rule::record {
110            let mut date: Option<NaiveDate> = None;
111            let mut exercise_name: Option<String> = None;
112            let mut target: Option<TargetReps> = None;
113            let mut sets: Vec<Set> = Vec::new();
114
115            for field in record.into_inner() {
116                match field.as_rule() {
117                    Rule::date => {
118                        let date_str = field.as_str();
119                        let parsed_date = NaiveDate::parse_from_str(date_str, "%d.%m.%Y")?;
120                        date = Some(parsed_date);
121                    }
122                    Rule::exercise_name => {
123                        let name = field.as_str().trim().to_string();
124                        exercise_name = Some(name);
125                    }
126                    Rule::target => {
127                        let mut parts = field.into_inner();
128                        let sets_count_str = parts
129                            .next()
130                            .ok_or(GymDataParserError::TargetParseError)?
131                            .as_str()
132                            .trim();
133                        let min_reps_str = parts
134                            .next()
135                            .ok_or(GymDataParserError::TargetParseError)?
136                            .as_str()
137                            .trim();
138                        let max_reps_str = parts
139                            .next()
140                            .ok_or(GymDataParserError::TargetParseError)?
141                            .as_str()
142                            .trim();
143
144                        let sets_count = sets_count_str.parse::<u32>()?;
145                        let min_reps = min_reps_str.parse::<u32>()?;
146                        let max_reps = max_reps_str.parse::<u32>()?;
147
148                        target = Some(TargetReps {
149                            sets_count,
150                            min_reps,
151                            max_reps,
152                        });
153                    }
154                    Rule::set_group => {
155                        for set_group in field.into_inner() {
156                            let mut attempts = Vec::new();
157                            for set in set_group.into_inner() {
158                                let mut set_parts = set.into_inner();
159                                let weight_str = set_parts
160                                    .next()
161                                    .ok_or(GymDataParserError::SetGroupParseError)?
162                                    .as_str()
163                                    .trim();
164                                let reps_str = set_parts
165                                    .next()
166                                    .ok_or(GymDataParserError::SetGroupParseError)?
167                                    .as_str()
168                                    .trim();
169
170                                let weight = weight_str.parse::<u32>()?;
171                                let reps = reps_str.parse::<u32>()?;
172
173                                attempts.push(Attempt { weight, reps });
174                            }
175                            sets.push(Set { attempts });
176                        }
177                    }
178                    _ => {}
179                }
180            }
181
182            let date = date.ok_or(GymDataParserError::MissingDateError)?;
183            let exercise_name = exercise_name.ok_or(GymDataParserError::ExerciseNameParseError)?;
184            let target = target.ok_or(GymDataParserError::TargetParseError)?;
185
186            records.push(ExerciseRecord {
187                date,
188                exercise_name,
189                target,
190                sets,
191            });
192        }
193    }
194
195    Ok(records)
196}