use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use super::download::ElpFilePaths;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Coordinate {
Longitude,
Latitude,
Distance,
}
impl Coordinate {
#[allow(dead_code)]
pub fn index(&self) -> usize {
match self {
Coordinate::Longitude => 0,
Coordinate::Latitude => 1,
Coordinate::Distance => 2,
}
}
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Coordinate::Longitude => write!(f, "Longitude"),
Coordinate::Latitude => write!(f, "Latitude"),
Coordinate::Distance => write!(f, "Distance"),
}
}
}
#[derive(Debug, Clone)]
pub struct MainTerm {
pub delaunay: [i32; 4],
pub coeffs: [f64; 7],
}
impl MainTerm {
pub fn amplitude(&self) -> f64 {
self.coeffs[0].abs()
}
}
#[derive(Debug, Clone)]
pub struct PertTerm {
pub sin_coeff: f64,
pub cos_coeff: f64,
pub multipliers: [i32; 16],
}
impl PertTerm {
pub fn amplitude(&self) -> f64 {
libm::sqrt(self.sin_coeff.powi(2) + self.cos_coeff.powi(2))
}
pub fn phase(&self) -> f64 {
libm::atan2(self.cos_coeff, self.sin_coeff)
}
}
#[derive(Debug, Clone)]
pub struct MainSeries {
pub coordinate: Coordinate,
pub terms: Vec<MainTerm>,
}
#[derive(Debug, Clone)]
pub struct PertBlock {
pub time_power: u8,
pub terms: Vec<PertTerm>,
}
#[derive(Debug, Clone)]
pub struct PertSeries {
pub coordinate: Coordinate,
pub blocks: Vec<PertBlock>,
}
#[derive(Debug, Clone)]
pub struct ElpData {
pub main: [MainSeries; 3],
pub pert: [PertSeries; 3],
}
impl ElpData {
pub fn total_main_terms(&self) -> usize {
self.main.iter().map(|s| s.terms.len()).sum()
}
pub fn total_pert_terms(&self) -> usize {
self.pert
.iter()
.flat_map(|s| s.blocks.iter())
.map(|b| b.terms.len())
.sum()
}
pub fn total_terms(&self) -> usize {
self.total_main_terms() + self.total_pert_terms()
}
}
#[derive(Debug)]
pub enum ParseError {
IoError(std::io::Error),
InvalidHeader(String),
InvalidMainTerm(String),
InvalidPertTerm(String),
InvalidFormat(String),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::IoError(e) => write!(f, "IO error: {}", e),
ParseError::InvalidHeader(s) => write!(f, "Invalid header: {}", s),
ParseError::InvalidMainTerm(s) => write!(f, "Invalid main term: {}", s),
ParseError::InvalidPertTerm(s) => write!(f, "Invalid pert term: {}", s),
ParseError::InvalidFormat(s) => write!(f, "Invalid format: {}", s),
}
}
}
impl std::error::Error for ParseError {}
impl From<std::io::Error> for ParseError {
fn from(e: std::io::Error) -> Self {
ParseError::IoError(e)
}
}
fn parse_main_header(line: &str) -> Result<(String, usize), ParseError> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
return Err(ParseError::InvalidHeader(format!(
"Main header too short: '{}'",
line
)));
}
let term_count: usize = parts
.last()
.ok_or_else(|| ParseError::InvalidHeader("No term count".to_string()))?
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid term count in: '{}'", line)))?;
Ok((line.to_string(), term_count))
}
fn parse_main_term(line: &str) -> Result<MainTerm, ParseError> {
if line.len() < 80 {
return Err(ParseError::InvalidMainTerm(format!(
"Line too short ({}): '{}'",
line.len(),
line
)));
}
let mut delaunay = [0i32; 4];
for (i, d) in delaunay.iter_mut().enumerate() {
let start = i * 3;
let end = start + 3;
let s = &line[start..end];
*d = s.trim().parse().map_err(|_| {
ParseError::InvalidMainTerm(format!("Invalid delaunay[{}]: '{}'", i, s))
})?;
}
let mut coeffs = [0.0f64; 7];
let coeff_start = 14;
coeffs[0] = line[coeff_start..coeff_start + 13]
.trim()
.parse()
.map_err(|e| {
ParseError::InvalidMainTerm(format!(
"Invalid coeff[0]: '{}' - {}",
&line[coeff_start..coeff_start + 13],
e
))
})?;
for (i, c) in coeffs.iter_mut().enumerate().skip(1) {
let start = coeff_start + 13 + (i - 1) * 12;
let end = start + 12;
if end <= line.len() {
let s = &line[start..end];
*c = s.trim().parse().unwrap_or(0.0);
}
}
Ok(MainTerm { delaunay, coeffs })
}
fn parse_main_file(path: &Path, coord: Coordinate) -> Result<MainSeries, ParseError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let first_line = lines
.next()
.ok_or_else(|| ParseError::InvalidFormat("Empty file".to_string()))??;
let (_, term_count) = parse_main_header(&first_line)?;
let mut terms = Vec::with_capacity(term_count);
for line_result in lines {
let line = line_result?;
if line.trim().is_empty() {
continue;
}
let term = parse_main_term(&line)?;
terms.push(term);
}
if terms.len() != term_count {
return Err(ParseError::InvalidFormat(format!(
"Expected {} terms, found {}",
term_count,
terms.len()
)));
}
Ok(MainSeries {
coordinate: coord,
terms,
})
}
fn parse_pert_header(line: &str) -> Result<(usize, u8), ParseError> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
return Err(ParseError::InvalidHeader(format!(
"Pert header too short: '{}'",
line
)));
}
let term_count: usize = parts[parts.len() - 2]
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid term count in: '{}'", line)))?;
let time_power: u8 = parts[parts.len() - 1]
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid time power in: '{}'", line)))?;
Ok((term_count, time_power))
}
fn parse_fortran_double(s: &str) -> Result<f64, ParseError> {
let s = s.trim();
let s = s.replace('D', "E").replace('d', "e");
s.parse()
.map_err(|_| ParseError::InvalidPertTerm(format!("Invalid double: '{}'", s)))
}
fn parse_pert_term(line: &str) -> Result<PertTerm, ParseError> {
if line.len() < 90 {
return Err(ParseError::InvalidPertTerm(format!(
"Line too short ({}): '{}'",
line.len(),
line
)));
}
let sin_coeff = parse_fortran_double(&line[5..25])?;
let cos_coeff = parse_fortran_double(&line[25..45])?;
let mut multipliers = [0i32; 16];
for (i, m) in multipliers.iter_mut().enumerate() {
let start = 45 + i * 3;
let end = start + 3;
if end <= line.len() {
let s = &line[start..end];
*m = s.trim().parse().unwrap_or(0);
}
}
Ok(PertTerm {
sin_coeff,
cos_coeff,
multipliers,
})
}
fn parse_pert_file(path: &Path, coord: Coordinate) -> Result<PertSeries, ParseError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut lines = reader.lines().peekable();
let mut blocks = Vec::new();
while let Some(line_result) = lines.next() {
let line = line_result?;
if line.contains("PERTURBATIONS")
|| line.contains("LONGITUDE")
|| line.contains("LATITUDE")
|| line.contains("DISTANCE")
{
let (term_count, time_power) = parse_pert_header(&line)?;
let mut terms = Vec::with_capacity(term_count);
for _ in 0..term_count {
if let Some(term_line_result) = lines.next() {
let term_line = term_line_result?;
let term = parse_pert_term(&term_line)?;
terms.push(term);
}
}
blocks.push(PertBlock { time_power, terms });
}
}
Ok(PertSeries {
coordinate: coord,
blocks,
})
}
pub fn parse_files(paths: &ElpFilePaths) -> Result<ElpData, ParseError> {
println!(" Parsing ELP_MAIN.S1 (Longitude)...");
let main_lon = parse_main_file(&paths.main_longitude, Coordinate::Longitude)?;
println!(" {} terms", main_lon.terms.len());
println!(" Parsing ELP_MAIN.S2 (Latitude)...");
let main_lat = parse_main_file(&paths.main_latitude, Coordinate::Latitude)?;
println!(" {} terms", main_lat.terms.len());
println!(" Parsing ELP_MAIN.S3 (Distance)...");
let main_dist = parse_main_file(&paths.main_distance, Coordinate::Distance)?;
println!(" {} terms", main_dist.terms.len());
println!(" Parsing ELP_PERT.S1 (Longitude perturbations)...");
let pert_lon = parse_pert_file(&paths.pert_longitude, Coordinate::Longitude)?;
let pert_lon_count: usize = pert_lon.blocks.iter().map(|b| b.terms.len()).sum();
println!(
" {} blocks, {} terms",
pert_lon.blocks.len(),
pert_lon_count
);
println!(" Parsing ELP_PERT.S2 (Latitude perturbations)...");
let pert_lat = parse_pert_file(&paths.pert_latitude, Coordinate::Latitude)?;
let pert_lat_count: usize = pert_lat.blocks.iter().map(|b| b.terms.len()).sum();
println!(
" {} blocks, {} terms",
pert_lat.blocks.len(),
pert_lat_count
);
println!(" Parsing ELP_PERT.S3 (Distance perturbations)...");
let pert_dist = parse_pert_file(&paths.pert_distance, Coordinate::Distance)?;
let pert_dist_count: usize = pert_dist.blocks.iter().map(|b| b.terms.len()).sum();
println!(
" {} blocks, {} terms",
pert_dist.blocks.len(),
pert_dist_count
);
Ok(ElpData {
main: [main_lon, main_lat, main_dist],
pert: [pert_lon, pert_lat, pert_dist],
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_coordinate_index() {
assert_eq!(Coordinate::Longitude.index(), 0);
assert_eq!(Coordinate::Latitude.index(), 1);
assert_eq!(Coordinate::Distance.index(), 2);
}
#[test]
fn test_coordinate_display() {
assert_eq!(format!("{}", Coordinate::Longitude), "Longitude");
assert_eq!(format!("{}", Coordinate::Latitude), "Latitude");
assert_eq!(format!("{}", Coordinate::Distance), "Distance");
}
#[test]
fn test_main_term_amplitude() {
let term = MainTerm {
delaunay: [0, 0, 1, 0],
coeffs: [22639.55, 0.0, 0.0, 412529.61, 0.0, 0.0, 0.0],
};
assert!((term.amplitude() - 22639.55).abs() < 0.01);
}
#[test]
fn test_main_term_amplitude_negative() {
let term = MainTerm {
delaunay: [0, 0, 0, 0],
coeffs: [-12345.67, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
};
assert!((term.amplitude() - 12345.67).abs() < 0.01);
}
#[test]
fn test_pert_term_amplitude() {
let term = PertTerm {
sin_coeff: -12.749,
cos_coeff: 6.369,
multipliers: [0; 16],
};
let amp = term.amplitude();
assert!(amp > 14.0 && amp < 15.0);
}
#[test]
fn test_pert_term_phase() {
let term = PertTerm {
sin_coeff: 1.0,
cos_coeff: 0.0,
multipliers: [0; 16],
};
assert!((term.phase() - 0.0).abs() < 1e-10);
let term2 = PertTerm {
sin_coeff: 0.0,
cos_coeff: 1.0,
multipliers: [0; 16],
};
assert!((term2.phase() - std::f64::consts::FRAC_PI_2).abs() < 1e-10);
}
#[test]
fn test_elp_data_total_main_terms() {
let data = ElpData {
main: [
MainSeries {
coordinate: Coordinate::Longitude,
terms: vec![
MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
},
MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
},
],
},
MainSeries {
coordinate: Coordinate::Latitude,
terms: vec![MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
}],
},
MainSeries {
coordinate: Coordinate::Distance,
terms: vec![
MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
},
MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
},
MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
},
],
},
],
pert: [
PertSeries {
coordinate: Coordinate::Longitude,
blocks: vec![],
},
PertSeries {
coordinate: Coordinate::Latitude,
blocks: vec![],
},
PertSeries {
coordinate: Coordinate::Distance,
blocks: vec![],
},
],
};
assert_eq!(data.total_main_terms(), 6);
}
#[test]
fn test_elp_data_total_pert_terms() {
let data = ElpData {
main: [
MainSeries {
coordinate: Coordinate::Longitude,
terms: vec![],
},
MainSeries {
coordinate: Coordinate::Latitude,
terms: vec![],
},
MainSeries {
coordinate: Coordinate::Distance,
terms: vec![],
},
],
pert: [
PertSeries {
coordinate: Coordinate::Longitude,
blocks: vec![PertBlock {
time_power: 0,
terms: vec![
PertTerm {
sin_coeff: 0.0,
cos_coeff: 0.0,
multipliers: [0; 16],
},
PertTerm {
sin_coeff: 0.0,
cos_coeff: 0.0,
multipliers: [0; 16],
},
],
}],
},
PertSeries {
coordinate: Coordinate::Latitude,
blocks: vec![PertBlock {
time_power: 1,
terms: vec![PertTerm {
sin_coeff: 0.0,
cos_coeff: 0.0,
multipliers: [0; 16],
}],
}],
},
PertSeries {
coordinate: Coordinate::Distance,
blocks: vec![],
},
],
};
assert_eq!(data.total_pert_terms(), 3);
}
#[test]
fn test_elp_data_total_terms() {
let data = ElpData {
main: [
MainSeries {
coordinate: Coordinate::Longitude,
terms: vec![MainTerm {
delaunay: [0; 4],
coeffs: [0.0; 7],
}],
},
MainSeries {
coordinate: Coordinate::Latitude,
terms: vec![],
},
MainSeries {
coordinate: Coordinate::Distance,
terms: vec![],
},
],
pert: [
PertSeries {
coordinate: Coordinate::Longitude,
blocks: vec![PertBlock {
time_power: 0,
terms: vec![
PertTerm {
sin_coeff: 0.0,
cos_coeff: 0.0,
multipliers: [0; 16],
},
PertTerm {
sin_coeff: 0.0,
cos_coeff: 0.0,
multipliers: [0; 16],
},
],
}],
},
PertSeries {
coordinate: Coordinate::Latitude,
blocks: vec![],
},
PertSeries {
coordinate: Coordinate::Distance,
blocks: vec![],
},
],
};
assert_eq!(data.total_terms(), 3);
}
#[test]
fn test_parse_error_display_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = ParseError::IoError(io_err);
let msg = format!("{}", err);
assert!(msg.contains("IO error"));
assert!(msg.contains("file not found"));
}
#[test]
fn test_parse_error_display_invalid_header() {
let err = ParseError::InvalidHeader("bad header".to_string());
assert_eq!(format!("{}", err), "Invalid header: bad header");
}
#[test]
fn test_parse_error_display_invalid_main_term() {
let err = ParseError::InvalidMainTerm("bad term".to_string());
assert_eq!(format!("{}", err), "Invalid main term: bad term");
}
#[test]
fn test_parse_error_display_invalid_pert_term() {
let err = ParseError::InvalidPertTerm("bad pert".to_string());
assert_eq!(format!("{}", err), "Invalid pert term: bad pert");
}
#[test]
fn test_parse_error_display_invalid_format() {
let err = ParseError::InvalidFormat("bad format".to_string());
assert_eq!(format!("{}", err), "Invalid format: bad format");
}
#[test]
fn test_parse_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let err: ParseError = io_err.into();
match err {
ParseError::IoError(e) => assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied),
_ => panic!("Expected IoError variant"),
}
}
#[test]
fn test_parse_fortran_double() {
let val = parse_fortran_double("-0.1274921554086D+02").unwrap();
assert!((val - (-12.74921554086)).abs() < 1e-10);
let val2 = parse_fortran_double("0.6368794709728D+01").unwrap();
assert!((val2 - 6.368794709728).abs() < 1e-10);
}
#[test]
fn test_parse_fortran_double_lowercase() {
let val = parse_fortran_double("1.5d+00").unwrap();
assert!((val - 1.5).abs() < 1e-10);
}
#[test]
fn test_parse_fortran_double_standard_notation() {
let val = parse_fortran_double("1.234e+05").unwrap();
assert!((val - 123400.0).abs() < 1e-10);
}
#[test]
fn test_parse_fortran_double_invalid() {
let result = parse_fortran_double("not_a_number");
assert!(result.is_err());
match result {
Err(ParseError::InvalidPertTerm(msg)) => assert!(msg.contains("Invalid double")),
_ => panic!("Expected InvalidPertTerm error"),
}
}
#[test]
fn test_parse_main_header_valid() {
let line = "ELP_MAIN.S1 Longitude 1023";
let result = parse_main_header(line).unwrap();
assert_eq!(result.0, line);
assert_eq!(result.1, 1023);
}
#[test]
fn test_parse_main_header_too_short() {
let result = parse_main_header("AB");
assert!(result.is_err());
match result {
Err(ParseError::InvalidHeader(msg)) => assert!(msg.contains("too short")),
_ => panic!("Expected InvalidHeader error"),
}
}
#[test]
fn test_parse_main_header_invalid_term_count() {
let result = parse_main_header("ELP MAIN abc");
assert!(result.is_err());
match result {
Err(ParseError::InvalidHeader(msg)) => assert!(msg.contains("Invalid term count")),
_ => panic!("Expected InvalidHeader error"),
}
}
#[test]
fn test_parse_main_term_valid() {
let line = " 0 0 1 0 22639.55000 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000";
let result = parse_main_term(line).unwrap();
assert_eq!(result.delaunay, [0, 0, 1, 0]);
assert!((result.coeffs[0] - 22639.55).abs() < 0.01);
}
#[test]
fn test_parse_main_term_too_short() {
let result = parse_main_term("short");
assert!(result.is_err());
match result {
Err(ParseError::InvalidMainTerm(msg)) => assert!(msg.contains("too short")),
_ => panic!("Expected InvalidMainTerm error"),
}
}
#[test]
fn test_parse_main_term_invalid_delaunay() {
let line = "abc 0 1 0 22639.55000 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000";
let result = parse_main_term(line);
assert!(result.is_err());
match result {
Err(ParseError::InvalidMainTerm(msg)) => assert!(msg.contains("Invalid delaunay")),
_ => panic!("Expected InvalidMainTerm error"),
}
}
#[test]
fn test_parse_main_term_invalid_coeff() {
let line = " 0 0 1 0 not_a_number 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000";
let result = parse_main_term(line);
assert!(result.is_err());
match result {
Err(ParseError::InvalidMainTerm(msg)) => assert!(msg.contains("Invalid coeff[0]")),
_ => panic!("Expected InvalidMainTerm error"),
}
}
#[test]
fn test_parse_pert_header_valid() {
let line = "PERTURBATIONS LONGITUDE 234 0";
let result = parse_pert_header(line).unwrap();
assert_eq!(result.0, 234);
assert_eq!(result.1, 0);
}
#[test]
fn test_parse_pert_header_with_time_power() {
let line = "PERTURBATIONS LONGITUDE 567 2";
let result = parse_pert_header(line).unwrap();
assert_eq!(result.0, 567);
assert_eq!(result.1, 2);
}
#[test]
fn test_parse_pert_header_too_short() {
let result = parse_pert_header("AB");
assert!(result.is_err());
match result {
Err(ParseError::InvalidHeader(msg)) => assert!(msg.contains("too short")),
_ => panic!("Expected InvalidHeader error"),
}
}
#[test]
fn test_parse_pert_header_invalid_term_count() {
let result = parse_pert_header("PERT LON abc 0");
assert!(result.is_err());
match result {
Err(ParseError::InvalidHeader(msg)) => assert!(msg.contains("Invalid term count")),
_ => panic!("Expected InvalidHeader error"),
}
}
#[test]
fn test_parse_pert_header_invalid_time_power() {
let result = parse_pert_header("PERT LON 100 xyz");
assert!(result.is_err());
match result {
Err(ParseError::InvalidHeader(msg)) => assert!(msg.contains("Invalid time power")),
_ => panic!("Expected InvalidHeader error"),
}
}
#[test]
fn test_parse_pert_term_valid() {
let line = " 1-0.1274921554086D+02 0.6368794709728D+01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
let result = parse_pert_term(line).unwrap();
assert!((result.sin_coeff - (-12.74921554086)).abs() < 1e-10);
assert!((result.cos_coeff - 6.368794709728).abs() < 1e-10);
}
#[test]
fn test_parse_pert_term_too_short() {
let result = parse_pert_term("short");
assert!(result.is_err());
match result {
Err(ParseError::InvalidPertTerm(msg)) => assert!(msg.contains("too short")),
_ => panic!("Expected InvalidPertTerm error"),
}
}
#[test]
fn test_parse_pert_term_with_multipliers() {
let line = " 1 0.1000000000000D+01 0.2000000000000D+01 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16";
let result = parse_pert_term(line).unwrap();
assert_eq!(result.multipliers[0], 1);
assert_eq!(result.multipliers[1], 2);
assert_eq!(result.multipliers[15], 16);
}
fn create_mock_main_file(dir: &Path, name: &str, term_count: usize) -> std::path::PathBuf {
let path = dir.join(name);
let mut file = File::create(&path).unwrap();
writeln!(file, "ELP_MAIN.S1 Longitude {}", term_count).unwrap();
for i in 0..term_count {
let coeff = 1000.0 - (i as f64);
writeln!(
file,
" {} 0 1 0 {:12.5} 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000",
i % 10, coeff
).unwrap();
}
path
}
fn create_mock_pert_file(dir: &Path, name: &str, blocks: &[(usize, u8)]) -> std::path::PathBuf {
let path = dir.join(name);
let mut file = File::create(&path).unwrap();
for (term_count, time_power) in blocks {
writeln!(
file,
"PERTURBATIONS LONGITUDE {} {}",
term_count, time_power
)
.unwrap();
for _ in 0..*term_count {
writeln!(
file,
" 1-0.1274921554086D+02 0.6368794709728D+01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
).unwrap();
}
}
path
}
#[test]
fn test_parse_main_file_valid() {
let temp_dir = TempDir::new().unwrap();
let path = create_mock_main_file(temp_dir.path(), "test_main.dat", 3);
let result = parse_main_file(&path, Coordinate::Longitude).unwrap();
assert_eq!(result.coordinate, Coordinate::Longitude);
assert_eq!(result.terms.len(), 3);
}
#[test]
fn test_parse_main_file_empty() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("empty.dat");
File::create(&path).unwrap();
let result = parse_main_file(&path, Coordinate::Longitude);
assert!(result.is_err());
match result {
Err(ParseError::InvalidFormat(msg)) => assert!(msg.contains("Empty file")),
_ => panic!("Expected InvalidFormat error"),
}
}
#[test]
fn test_parse_main_file_term_count_mismatch() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("mismatch.dat");
let mut file = File::create(&path).unwrap();
writeln!(file, "ELP_MAIN.S1 Longitude 5").unwrap();
writeln!(
file,
" 0 0 1 0 22639.55000 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000"
).unwrap();
let result = parse_main_file(&path, Coordinate::Longitude);
assert!(result.is_err());
match result {
Err(ParseError::InvalidFormat(msg)) => {
assert!(msg.contains("Expected 5 terms"));
assert!(msg.contains("found 1"));
}
_ => panic!("Expected InvalidFormat error"),
}
}
#[test]
fn test_parse_main_file_skips_empty_lines() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("with_empty.dat");
let mut file = File::create(&path).unwrap();
writeln!(file, "ELP_MAIN.S1 Longitude 2").unwrap();
writeln!(
file,
" 0 0 1 0 22639.55000 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000"
).unwrap();
writeln!(file).unwrap(); writeln!(file, " ").unwrap(); writeln!(
file,
" 1 0 1 0 12345.00000 0.00000 0.00000 412529.61 0.00000 0.00000 0.00000"
).unwrap();
let result = parse_main_file(&path, Coordinate::Longitude).unwrap();
assert_eq!(result.terms.len(), 2);
}
#[test]
fn test_parse_main_file_not_found() {
let result = parse_main_file(Path::new("/nonexistent/file.dat"), Coordinate::Longitude);
assert!(result.is_err());
match result {
Err(ParseError::IoError(_)) => {}
_ => panic!("Expected IoError"),
}
}
#[test]
fn test_parse_pert_file_valid() {
let temp_dir = TempDir::new().unwrap();
let path = create_mock_pert_file(temp_dir.path(), "test_pert.dat", &[(3, 0), (2, 1)]);
let result = parse_pert_file(&path, Coordinate::Longitude).unwrap();
assert_eq!(result.coordinate, Coordinate::Longitude);
assert_eq!(result.blocks.len(), 2);
assert_eq!(result.blocks[0].terms.len(), 3);
assert_eq!(result.blocks[0].time_power, 0);
assert_eq!(result.blocks[1].terms.len(), 2);
assert_eq!(result.blocks[1].time_power, 1);
}
#[test]
fn test_parse_pert_file_multiple_keywords() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("multi_keyword.dat");
let mut file = File::create(&path).unwrap();
writeln!(file, "LONGITUDE 2 0").unwrap();
writeln!(
file,
" 1-0.1000000000000D+01 0.2000000000000D+01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
).unwrap();
writeln!(
file,
" 1-0.3000000000000D+01 0.4000000000000D+01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
).unwrap();
writeln!(file, "LATITUDE 1 1").unwrap();
writeln!(
file,
" 1-0.5000000000000D+01 0.6000000000000D+01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
).unwrap();
let result = parse_pert_file(&path, Coordinate::Longitude).unwrap();
assert_eq!(result.blocks.len(), 2);
}
#[test]
fn test_parse_pert_file_not_found() {
let result = parse_pert_file(Path::new("/nonexistent/file.dat"), Coordinate::Longitude);
assert!(result.is_err());
match result {
Err(ParseError::IoError(_)) => {}
_ => panic!("Expected IoError"),
}
}
#[test]
fn test_parse_pert_file_with_distance_keyword() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("distance.dat");
let mut file = File::create(&path).unwrap();
writeln!(file, "DISTANCE 1 0").unwrap();
writeln!(
file,
" 1-0.1000000000000D+01 0.2000000000000D+01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
).unwrap();
let result = parse_pert_file(&path, Coordinate::Distance).unwrap();
assert_eq!(result.blocks.len(), 1);
assert_eq!(result.coordinate, Coordinate::Distance);
}
fn create_full_mock_elp_files(dir: &Path) -> ElpFilePaths {
ElpFilePaths {
main_longitude: create_mock_main_file(dir, "ELP_MAIN.S1", 2),
main_latitude: create_mock_main_file(dir, "ELP_MAIN.S2", 3),
main_distance: create_mock_main_file(dir, "ELP_MAIN.S3", 1),
pert_longitude: create_mock_pert_file(dir, "ELP_PERT.S1", &[(2, 0)]),
pert_latitude: create_mock_pert_file(dir, "ELP_PERT.S2", &[(1, 0), (1, 1)]),
pert_distance: create_mock_pert_file(dir, "ELP_PERT.S3", &[(3, 0)]),
}
}
#[test]
fn test_parse_files_valid() {
let temp_dir = TempDir::new().unwrap();
let paths = create_full_mock_elp_files(temp_dir.path());
let result = parse_files(&paths).unwrap();
assert_eq!(result.main[0].coordinate, Coordinate::Longitude);
assert_eq!(result.main[0].terms.len(), 2);
assert_eq!(result.main[1].coordinate, Coordinate::Latitude);
assert_eq!(result.main[1].terms.len(), 3);
assert_eq!(result.main[2].coordinate, Coordinate::Distance);
assert_eq!(result.main[2].terms.len(), 1);
assert_eq!(result.pert[0].coordinate, Coordinate::Longitude);
assert_eq!(result.pert[0].blocks.len(), 1);
assert_eq!(result.pert[1].coordinate, Coordinate::Latitude);
assert_eq!(result.pert[1].blocks.len(), 2);
assert_eq!(result.pert[2].coordinate, Coordinate::Distance);
assert_eq!(result.pert[2].blocks.len(), 1);
assert_eq!(result.total_main_terms(), 6);
assert_eq!(result.total_pert_terms(), 7);
assert_eq!(result.total_terms(), 13);
}
#[test]
fn test_parse_files_missing_main_file() {
let temp_dir = TempDir::new().unwrap();
let paths = ElpFilePaths {
main_longitude: temp_dir.path().join("nonexistent.dat"),
main_latitude: create_mock_main_file(temp_dir.path(), "ELP_MAIN.S2", 1),
main_distance: create_mock_main_file(temp_dir.path(), "ELP_MAIN.S3", 1),
pert_longitude: create_mock_pert_file(temp_dir.path(), "ELP_PERT.S1", &[(1, 0)]),
pert_latitude: create_mock_pert_file(temp_dir.path(), "ELP_PERT.S2", &[(1, 0)]),
pert_distance: create_mock_pert_file(temp_dir.path(), "ELP_PERT.S3", &[(1, 0)]),
};
let result = parse_files(&paths);
assert!(result.is_err());
}
#[test]
fn test_parse_files_missing_pert_file() {
let temp_dir = TempDir::new().unwrap();
let paths = ElpFilePaths {
main_longitude: create_mock_main_file(temp_dir.path(), "ELP_MAIN.S1", 1),
main_latitude: create_mock_main_file(temp_dir.path(), "ELP_MAIN.S2", 1),
main_distance: create_mock_main_file(temp_dir.path(), "ELP_MAIN.S3", 1),
pert_longitude: create_mock_pert_file(temp_dir.path(), "ELP_PERT.S1", &[(1, 0)]),
pert_latitude: temp_dir.path().join("nonexistent.dat"),
pert_distance: create_mock_pert_file(temp_dir.path(), "ELP_PERT.S3", &[(1, 0)]),
};
let result = parse_files(&paths);
assert!(result.is_err());
}
#[test]
fn test_parse_main_term_short_line_coeffs() {
let line =
" 0 0 1 0 22639.55000 ";
let result = parse_main_term(line).unwrap();
assert!((result.coeffs[0] - 22639.55).abs() < 0.01);
assert!((result.coeffs[1] - 0.0).abs() < 1e-10);
}
#[test]
fn test_parse_pert_term_short_multipliers() {
let line = " 1 0.1000000000000D+01 0.2000000000000D+01 1 2 3 4 ";
let result = parse_pert_term(line).unwrap();
assert_eq!(result.multipliers[0], 1);
assert_eq!(result.multipliers[1], 2);
assert_eq!(result.multipliers[2], 3);
assert_eq!(result.multipliers[3], 4);
assert_eq!(result.multipliers[4], 0);
}
#[test]
fn test_parse_error_is_std_error() {
fn assert_std_error<T: std::error::Error>() {}
assert_std_error::<ParseError>();
}
}