workout_note_parser/
lib.rs

1//! This module contains the implementation of a parser for workout data.
2//!
3//! The parser is implemented using the pest parser generator and is used to parse
4//! workout data in a custom format. The parsed data is then used to generate
5//! workout plans and track progress.
6
7use pest::{iterators::Pair, Parser};
8use serde::{Deserialize, Serialize};
9
10#[macro_use]
11extern crate pest_derive;
12
13pub mod error;
14use error::Error;
15
16#[derive(Parser)]
17#[grammar = "workout.pest"]
18struct WorkoutParser;
19
20/// Represents a set in weightlifting, consisting of a weight and the number of repetitions performed.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Set {
23    /// The weight used for this set.
24    pub weight: f32,
25    /// The number of repetitions performed for this set.
26    pub n_reps: i32,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30/// A struct representing an exercise, actually, a single set
31pub struct Exercise {
32    // the name of your exercise
33    pub name: String,
34    // this is a vector because you can do two sets without a rest, and this counts as a single set
35    pub sets: Vec<Set>,
36    // a comment about the exercise
37    pub comment: Option<String>,
38}
39
40/// Parses a set of exercises from a `Pair` object.
41///
42/// # Arguments
43///
44/// * `name` - A `String` representing the name of the exercise.
45/// * `pair` - A `Pair` object representing the set of exercises to parse.
46///
47/// # Returns
48///
49/// Returns a `Result` containing an `Exercise` object if parsing was successful, or an `Error` if parsing failed.
50pub fn parse_set(name: String, pair: Pair<'_, Rule>) -> Result<Exercise, Error> {
51    if pair.as_rule() != Rule::set {
52        return Err(Error::ExpectedRule(Rule::set));
53    }
54    let mut pairella = Some(pair);
55
56    let mut sets = Vec::new();
57    let mut comment = None;
58    while let Some(pair) = pairella {
59        match pair.as_rule() {
60            Rule::comment => {
61                comment = Some(pair.as_str().trim().to_string());
62                pairella = pair.into_inner().next();
63            }
64            Rule::set => {
65                let mut pairs = pair.into_inner();
66                let weight = pairs
67                    .next()
68                    .ok_or(Error::ExpectedRule(Rule::weight))?
69                    .as_str()
70                    .trim()
71                    .parse::<f32>()?;
72                let n_reps = pairs
73                    .next()
74                    .ok_or(Error::ExpectedRule(Rule::reps))?
75                    .as_str()
76                    .trim()
77                    .parse::<i32>()?;
78
79                sets.push(Set { weight, n_reps });
80
81                pairella = pairs.next();
82            }
83            _ => return Err(Error::UnexpectedRule(pair.as_rule())),
84        }
85    }
86
87    Ok(Exercise {
88        name,
89        sets,
90        comment,
91    })
92}
93
94/// Parses an exercise from a `Pair` and returns a vector of `Exercise`s.
95///
96/// # Arguments
97///
98/// * `pair` - A `Pair` that represents an exercise.
99///
100/// # Returns
101///
102/// A `Result` containing a vector of `Exercise`s if parsing was successful, or an `Error` if parsing failed.
103pub fn get_exercise_from_pairs(pair: Pair<'_, Rule>) -> Result<Vec<Exercise>, Error> {
104    if pair.as_rule() != Rule::exercise {
105        return Err(Error::ExpectedRule(Rule::exercise));
106    }
107
108    let mut rules = pair.into_inner();
109    let rule = rules.next().ok_or(Error::ExpectedRule(Rule::name))?;
110    if rule.as_rule() != Rule::name {
111        return Err(Error::ExpectedRule(Rule::name));
112    }
113
114    let name = rule.as_str().to_string();
115
116    rules
117        .filter(|r| r.as_rule() == Rule::set)
118        .map(|r| parse_set(name.clone(), r))
119        .collect()
120}
121
122/// Parses a workout input string and returns a vector of exercises.
123///
124/// # Arguments
125///
126/// * `input` - A string slice that holds the workout input.
127///
128/// # Returns
129///
130/// * `Result<Vec<Exercise>, Error>` - A `Result` that holds a vector of `Exercise`s if parsing was successful, or an `Error` if parsing failed.
131pub fn parse_workout(input: &str) -> Result<Vec<Exercise>, Error> {
132    let mut parsed = WorkoutParser::parse(Rule::workout, input).map_err(Box::new)?;
133
134    let workout = parsed.next().ok_or(Error::ExpectedRule(Rule::workout))?;
135    if workout.as_rule() != Rule::workout {
136        return Err(Error::ExpectedRule(Rule::workout));
137    }
138
139    let vec = workout
140        .into_inner()
141        .filter(|r| r.as_rule() == Rule::exercise)
142        .map(get_exercise_from_pairs)
143        .collect::<Result<Vec<_>, _>>()?;
144    let vec = vec.into_iter().flatten().collect::<Vec<_>>();
145
146    Ok(vec)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use anyhow::Result;
153    use pest::Parser;
154
155    #[test]
156    fn weight() -> Result<()> {
157        let parsed = WorkoutParser::parse(Rule::weight, "35        ")?;
158        assert!(parsed.as_str() == "35");
159
160        Ok(())
161    }
162
163    #[test]
164    fn set() -> Result<()> {
165        WorkoutParser::parse(Rule::set, "35.5 x 10")?;
166
167        Ok(())
168    }
169
170    #[test]
171    fn extended_set() -> Result<()> {
172        WorkoutParser::parse(Rule::set, "35.5 x 10 + 40 x 8")?;
173
174        WorkoutParser::parse(Rule::set, "35.5 x 10 + 40 x 8 # this was really tough")?;
175
176        Ok(())
177    }
178
179    #[test]
180    fn workout() -> Result<()> {
181        let input = r#"
182        vert block
183        35.5 x 10
184        35.5 x 10 + 40 x 8
185        35.5 x 10 + 40 x 8 this was really tough
186    
187        vert block
188        35.5 x 10
189        35.5 x 10 + 40 x 8
190        35.5 x 10 + 40 x 8 this was really tough
191
192        vert block
193        35.5 x 10
194        35.5 x 10 + 40 x 8
195        35.5 x 10 + 40 x 8 this was really tough
196        "#;
197
198        let parsed = WorkoutParser::parse(Rule::workout, input)?;
199        println!("Parsed: {:#?}", parsed);
200
201        Ok(())
202    }
203
204    #[test]
205    #[should_panic]
206    fn bad_workout() {
207        let input = r#"
208        vert block
209        35.5 x
210        35.5 x 10 + 40 x 8
211        35.5 x 10 + 40 x 8 this was really tough
212        "#;
213
214        WorkoutParser::parse(Rule::workout, input).unwrap();
215    }
216}