Skip to main content

medical_parser/
lib.rs

1use pest::Parser;
2use pest_derive::Parser;
3use thiserror::Error;
4
5#[derive(Parser)]
6#[grammar = "grammar.pest"]
7pub struct Grammar;
8
9/// Represents a single patient record.
10#[derive(Debug)]
11pub struct Patient {
12    pub name: String,
13    pub age: u8,
14    pub visits: Vec<Visit>,
15}
16
17/// Represents one medical visit.
18#[derive(Debug)]
19pub struct Visit {
20    pub diagnosis: String,
21    pub temperature: f32,
22    pub notes: String,
23}
24
25#[derive(Error, Debug)]
26pub enum ParseError {
27    #[error(transparent)]
28    PestError(#[from] Box<pest::error::Error<Rule>>),
29
30    #[error("Invalid data format: {0}")]
31    DataError(String),
32}
33
34/// Parses a full medical document and returns a list of patients.
35pub fn parse_medical_document(input: &str) -> Result<Vec<Patient>, ParseError> {
36    let pairs =
37        Grammar::parse(Rule::file, input).map_err(|e| ParseError::PestError(Box::new(e)))?;
38    let mut patients = Vec::new();
39
40    for p in pairs {
41        for patient in p.into_inner() {
42            if patient.as_rule() == Rule::patient {
43                patients.push(parse_patient(patient)?);
44            }
45        }
46    }
47
48    Ok(patients)
49}
50
51/// Parses a single `<patient>` element.
52fn parse_patient(pair: pest::iterators::Pair<Rule>) -> Result<Patient, ParseError> {
53    let mut name = String::new();
54    let mut age = 0;
55    let mut visits = Vec::new();
56
57    for part in pair.into_inner() {
58        match part.as_rule() {
59            Rule::name => name = part.into_inner().as_str().trim().to_string(),
60            Rule::age => {
61                age = part
62                    .into_inner()
63                    .as_str()
64                    .parse()
65                    .map_err(|_| ParseError::DataError("Invalid age value".into()))?;
66            }
67            Rule::visit => visits.push(parse_visit(part)?),
68            _ => {}
69        }
70    }
71
72    Ok(Patient { name, age, visits })
73}
74
75/// Parses a single `<visit>` element.
76fn parse_visit(pair: pest::iterators::Pair<Rule>) -> Result<Visit, ParseError> {
77    let mut diagnosis = String::new();
78    let mut temperature = 0.0;
79    let mut notes = String::new();
80
81    for part in pair.into_inner() {
82        match part.as_rule() {
83            Rule::diagnosis => diagnosis = part.into_inner().as_str().trim().to_string(),
84            Rule::temperature => {
85                temperature = part
86                    .into_inner()
87                    .as_str()
88                    .parse()
89                    .map_err(|_| ParseError::DataError("Invalid temperature".into()))?;
90            }
91            Rule::notes => notes = part.into_inner().as_str().trim().to_string(),
92            _ => {}
93        }
94    }
95
96    Ok(Visit {
97        diagnosis,
98        temperature,
99        notes,
100    })
101}
102
103/// Prints patient and visit data in a readable format.
104pub fn print_patients(patients: &[Patient]) {
105    for p in patients {
106        println!("{} (age {})", p.name, p.age);
107        for v in &p.visits {
108            println!("  {}, {} °C — {}", v.diagnosis, v.temperature, v.notes);
109        }
110        println!();
111    }
112}
113
114/// Returns the number of patients in the document.
115pub fn count_patients(patients: &[Patient]) -> usize {
116    patients.len()
117}
118
119/// Returns the total number of visits across all patients.
120pub fn count_total_visits(patients: &[Patient]) -> usize {
121    patients.iter().map(|p| p.visits.len()).sum()
122}