use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Variable {
A,
Lambda,
K,
H,
Q,
P,
}
impl Variable {
pub fn from_index(idx: u8) -> Option<Self> {
match idx {
1 => Some(Variable::A),
2 => Some(Variable::Lambda),
3 => Some(Variable::K),
4 => Some(Variable::H),
5 => Some(Variable::Q),
6 => Some(Variable::P),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Variable::A => "A (semi-major axis)",
Variable::Lambda => "Lambda (mean longitude)",
Variable::K => "K (e*cos(perihelion))",
Variable::H => "H (e*sin(perihelion))",
Variable::Q => "Q (sin(i/2)*cos(node))",
Variable::P => "P (sin(i/2)*sin(node))",
}
}
}
impl fmt::Display for Variable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Variable::A => "A",
Variable::Lambda => "L",
Variable::K => "K",
Variable::H => "H",
Variable::Q => "Q",
Variable::P => "P",
};
write!(f, "{}", name)
}
}
#[derive(Debug, Clone)]
pub struct Vsop2013Header {
pub planet: u8,
pub variable: Variable,
pub time_power: u8,
pub term_count: u32,
}
#[derive(Debug, Clone)]
pub struct Vsop2013Term {
pub multipliers: [i32; 17],
pub s_coeff: f64,
pub c_coeff: f64,
}
impl Vsop2013Term {
pub fn amplitude(&self) -> f64 {
libm::sqrt(self.s_coeff.powi(2) + self.c_coeff.powi(2))
}
}
#[derive(Debug, Clone)]
pub struct Vsop2013Block {
pub header: Vsop2013Header,
pub terms: Vec<Vsop2013Term>,
}
#[derive(Debug, Clone)]
pub struct Vsop2013File {
pub planet: u8,
pub blocks: Vec<Vsop2013Block>,
}
impl Vsop2013File {
pub fn total_terms(&self) -> usize {
self.blocks.iter().map(|b| b.terms.len()).sum()
}
pub fn blocks_for_variable(&self, var: Variable) -> impl Iterator<Item = &Vsop2013Block> {
self.blocks.iter().filter(move |b| b.header.variable == var)
}
}
#[derive(Debug)]
pub enum ParseError {
IoError(std::io::Error),
InvalidHeader(String),
InvalidTerm(String),
InvalidVariable(u8),
MissingTerms { expected: u32, found: u32 },
}
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::InvalidTerm(s) => write!(f, "Invalid term: {}", s),
ParseError::InvalidVariable(v) => write!(f, "Invalid variable index: {}", v),
ParseError::MissingTerms { expected, found } => {
write!(f, "Expected {} terms, found {}", expected, found)
}
}
}
}
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_fortran_float(s: &str, exp: &str) -> Result<f64, ParseError> {
let mantissa: f64 = s
.trim()
.parse()
.map_err(|_| ParseError::InvalidTerm(format!("Invalid mantissa: '{}'", s)))?;
let exponent: i32 = exp
.trim()
.parse()
.map_err(|_| ParseError::InvalidTerm(format!("Invalid exponent: '{}'", exp)))?;
Ok(mantissa * 10f64.powi(exponent))
}
fn parse_header(line: &str) -> Result<Vsop2013Header, ParseError> {
if !line.starts_with(" VSOP2013") {
return Err(ParseError::InvalidHeader(format!(
"Line doesn't start with ' VSOP2013': '{}'",
line
)));
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
return Err(ParseError::InvalidHeader(format!(
"Not enough parts in header: '{}'",
line
)));
}
let planet: u8 = parts[1]
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid planet: '{}'", parts[1])))?;
let var_idx: u8 = parts[2]
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid variable: '{}'", parts[2])))?;
let time_power: u8 = parts[3]
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid time power: '{}'", parts[3])))?;
let term_count: u32 = parts[4]
.parse()
.map_err(|_| ParseError::InvalidHeader(format!("Invalid term count: '{}'", parts[4])))?;
let variable = Variable::from_index(var_idx).ok_or(ParseError::InvalidVariable(var_idx))?;
Ok(Vsop2013Header {
planet,
variable,
time_power,
term_count,
})
}
fn tokenize_numbers(s: &str) -> Vec<&str> {
let mut tokens = Vec::new();
let mut start = None;
let bytes = s.as_bytes();
for (i, &ch) in bytes.iter().enumerate() {
let is_digit = ch.is_ascii_digit();
let is_sign = ch == b'-' || ch == b'+';
let is_dot = ch == b'.';
match start {
None => {
if is_digit || is_sign || is_dot {
start = Some(i);
}
}
Some(s_idx) => {
if is_sign && i > 0 && bytes[i - 1].is_ascii_digit() {
tokens.push(&s[s_idx..i]);
start = Some(i);
} else if !is_digit && !is_sign && !is_dot {
tokens.push(&s[s_idx..i]);
start = None;
}
}
}
}
if let Some(s_idx) = start {
tokens.push(&s[s_idx..]);
}
tokens
}
fn parse_term(line: &str) -> Result<Vsop2013Term, ParseError> {
let tokens = tokenize_numbers(line);
if tokens.len() < 22 {
return Err(ParseError::InvalidTerm(format!(
"Not enough tokens ({}): '{}'",
tokens.len(),
line
)));
}
let mut multipliers = [0i32; 17];
for i in 0..17 {
multipliers[i] = tokens[i + 1].parse().map_err(|_| {
ParseError::InvalidTerm(format!("Invalid multiplier: '{}'", tokens[i + 1]))
})?;
}
let s_coeff = parse_fortran_float(tokens[18], tokens[19])?;
let c_coeff = parse_fortran_float(tokens[20], tokens[21])?;
Ok(Vsop2013Term {
multipliers,
s_coeff,
c_coeff,
})
}
pub fn parse_file(path: &Path) -> Result<Vsop2013File, ParseError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let lines = reader.lines();
let mut blocks = Vec::new();
let mut current_block: Option<Vsop2013Block> = None;
let mut planet: Option<u8> = None;
for line_result in lines {
let line = line_result?;
if line.starts_with(" VSOP2013") {
if let Some(block) = current_block.take() {
if block.terms.len() as u32 != block.header.term_count {
return Err(ParseError::MissingTerms {
expected: block.header.term_count,
found: block.terms.len() as u32,
});
}
blocks.push(block);
}
let header = parse_header(&line)?;
if planet.is_none() {
planet = Some(header.planet);
}
current_block = Some(Vsop2013Block {
header,
terms: Vec::new(),
});
} else if let Some(ref mut block) = current_block {
let term = parse_term(&line)?;
block.terms.push(term);
}
}
if let Some(block) = current_block {
if block.terms.len() as u32 != block.header.term_count {
return Err(ParseError::MissingTerms {
expected: block.header.term_count,
found: block.terms.len() as u32,
});
}
blocks.push(block);
}
Ok(Vsop2013File {
planet: planet.unwrap_or(0),
blocks,
})
}
pub fn planet_name(planet: u8) -> &'static str {
match planet {
1 => "Mercury",
2 => "Venus",
3 => "Earth-Moon Barycenter",
4 => "Mars",
5 => "Jupiter",
6 => "Saturn",
7 => "Uranus",
8 => "Neptune",
9 => "Pluto",
_ => "Unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
const TEST_FILE: &str = "references/ephemeris/vsop2013/VSOP2013p3.dat";
#[test]
fn test_variable_from_index_all_valid() {
assert_eq!(Variable::from_index(1), Some(Variable::A));
assert_eq!(Variable::from_index(2), Some(Variable::Lambda));
assert_eq!(Variable::from_index(3), Some(Variable::K));
assert_eq!(Variable::from_index(4), Some(Variable::H));
assert_eq!(Variable::from_index(5), Some(Variable::Q));
assert_eq!(Variable::from_index(6), Some(Variable::P));
}
#[test]
fn test_variable_from_index_invalid() {
assert_eq!(Variable::from_index(0), None);
assert_eq!(Variable::from_index(7), None);
assert_eq!(Variable::from_index(100), None);
}
#[test]
fn test_variable_name_all_variants() {
assert_eq!(Variable::A.name(), "A (semi-major axis)");
assert_eq!(Variable::Lambda.name(), "Lambda (mean longitude)");
assert_eq!(Variable::K.name(), "K (e*cos(perihelion))");
assert_eq!(Variable::H.name(), "H (e*sin(perihelion))");
assert_eq!(Variable::Q.name(), "Q (sin(i/2)*cos(node))");
assert_eq!(Variable::P.name(), "P (sin(i/2)*sin(node))");
}
#[test]
fn test_variable_display_all() {
assert_eq!(format!("{}", Variable::A), "A");
assert_eq!(format!("{}", Variable::Lambda), "L");
assert_eq!(format!("{}", Variable::K), "K");
assert_eq!(format!("{}", Variable::H), "H");
assert_eq!(format!("{}", Variable::Q), "Q");
assert_eq!(format!("{}", Variable::P), "P");
}
#[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 display = format!("{}", err);
assert!(display.contains("IO error"));
assert!(display.contains("file not found"));
}
#[test]
fn test_parse_error_display_invalid_header() {
let err = ParseError::InvalidHeader("bad header".to_string());
let display = format!("{}", err);
assert!(display.contains("Invalid header"));
assert!(display.contains("bad header"));
}
#[test]
fn test_parse_error_display_invalid_term() {
let err = ParseError::InvalidTerm("bad term".to_string());
let display = format!("{}", err);
assert!(display.contains("Invalid term"));
assert!(display.contains("bad term"));
}
#[test]
fn test_parse_error_display_invalid_variable() {
let err = ParseError::InvalidVariable(99);
let display = format!("{}", err);
assert!(display.contains("Invalid variable index"));
assert!(display.contains("99"));
}
#[test]
fn test_parse_error_display_missing_terms() {
let err = ParseError::MissingTerms {
expected: 100,
found: 50,
};
let display = format!("{}", err);
assert!(display.contains("Expected 100 terms"));
assert!(display.contains("found 50"));
}
#[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!(e.to_string().contains("access denied")),
_ => panic!("Expected IoError variant"),
}
}
#[test]
fn test_parse_header() {
let line = " VSOP2013 3 1 0 32658 EARTH-MOON VARIABLE A *T*00";
let header = parse_header(line).unwrap();
assert_eq!(header.planet, 3);
assert_eq!(header.variable, Variable::A);
assert_eq!(header.time_power, 0);
assert_eq!(header.term_count, 32658);
}
#[test]
fn test_parse_header_all_variables() {
for (idx, expected_var) in [
(1, Variable::A),
(2, Variable::Lambda),
(3, Variable::K),
(4, Variable::H),
(5, Variable::Q),
(6, Variable::P),
] {
let line = format!(" VSOP2013 5 {} 2 100 JUPITER VAR", idx);
let header = parse_header(&line).unwrap();
assert_eq!(header.variable, expected_var);
}
}
#[test]
fn test_parse_header_error_not_vsop() {
let line = "SOME OTHER FORMAT";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidHeader(s) => assert!(s.contains("doesn't start with")),
_ => panic!("Expected InvalidHeader"),
}
}
#[test]
fn test_parse_header_error_not_enough_parts() {
let line = " VSOP2013 1 2";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidHeader(s) => assert!(s.contains("Not enough parts")),
_ => panic!("Expected InvalidHeader"),
}
}
#[test]
fn test_parse_header_error_invalid_planet() {
let line = " VSOP2013 X 1 0 100 INVALID";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidHeader(s) => assert!(s.contains("Invalid planet")),
_ => panic!("Expected InvalidHeader"),
}
}
#[test]
fn test_parse_header_error_invalid_variable_number() {
let line = " VSOP2013 3 Y 0 100 INVALID";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidHeader(s) => assert!(s.contains("Invalid variable")),
_ => panic!("Expected InvalidHeader"),
}
}
#[test]
fn test_parse_header_error_invalid_time_power() {
let line = " VSOP2013 3 1 Z 100 INVALID";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidHeader(s) => assert!(s.contains("Invalid time power")),
_ => panic!("Expected InvalidHeader"),
}
}
#[test]
fn test_parse_header_error_invalid_term_count() {
let line = " VSOP2013 3 1 0 XXX INVALID";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidHeader(s) => assert!(s.contains("Invalid term count")),
_ => panic!("Expected InvalidHeader"),
}
}
#[test]
fn test_parse_header_error_invalid_variable_index() {
let line = " VSOP2013 3 9 0 100 INVALID";
let result = parse_header(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidVariable(idx) => assert_eq!(idx, 9),
_ => panic!("Expected InvalidVariable"),
}
}
#[test]
fn test_tokenize_numbers_basic() {
let tokens = tokenize_numbers("1 2 3");
assert_eq!(tokens, vec!["1", "2", "3"]);
}
#[test]
fn test_tokenize_numbers_with_signs() {
let tokens = tokenize_numbers("-1 +2 -3");
assert_eq!(tokens, vec!["-1", "+2", "-3"]);
}
#[test]
fn test_tokenize_numbers_with_decimals() {
let tokens = tokenize_numbers("1.5 -2.3 +0.7");
assert_eq!(tokens, vec!["1.5", "-2.3", "+0.7"]);
}
#[test]
fn test_tokenize_numbers_fortran_style() {
let tokens = tokenize_numbers("-0.7736236063963646 -08");
assert_eq!(tokens, vec!["-0.7736236063963646", "-08"]);
}
#[test]
fn test_tokenize_numbers_digit_followed_by_sign() {
let tokens = tokenize_numbers("1.234-05");
assert_eq!(tokens, vec!["1.234", "-05"]);
let tokens = tokenize_numbers("9.87+03");
assert_eq!(tokens, vec!["9.87", "+03"]);
let tokens = tokenize_numbers("1.0-01 2.0+02");
assert_eq!(tokens, vec!["1.0", "-01", "2.0", "+02"]);
}
#[test]
fn test_tokenize_numbers_vsop_term_line() {
let line = " 2 0 0 2 0 0 0 0 0 0 -2 0 0 0 0 0 0 0 -0.7736236063963646 -08 0.1120495653357545 -04";
let tokens = tokenize_numbers(line);
assert!(tokens.len() >= 22);
assert_eq!(tokens[0], "2");
assert_eq!(tokens[1], "0");
assert_eq!(tokens[10], "-2");
}
#[test]
fn test_parse_term() {
let line = " 2 0 0 2 0 0 0 0 0 0 -2 0 0 0 0 0 0 0 -0.7736236063963646 -08 0.1120495653357545 -04";
let term = parse_term(line).unwrap();
assert_eq!(term.multipliers[0], 0);
assert_eq!(term.multipliers[1], 0);
assert_eq!(term.multipliers[2], 2);
assert_eq!(term.multipliers[9], -2);
let expected_s = -7.736236063963646e-9;
let expected_c = 1.120495653357545e-5;
assert!((term.s_coeff - expected_s).abs() < 1e-20);
assert!((term.c_coeff - expected_c).abs() < 1e-16);
}
#[test]
fn test_parse_term_not_enough_tokens() {
let line = "1 2 3 4 5";
let result = parse_term(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidTerm(s) => assert!(s.contains("Not enough tokens")),
_ => panic!("Expected InvalidTerm"),
}
}
#[test]
fn test_parse_term_invalid_multiplier() {
let line = " 2 9999999999999 0 2 0 0 0 0 0 0 -2 0 0 0 0 0 0 0 -0.77 -08 0.11 -04";
let result = parse_term(line);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidTerm(s) => assert!(s.contains("Invalid multiplier")),
_ => panic!("Expected InvalidTerm"),
}
}
#[test]
fn test_parse_fortran_float() {
assert!(
(parse_fortran_float("-0.7736236063963646", "-08").unwrap() - (-7.736236063963646e-9))
.abs()
< 1e-20
);
assert!(
(parse_fortran_float("0.1000001017641000", "+01").unwrap() - 1.000001017641).abs()
< 1e-15
);
assert!(
(parse_fortran_float("0.1120495653357545", "-04").unwrap() - 1.120495653357545e-5)
.abs()
< 1e-16
);
}
#[test]
fn test_parse_fortran_float_invalid_mantissa() {
let result = parse_fortran_float("not_a_number", "-08");
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidTerm(s) => assert!(s.contains("Invalid mantissa")),
_ => panic!("Expected InvalidTerm"),
}
}
#[test]
fn test_parse_fortran_float_invalid_exponent() {
let result = parse_fortran_float("0.123", "abc");
assert!(result.is_err());
match result.unwrap_err() {
ParseError::InvalidTerm(s) => assert!(s.contains("Invalid exponent")),
_ => panic!("Expected InvalidTerm"),
}
}
#[test]
fn test_amplitude() {
let term = Vsop2013Term {
multipliers: [0; 17],
s_coeff: 3.0,
c_coeff: 4.0,
};
assert!((term.amplitude() - 5.0).abs() < 1e-15);
}
#[test]
fn test_vsop2013_file_total_terms() {
let vsop = Vsop2013File {
planet: 3,
blocks: vec![
Vsop2013Block {
header: Vsop2013Header {
planet: 3,
variable: Variable::A,
time_power: 0,
term_count: 2,
},
terms: vec![
Vsop2013Term {
multipliers: [0; 17],
s_coeff: 1.0,
c_coeff: 1.0,
},
Vsop2013Term {
multipliers: [0; 17],
s_coeff: 2.0,
c_coeff: 2.0,
},
],
},
Vsop2013Block {
header: Vsop2013Header {
planet: 3,
variable: Variable::Lambda,
time_power: 0,
term_count: 1,
},
terms: vec![Vsop2013Term {
multipliers: [0; 17],
s_coeff: 3.0,
c_coeff: 3.0,
}],
},
],
};
assert_eq!(vsop.total_terms(), 3);
}
#[test]
fn test_vsop2013_file_blocks_for_variable() {
let vsop = Vsop2013File {
planet: 3,
blocks: vec![
Vsop2013Block {
header: Vsop2013Header {
planet: 3,
variable: Variable::A,
time_power: 0,
term_count: 1,
},
terms: vec![Vsop2013Term {
multipliers: [0; 17],
s_coeff: 1.0,
c_coeff: 1.0,
}],
},
Vsop2013Block {
header: Vsop2013Header {
planet: 3,
variable: Variable::Lambda,
time_power: 0,
term_count: 1,
},
terms: vec![Vsop2013Term {
multipliers: [0; 17],
s_coeff: 2.0,
c_coeff: 2.0,
}],
},
Vsop2013Block {
header: Vsop2013Header {
planet: 3,
variable: Variable::A,
time_power: 1,
term_count: 1,
},
terms: vec![Vsop2013Term {
multipliers: [0; 17],
s_coeff: 3.0,
c_coeff: 3.0,
}],
},
],
};
let a_blocks: Vec<_> = vsop.blocks_for_variable(Variable::A).collect();
assert_eq!(a_blocks.len(), 2);
assert_eq!(a_blocks[0].header.time_power, 0);
assert_eq!(a_blocks[1].header.time_power, 1);
let lambda_blocks: Vec<_> = vsop.blocks_for_variable(Variable::Lambda).collect();
assert_eq!(lambda_blocks.len(), 1);
}
#[test]
fn test_planet_name_all_planets() {
assert_eq!(planet_name(1), "Mercury");
assert_eq!(planet_name(2), "Venus");
assert_eq!(planet_name(3), "Earth-Moon Barycenter");
assert_eq!(planet_name(4), "Mars");
assert_eq!(planet_name(5), "Jupiter");
assert_eq!(planet_name(6), "Saturn");
assert_eq!(planet_name(7), "Uranus");
assert_eq!(planet_name(8), "Neptune");
assert_eq!(planet_name(9), "Pluto");
}
#[test]
fn test_planet_name_unknown() {
assert_eq!(planet_name(0), "Unknown");
assert_eq!(planet_name(10), "Unknown");
assert_eq!(planet_name(255), "Unknown");
}
#[test]
fn test_parse_file_with_mock_data() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test_vsop.dat");
let content = " VSOP2013 3 1 0 2 EARTH VAR A T^0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1000000000000000 +00 0.0000000000000000 +00
2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2000000000000000 +00 0.0000000000000000 +00
VSOP2013 3 2 0 1 EARTH VAR LAMBDA T^0
3 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3000000000000000 +00 0.0000000000000000 +00
";
std::fs::write(&file_path, content).unwrap();
let result = parse_file(&file_path);
assert!(result.is_ok(), "Parse failed: {:?}", result.err());
let vsop = result.unwrap();
assert_eq!(vsop.planet, 3);
assert_eq!(vsop.blocks.len(), 2);
let block1 = &vsop.blocks[0];
assert_eq!(block1.header.variable, Variable::A);
assert_eq!(block1.header.time_power, 0);
assert_eq!(block1.header.term_count, 2);
assert_eq!(block1.terms.len(), 2);
let block2 = &vsop.blocks[1];
assert_eq!(block2.header.variable, Variable::Lambda);
assert_eq!(block2.terms.len(), 1);
}
#[test]
fn test_parse_file_missing_terms_error() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("bad_vsop.dat");
let content = " VSOP2013 3 1 0 5 EARTH VAR A T^0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1000000000000000 +00 0.0000000000000000 +00
";
std::fs::write(&file_path, content).unwrap();
let result = parse_file(&file_path);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::MissingTerms { expected, found } => {
assert_eq!(expected, 5);
assert_eq!(found, 1);
}
e => panic!("Expected MissingTerms, got {:?}", e),
}
}
#[test]
fn test_parse_file_missing_terms_mid_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("bad_vsop_mid.dat");
let content = " VSOP2013 3 1 0 5 EARTH VAR A T^0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1000000000000000 +00 0.0000000000000000 +00
VSOP2013 3 2 0 1 EARTH VAR LAMBDA T^0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2000000000000000 +00 0.0000000000000000 +00
";
std::fs::write(&file_path, content).unwrap();
let result = parse_file(&file_path);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::MissingTerms { expected, found } => {
assert_eq!(expected, 5);
assert_eq!(found, 1);
}
e => panic!("Expected MissingTerms, got {:?}", e),
}
}
#[test]
fn test_parse_file_empty() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("empty.dat");
std::fs::write(&file_path, "").unwrap();
let result = parse_file(&file_path);
assert!(result.is_ok());
let vsop = result.unwrap();
assert_eq!(vsop.planet, 0);
assert!(vsop.blocks.is_empty());
}
#[test]
fn test_parse_file_io_error() {
let result = parse_file(Path::new("/nonexistent/path/file.dat"));
assert!(result.is_err());
match result.unwrap_err() {
ParseError::IoError(_) => {}
e => panic!("Expected IoError, got {:?}", e),
}
}
#[test]
#[ignore = "requires local VSOP2013 data files"]
fn test_parse_file() {
let path = Path::new(TEST_FILE);
let result = parse_file(path);
assert!(result.is_ok(), "Parse failed: {:?}", result.err());
let vsop = result.unwrap();
assert_eq!(vsop.planet, 3);
assert!(!vsop.blocks.is_empty());
let first_block = &vsop.blocks[0];
assert_eq!(first_block.header.variable, Variable::A);
assert_eq!(first_block.header.time_power, 0);
assert_eq!(first_block.header.term_count, 32658);
assert_eq!(first_block.terms.len(), 32658);
let total = vsop.total_terms();
assert!(total > 100_000, "Expected > 100k terms, got {}", total);
}
#[test]
#[ignore = "requires local VSOP2013 data files"]
fn test_blocks_for_variable() {
let path = Path::new(TEST_FILE);
let vsop = parse_file(path).unwrap();
let a_blocks: Vec<_> = vsop.blocks_for_variable(Variable::A).collect();
assert!(!a_blocks.is_empty());
for block in &a_blocks {
assert_eq!(block.header.variable, Variable::A);
}
}
}