use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub struct DydRecord {
pub model_type: String,
pub machine_id: String,
pub bus_number: u32,
pub params: Vec<f64>,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("parse error on line {line}: {message}")]
ParseError { line: usize, message: String },
#[error("unknown model type: {0}")]
UnknownModel(String),
}
pub fn loads(text: &str) -> Result<Vec<DydRecord>, Error> {
let mut records = Vec::new();
for (line_idx, raw_line) in text.lines().enumerate() {
let line_num = line_idx + 1;
let trimmed = raw_line.trim();
if trimmed.is_empty() || trimmed.starts_with('@') {
continue;
}
let record = parse_line(trimmed, line_num)?;
records.push(record);
}
Ok(records)
}
fn parse_line(line: &str, line_num: usize) -> Result<DydRecord, Error> {
let (header, param_str) = match line.split_once('/') {
Some(pair) => pair,
None => {
return Err(Error::ParseError {
line: line_num,
message: "missing '/' separator between header and parameters".into(),
});
}
};
let header = header.trim();
let mut tokens = header.splitn(2, char::is_whitespace);
let model_type = match tokens.next() {
Some(t) if !t.is_empty() => t.to_uppercase(),
_ => {
return Err(Error::ParseError {
line: line_num,
message: "missing model type".into(),
});
}
};
let rest = tokens.next().unwrap_or("").trim();
let machine_id;
let after_id;
if let Some(open_pos) = rest.find('"') {
let after_open = &rest[open_pos + 1..];
if let Some(close_pos) = after_open.find('"') {
machine_id = after_open[..close_pos].to_string();
after_id = after_open[close_pos + 1..].trim();
} else {
return Err(Error::ParseError {
line: line_num,
message: "unterminated machine ID quote".into(),
});
}
} else {
return Err(Error::ParseError {
line: line_num,
message: "missing quoted machine ID".into(),
});
}
let bus_number_str = after_id.split_whitespace().next().unwrap_or("");
let bus_number: u32 = bus_number_str.parse().map_err(|_| Error::ParseError {
line: line_num,
message: format!("invalid bus number: '{bus_number_str}'"),
})?;
let params: Result<Vec<f64>, _> = param_str
.split_whitespace()
.map(|tok| tok.parse::<f64>())
.collect();
let params = params.map_err(|e| Error::ParseError {
line: line_num,
message: format!("invalid parameter value: {e}"),
})?;
Ok(DydRecord {
model_type,
machine_id,
bus_number,
params,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dyd_parse_genrou_record() {
let text =
r#" GENROU "1" 1 / 8.0 0.03 0.4 0.05 6.5 0.0 1.8 1.7 0.3 0.55 0.25 0.2 0.1 0.13"#;
let records = loads(text).unwrap();
assert_eq!(records.len(), 1);
let r = &records[0];
assert_eq!(r.model_type, "GENROU");
assert_eq!(r.machine_id, "1");
assert_eq!(r.bus_number, 1);
assert_eq!(r.params.len(), 14);
assert!((r.params[0] - 8.0).abs() < 1e-10); assert!((r.params[4] - 6.5).abs() < 1e-10); }
#[test]
fn test_dyd_comment_lines_ignored() {
let text = "@ This is a comment\n GENCLS \"1\" 2 / 6.5 0.0\n@ another comment\n";
let records = loads(text).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].model_type, "GENCLS");
}
#[test]
fn test_dyd_blank_lines_ignored() {
let text = "\n\n GENCLS \"1\" 3 / 5.0 2.0\n\n";
let records = loads(text).unwrap();
assert_eq!(records.len(), 1);
}
#[test]
fn test_dyd_multiple_records() {
let text = concat!(
" GENROU \"1\" 1 / 8.0 0.03 0.4 0.05 6.5 0.0 1.8 1.7 0.3 0.55 0.25 0.2 0.1 0.13\n",
" GENCLS \"1\" 2 / 5.0 0.0\n",
" EXST1 \"1\" 1 / 0.01 200.0 0.02 0.0 0.0 5.0 -5.0 0.0\n",
);
let records = loads(text).unwrap();
assert_eq!(records.len(), 3);
assert_eq!(records[0].model_type, "GENROU");
assert_eq!(records[1].model_type, "GENCLS");
assert_eq!(records[2].model_type, "EXST1");
}
#[test]
fn test_dyd_missing_separator_error() {
let text = " GENROU \"1\" 1 8.0 0.03";
let result = loads(text);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("separator"), "Got: {err}");
}
}