use crate::error::{parse_error, Result};
use crate::eulumdat::{Eulumdat, LampSet, Symmetry, TypeIndicator};
pub struct Parser;
impl Parser {
pub fn parse(content: &str) -> Result<Eulumdat> {
let mut ldt = Eulumdat::new();
let lines: Vec<&str> = content.lines().collect();
let mut idx = 0;
let next_line = |i: &mut usize| -> Result<&str> {
if *i >= lines.len() {
return Err(parse_error(*i, "unexpected end of file"));
}
let line = lines[*i];
*i += 1;
Ok(line)
};
ldt.identification = next_line(&mut idx)?.to_string();
let ityp_line = idx;
let ityp = Self::parse_int(next_line(&mut idx)?, ityp_line)?;
ldt.type_indicator = TypeIndicator::from_int(ityp)?;
let isym_line = idx;
let isym = Self::parse_int(next_line(&mut idx)?, isym_line)?;
ldt.symmetry = Symmetry::from_int(isym)?;
let nc_line = idx;
ldt.num_c_planes = Self::parse_int(next_line(&mut idx)?, nc_line)? as usize;
let dc_line = idx;
ldt.c_plane_distance = Self::parse_float(next_line(&mut idx)?, dc_line)?;
let ng_line = idx;
ldt.num_g_planes = Self::parse_int(next_line(&mut idx)?, ng_line)? as usize;
let dg_line = idx;
ldt.g_plane_distance = Self::parse_float(next_line(&mut idx)?, dg_line)?;
ldt.measurement_report_number = next_line(&mut idx)?.to_string();
ldt.luminaire_name = next_line(&mut idx)?.to_string();
ldt.luminaire_number = next_line(&mut idx)?.to_string();
ldt.file_name = next_line(&mut idx)?.to_string();
ldt.date_user = next_line(&mut idx)?.to_string();
let l_line = idx;
ldt.length = Self::parse_float(next_line(&mut idx)?, l_line)?;
let b_line = idx;
ldt.width = Self::parse_float(next_line(&mut idx)?, b_line)?;
let h_line = idx;
ldt.height = Self::parse_float(next_line(&mut idx)?, h_line)?;
let la_line = idx;
ldt.luminous_area_length = Self::parse_float(next_line(&mut idx)?, la_line)?;
let b1_line = idx;
ldt.luminous_area_width = Self::parse_float(next_line(&mut idx)?, b1_line)?;
let hc0_line = idx;
ldt.height_c0 = Self::parse_float(next_line(&mut idx)?, hc0_line)?;
let hc90_line = idx;
ldt.height_c90 = Self::parse_float(next_line(&mut idx)?, hc90_line)?;
let hc180_line = idx;
ldt.height_c180 = Self::parse_float(next_line(&mut idx)?, hc180_line)?;
let hc270_line = idx;
ldt.height_c270 = Self::parse_float(next_line(&mut idx)?, hc270_line)?;
let dff_line = idx;
ldt.downward_flux_fraction = Self::parse_float(next_line(&mut idx)?, dff_line)?;
let lorl_line = idx;
ldt.light_output_ratio = Self::parse_float(next_line(&mut idx)?, lorl_line)?;
let cfli_line = idx;
ldt.conversion_factor = Self::parse_float(next_line(&mut idx)?, cfli_line)?;
let tilt_line = idx;
ldt.tilt_angle = Self::parse_float(next_line(&mut idx)?, tilt_line)?;
let n_line = idx;
let num_lamp_sets = Self::parse_int(next_line(&mut idx)?, n_line)? as usize;
for _ in 0..num_lamp_sets {
let mut lamp_set = LampSet::default();
let nl_line = idx;
lamp_set.num_lamps = Self::parse_int(next_line(&mut idx)?, nl_line)?;
lamp_set.lamp_type = next_line(&mut idx)?.to_string();
let tlf_line = idx;
lamp_set.total_luminous_flux = Self::parse_float(next_line(&mut idx)?, tlf_line)?;
lamp_set.color_appearance = next_line(&mut idx)?.to_string();
lamp_set.color_rendering_group = next_line(&mut idx)?.to_string();
let wb_line = idx;
lamp_set.wattage_with_ballast = Self::parse_float(next_line(&mut idx)?, wb_line)?;
ldt.lamp_sets.push(lamp_set);
}
for i in 0..10 {
let dr_line = idx;
ldt.direct_ratios[i] = Self::parse_float(next_line(&mut idx)?, dr_line)?;
}
for _ in 0..ldt.num_c_planes {
let c_line = idx;
let angle = Self::parse_float(next_line(&mut idx)?, c_line)?;
ldt.c_angles.push(angle);
}
for _ in 0..ldt.num_g_planes {
let g_line = idx;
let angle = Self::parse_float(next_line(&mut idx)?, g_line)?;
ldt.g_angles.push(angle);
}
let mc = ldt.symmetry.calc_mc(ldt.num_c_planes);
for c in 0..mc {
let mut row = Vec::with_capacity(ldt.num_g_planes);
for g in 0..ldt.num_g_planes {
let i_line = idx;
let intensity = Self::parse_float(next_line(&mut idx)?, i_line).map_err(|_| {
parse_error(
i_line,
format!("error reading intensity at C[{}] G[{}]", c, g),
)
})?;
row.push(intensity);
}
ldt.intensities.push(row);
}
Ok(ldt)
}
fn parse_int(s: &str, line: usize) -> Result<i32> {
Self::parse_number(s)
.map(|f| f as i32)
.map_err(|_| parse_error(line, format!("expected integer, got '{}'", s)))
}
fn parse_float(s: &str, line: usize) -> Result<f64> {
Self::parse_number(s)
.map_err(|_| parse_error(line, format!("expected number, got '{}'", s)))
}
fn parse_number(s: &str) -> Result<f64> {
let cleaned = s.trim().replace('_', "").replace(',', ".");
cleaned
.parse::<f64>()
.map_err(|_| parse_error(0, format!("cannot parse '{}' as number", s)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_number() {
assert!((Parser::parse_number("123.45").unwrap() - 123.45).abs() < 0.001);
assert!((Parser::parse_number("123,45").unwrap() - 123.45).abs() < 0.001);
assert!((Parser::parse_number(" 123.45 ").unwrap() - 123.45).abs() < 0.001);
assert!((Parser::parse_number("1_000").unwrap() - 1000.0).abs() < 0.001);
}
#[test]
fn test_parse_minimal() {
let content = r#"Test Luminaire
1
1
1
0
19
5
Report123
Test Light
LUM-001
test.ldt
2024-01-01
100
50
30
80
40
0
0
0
0
100
85
1
0
1
1
LED Module
1000
3000K
80
10
0.5
0.55
0.6
0.65
0.7
0.75
0.8
0.82
0.85
0.88
0
0
5
10
15
20
25
30
35
40
45
50
55
60
65
70
75
80
85
90
100
200
300
400
350
300
250
200
150
100
75
50
25
15
10
5
2
1
0
"#;
let ldt = Parser::parse(content).unwrap();
assert_eq!(ldt.identification, "Test Luminaire");
assert_eq!(ldt.type_indicator, TypeIndicator::PointSourceSymmetric);
assert_eq!(ldt.symmetry, Symmetry::VerticalAxis);
assert_eq!(ldt.num_g_planes, 19);
assert_eq!(ldt.luminaire_name, "Test Light");
assert_eq!(ldt.lamp_sets.len(), 1);
assert_eq!(ldt.lamp_sets[0].num_lamps, 1);
assert!((ldt.lamp_sets[0].total_luminous_flux - 1000.0).abs() < 0.001);
}
}