use crate::constants::GM_EARTH;
use crate::time::{Epoch, TimeSystem};
use crate::utils::BraheError;
use nalgebra::Vector6;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TleFormat {
Classic,
Alpha5,
}
pub fn calculate_tle_line_checksum(line: &str) -> u32 {
let mut checksum = 0u32;
for ch in line.chars().take(68) {
match ch {
'0'..='9' => checksum += ch.to_digit(10).unwrap_or(0),
'-' => checksum += 1,
_ => {} }
}
checksum % 10
}
pub fn validate_tle_line(line: &str) -> bool {
if line.len() != 69 {
return false;
}
if !line.starts_with('1') && !line.starts_with('2') {
return false;
}
let expected_checksum = calculate_tle_line_checksum(line);
let actual_checksum = line
.chars()
.nth(68)
.and_then(|c| c.to_digit(10))
.unwrap_or(10);
expected_checksum == actual_checksum
}
pub fn validate_tle_lines(line1: &str, line2: &str) -> bool {
if !validate_tle_line(line1) || !validate_tle_line(line2) {
return false;
}
if !line1.starts_with('1') || !line2.starts_with('2') {
return false;
}
let norad1 = line1.get(2..7).unwrap_or("").trim();
let norad2 = line2.get(2..7).unwrap_or("").trim();
norad1 == norad2
}
pub fn parse_norad_id(norad_str: &str) -> Result<u32, BraheError> {
if norad_str.len() > 5 {
return Err(BraheError::Error(format!(
"NORAD ID too long: {}. Expected 5 characters found {}",
norad_str,
norad_str.len()
)));
}
if norad_str.len() < 5 {
return Err(BraheError::Error(format!(
"NORAD ID too short: {}. Expected 5 characters found {}",
norad_str,
norad_str.len()
)));
}
let trimmed = norad_str.trim();
if trimmed.is_empty() {
return Err(BraheError::Error("Empty NORAD ID".to_string()));
}
let first_char = trimmed.chars().next().unwrap();
if first_char.is_ascii_digit() {
trimmed
.parse::<u32>()
.map_err(|_| BraheError::Error(format!("Invalid numeric NORAD ID: {}", trimmed)))
} else if first_char.is_ascii_alphabetic() {
norad_id_alpha5_to_numeric(trimmed)
} else {
Err(BraheError::Error(format!(
"Invalid NORAD ID format: {}",
trimmed
)))
}
}
pub fn norad_id_numeric_to_alpha5(norad_id: u32) -> Result<String, BraheError> {
if norad_id < 100000 {
return Ok(norad_id.to_string());
}
if norad_id > 339999 {
return Err(BraheError::Error(format!(
"NORAD ID {} is out of valid range (0-339999)",
norad_id
)));
}
let first_value = norad_id / 10000;
let numeric_part = norad_id % 10000;
let first_char = match first_value {
10..=17 => char::from(b'A' + (first_value - 10) as u8), 18..=22 => char::from(b'J' + (first_value - 18) as u8), 23..=33 => char::from(b'P' + (first_value - 23) as u8), _ => {
return Err(BraheError::Error(format!(
"Invalid Alpha-5 first value: {}",
first_value
)));
}
};
Ok(format!("{}{:04}", first_char, numeric_part))
}
pub fn norad_id_alpha5_to_numeric(alpha5_id: &str) -> Result<u32, BraheError> {
if alpha5_id.len() != 5 {
return Err(BraheError::Error(
"Alpha-5 ID must be exactly 5 characters".to_string(),
));
}
let chars: Vec<char> = alpha5_id.chars().collect();
let first_char = chars[0];
let first_value = match first_char {
'A'..='H' => (first_char as u32) - ('A' as u32) + 10,
'J'..='N' => (first_char as u32) - ('A' as u32) + 9, 'P'..='Z' => (first_char as u32) - ('A' as u32) + 8, _ => {
return Err(BraheError::Error(format!(
"Invalid Alpha-5 first character: {}",
first_char
)));
}
};
let remaining: String = chars[1..].iter().collect();
let numeric_part = remaining
.parse::<u32>()
.map_err(|_| BraheError::Error(format!("Invalid Alpha-5 numeric part: {}", remaining)))?;
if numeric_part > 9999 {
return Err(BraheError::Error(
"Alpha-5 numeric part cannot exceed 9999".to_string(),
));
}
Ok(first_value * 10000 + numeric_part)
}
pub fn epoch_from_tle(line1: &str) -> Result<Epoch, BraheError> {
if line1.len() < 32 {
return Err(BraheError::Error(
"TLE line 1 too short to extract epoch".to_string(),
));
}
let epoch_str = &line1[18..32];
let year_2digit: u32 = epoch_str[0..2]
.parse()
.map_err(|_| BraheError::Error("Invalid year in TLE".to_string()))?;
let year = if year_2digit < 57 {
2000 + year_2digit
} else {
1900 + year_2digit
};
let day_of_year: f64 = epoch_str[2..]
.parse()
.map_err(|_| BraheError::Error("Invalid day of year in TLE".to_string()))?;
Ok(Epoch::from_day_of_year(year, day_of_year, TimeSystem::UTC))
}
pub fn keplerian_elements_from_tle(
line1: &str,
line2: &str,
) -> Result<(Epoch, Vector6<f64>), BraheError> {
if !validate_tle_lines(line1, line2) {
return Err(BraheError::Error("Invalid TLE lines".to_string()));
}
let epoch = epoch_from_tle(line1)?;
let inclination: f64 = line2[8..16]
.trim()
.parse()
.map_err(|_| BraheError::Error("Invalid inclination in TLE".to_string()))?;
let raan: f64 = line2[17..25]
.trim()
.parse()
.map_err(|_| BraheError::Error("Invalid RAAN in TLE".to_string()))?;
let ecc_str = line2[26..33].trim();
let eccentricity: f64 = format!("0.{}", ecc_str)
.parse()
.map_err(|_| BraheError::Error("Invalid eccentricity in TLE".to_string()))?;
let arg_perigee: f64 = line2[34..42]
.trim()
.parse()
.map_err(|_| BraheError::Error("Invalid argument of perigee in TLE".to_string()))?;
let mean_anomaly: f64 = line2[43..51]
.trim()
.parse()
.map_err(|_| BraheError::Error("Invalid mean anomaly in TLE".to_string()))?;
let mean_motion_revs_per_day: f64 = line2[52..63]
.trim()
.parse()
.map_err(|_| BraheError::Error("Invalid mean motion in TLE".to_string()))?;
let mean_motion_rad_per_sec = mean_motion_revs_per_day * 2.0 * PI / 86400.0;
let semi_major_axis =
(GM_EARTH / (mean_motion_rad_per_sec * mean_motion_rad_per_sec)).powf(1.0 / 3.0);
let elements = Vector6::new(
semi_major_axis, eccentricity, inclination, raan, arg_perigee, mean_anomaly, );
Ok((epoch, elements))
}
pub fn keplerian_elements_to_tle(
epoch: &Epoch,
elements: &Vector6<f64>,
norad_id: &str,
) -> Result<(String, String), BraheError> {
let semi_major_axis = elements[0]; let mean_motion_rad_per_sec =
(GM_EARTH / (semi_major_axis * semi_major_axis * semi_major_axis)).sqrt();
let mean_motion_revs_per_day = mean_motion_rad_per_sec * 86400.0 / (2.0 * PI);
create_tle_lines(
epoch,
norad_id,
'U', "", mean_motion_revs_per_day,
elements[1], elements[2], elements[3], elements[4], elements[5], 0.0, 0.0, 0.0, 0, 0, 0, )
}
#[allow(clippy::too_many_arguments)]
pub fn create_tle_lines(
epoch: &Epoch,
norad_id: &str,
classification: char,
intl_designator: &str,
mean_motion: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
arg_periapsis: f64,
mean_anomaly: f64,
first_derivative: f64,
second_derivative: f64,
bstar: f64,
ephemeris_type: u8,
element_set_number: u16,
revolution_number: u32,
) -> Result<(String, String), BraheError> {
let norad_id_trimmed = norad_id.trim();
if norad_id_trimmed.len() > 5 {
return Err(BraheError::Error(format!(
"NORAD ID too long: {}. Expected 5 characters max",
norad_id_trimmed
)));
}
parse_norad_id(norad_id_trimmed)?;
if !(0.0..1.0).contains(&eccentricity) {
return Err(BraheError::Error(
"Eccentricity must be in range [0, 1)".to_string(),
));
}
if mean_motion <= 0.0 {
return Err(BraheError::Error(
"Mean motion must be positive".to_string(),
));
}
if !(0.0..1.0).contains(&eccentricity) {
return Err(BraheError::Error(
"Eccentricity must be in range [0, 1)".to_string(),
));
}
if !(0.0..=180.0).contains(&inclination) {
return Err(BraheError::Error(
"Inclination must be in range [0, 180] degrees".to_string(),
));
}
if !(0.0..360.0).contains(&arg_periapsis) {
return Err(BraheError::Error(
"Argument of periapsis must be in range [0, 360) degrees".to_string(),
));
}
if !(0.0..360.0).contains(&mean_anomaly) {
return Err(BraheError::Error(
"Mean anomaly must be in range [0, 360) degrees".to_string(),
));
}
if element_set_number > 9999 {
return Err(BraheError::Error(
"Element set number cannot exceed 9999".to_string(),
));
}
if revolution_number > 99999 {
return Err(BraheError::Error(
"Revolution number cannot exceed 99999".to_string(),
));
}
if !matches!(classification, 'U' | 'C' | 'S' | ' ') {
return Err(BraheError::Error(
"Classification must be 'U', 'C', 'S', or ' '".to_string(),
));
}
if intl_designator.len() > 8 {
return Err(BraheError::Error(
"International designator cannot exceed 8 characters".to_string(),
));
}
let mut epoch = *epoch;
epoch.time_system = TimeSystem::UTC; let year = epoch.year();
let day_of_year = epoch.day_of_year();
let year_2digit = year % 100;
let ndt2_sign = if first_derivative < 0.0 { "-" } else { " " };
let ndt2_abs_formatted = format!("{:9.8}", first_derivative.abs());
let ndt2_no_leading_zero = if ndt2_abs_formatted.starts_with("0.") {
&ndt2_abs_formatted[1..] } else {
&ndt2_abs_formatted
};
let ndt2_formatted = format!("{}{}", ndt2_sign, ndt2_no_leading_zero);
let ndt2_final = if ndt2_formatted.len() > 10 {
&ndt2_formatted[..10]
} else {
&ndt2_formatted
};
let nddt6_formatted = format_exponential(second_derivative);
let bstar_formatted = format_exponential(bstar);
let intl_des_formatted = format!("{:8}", intl_designator);
let intl_des_final = if intl_des_formatted.len() > 8 {
&intl_des_formatted[..8]
} else {
&intl_des_formatted
};
let line1_base = format!(
"1 {:5}{} {} {:02}{:012.8} {} {} {} {} {:04}",
norad_id_trimmed,
classification,
intl_des_final,
year_2digit,
day_of_year,
ndt2_final,
nddt6_formatted,
bstar_formatted,
ephemeris_type,
element_set_number
);
let line1_checksum = calculate_tle_line_checksum(&line1_base);
let line1 = format!("{}{}", line1_base, line1_checksum);
let ecc_formatted = format!("{:.7}", eccentricity);
let ecc_final = if let Some(stripped) = ecc_formatted.strip_prefix("0.") {
stripped
} else {
&ecc_formatted
};
let incl_norm = inclination.rem_euclid(360.0);
let raan_norm = raan.rem_euclid(360.0);
let argp_norm = arg_periapsis.rem_euclid(360.0);
let mean_anom_norm = mean_anomaly.rem_euclid(360.0);
let line2_base = format!(
"2 {:5} {:8.4} {:8.4} {} {:8.4} {:8.4} {:11.8}{:05}",
norad_id_trimmed,
incl_norm,
raan_norm,
ecc_final,
argp_norm,
mean_anom_norm,
mean_motion,
revolution_number
);
let line2_checksum = calculate_tle_line_checksum(&line2_base);
let line2 = format!("{}{}", line2_base, line2_checksum);
Ok((line1, line2))
}
fn format_exponential(value: f64) -> String {
if value == 0.0 {
return " 00000+0".to_string();
}
let abs_val = value.abs();
let sign = if value >= 0.0 { " " } else { "-" };
let log_val = abs_val.log10();
let exponent = log_val.floor() as i32;
let mantissa = abs_val / 10_f64.powi(exponent);
let body = (mantissa * 10000.0).round() as u32;
let exp_sign = if exponent >= 0 { "+" } else { "-" };
format!("{}{:05}{}{}", sign, body, exp_sign, exponent.abs())
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::RADIANS;
use crate::orbits::keplerian::semimajor_axis;
use approx::assert_abs_diff_eq;
use rstest::rstest;
#[rstest]
#[case(
"1 20580U 90037B 25261.05672437 .00006481 00000+0 23415-3 0 9990",
0
)]
#[case(
"1 24920U 97047A 25261.00856804 .00000165 00000+0 89800-4 0 9991",
1
)]
#[case(
"1 00900U 64063C 25261.21093924 .00000602 00000+0 60787-3 0 9992",
2
)]
#[case(
"1 26605U 00071A 25260.44643294 .00000025 00000+0 00000+0 0 9993",
3
)]
#[case(
"2 26410 146.0803 17.8086 8595307 233.2516 0.1184 0.44763667 19104",
4
)]
#[case(
"1 28414U 04035B 25261.30628127 .00003436 00000+0 25400-3 0 9995",
5
)]
#[case(
"1 28371U 04025F 25260.92882365 .00000356 00000+0 90884-4 0 9996",
6
)]
#[case(
"1 19751U 89001C 25260.63997541 .00000045 00000+0 00000+0 0 9997",
7
)]
#[case(
"1 29228U 06021A 25261.14661065 .00002029 00000+0 12599-3 0 9998",
8
)]
#[case(
"2 31127 98.3591 223.5782 0064856 30.4095 330.0844 14.63937036981529",
9
)]
fn test_calculate_tle_line_checksum(#[case] line: &str, #[case] expected: u32) {
let checksum = calculate_tle_line_checksum(line);
assert_eq!(checksum, expected);
}
#[rstest]
#[case("1 20580U 90037B 25261.05672437 .00006481 00000+0 23415-3 0 9990")]
#[case("1 24920U 97047A 25261.00856804 .00000165 00000+0 89800-4 0 9991")]
#[case("1 00900U 64063C 25261.21093924 .00000602 00000+0 60787-3 0 9992")]
#[case("1 26605U 00071A 25260.44643294 .00000025 00000+0 00000+0 0 9993")]
#[case("2 26410 146.0803 17.8086 8595307 233.2516 0.1184 0.44763667 19104")]
#[case("1 28414U 04035B 25261.30628127 .00003436 00000+0 25400-3 0 9995")]
#[case("1 28371U 04025F 25260.92882365 .00000356 00000+0 90884-4 0 9996")]
#[case("1 19751U 89001C 25260.63997541 .00000045 00000+0 00000+0 0 9997")]
#[case("1 29228U 06021A 25261.14661065 .00002029 00000+0 12599-3 0 9998")]
#[case("2 31127 98.3591 223.5782 0064856 30.4095 330.0844 14.63937036981529")]
fn test_validate_tle_line_valid(#[case] line: &str) {
assert!(validate_tle_line(line));
}
#[rstest]
#[case("1 20580U 90037B 25261.05672437 .00006481 00000+0 23415-3 0 9980")]
#[case("1 24920U 97047A 25261.00856804 .00000165 00000+0 89800-4 0 9931")]
#[case("1 00900U 64063C 25261.21093924 .00000602 00000+0 60787-3 0 9912")]
#[case("1 26605U 00071A 25260.44643294 .00000025 00000+0 00000+0 0 9983")]
#[case("2 26410 146.0803 17.8086 8595307 233.2516 0.1184 19104")]
#[case("1 28414U 04035B 25261.30628127 .00003436 00000+0 25400-3 0 9923421295")]
#[case("3 28371U 04025F 25260.92882365 .00000356 00000+0 90884-4 0 9996")]
#[case("3 19751U 89001C 25260.63997541 .00000045 00000+0 00000+0 0 9999")]
fn test_validate_tle_invalid(#[case] line: &str) {
assert!(!validate_tle_line(line));
}
#[rstest]
#[case(
"1 22195U 92070B 25260.83452377 -.00000009 00000+0 00000+0 0 9999",
"2 22195 52.6519 78.7552 0137761 68.4365 290.4819 6.47293897777784"
)]
#[case(
"1 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 9997",
"2 23613 13.4910 350.0515 0007963 105.8217 238.1991 1.00277726110516"
)]
fn test_validate_tle_lines_valid(#[case] line1: &str, #[case] line2: &str) {
assert!(validate_tle_lines(line1, line2));
}
#[rstest]
#[case(
"1 22195U 92070B 25260.83452377 -.00000009 00000+0 00000+0 0 9999",
"2 22196 52.6519 78.7552 0137761 68.4365 290.4819 6.47293897777784"
)]
#[case(
"1 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 9997",
"1 23613 13.4910 350.0515 0007963 105.8217 238.1991 1.00277726110516"
)]
#[case(
"1 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 999",
"2 23613 13.4910 350.0515 0007963 105.8217 238.1991 1.00277726110516"
)]
#[case(
"1 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 9997",
"2 23613 13.4910 350.0515 0007963 105.8217 238.1991 1.0027772611051"
)]
#[case(
"1 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 9997",
"3 23613 13.4910 350.0515 0007963 105.8217 238.1991 1.00277726110517"
)]
#[case(
"2 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 9998",
"2 23613 13.4910 350.0515 0007963 105.8217 238.1991 1.00277726110516"
)]
#[case(
"1 23613U 95035B 25260.68951341 -.00000252 00000+0 00000+0 0 9997",
"2 23614 13.4910 350.0515 0007963 105.8217 238.1991 1.00277726110517"
)]
fn test_validate_tle_lines_invalid(#[case] line1: &str, #[case] line2: &str) {
assert!(!validate_tle_lines(line1, line2));
}
#[rstest]
#[case("25544", 25544)]
#[case("00001", 1)]
#[case("99999", 99999)]
#[case(" 1", 1)]
fn test_parse_norad_id(#[case] id_str: &str, #[case] expected: u32) {
assert_eq!(parse_norad_id(id_str).unwrap(), expected);
}
#[rstest]
#[case("A0000", 100000)]
#[case("A0001", 100001)]
#[case("A9999", 109999)]
#[case("B0000", 110000)]
#[case("Z9999", 339999)]
#[case("B1234", 111234)]
#[case("C5678", 125678)]
#[case("D9012", 139012)]
#[case("E3456", 143456)]
#[case("F7890", 157890)]
#[case("G1234", 161234)]
#[case("H2345", 172345)]
#[case("J6789", 186789)]
#[case("K0123", 190123)]
#[case("L4567", 204567)]
#[case("M8901", 218901)]
#[case("N2345", 222345)]
#[case("P6789", 236789)]
#[case("Q0123", 240123)]
#[case("R4567", 254567)]
#[case("S8901", 268901)]
#[case("T2345", 272345)]
#[case("U6789", 286789)]
#[case("V0123", 290123)]
#[case("W4567", 304567)]
#[case("X8901", 318901)]
#[case("Y2345", 322345)]
#[case("Z6789", 336789)]
fn test_parse_norad_id_alpha5_valid(#[case] id_str: &str, #[case] expected: u32) {
assert_eq!(parse_norad_id(id_str).unwrap(), expected);
}
#[rstest]
#[case("I0001")] #[case("O1234")] #[case("A123")] #[case("A12345")] #[case("1234A")] #[case("!2345")] #[case("")] #[case(" ")] fn test_parse_norad_id_invalid(#[case] id_str: &str) {
assert!(parse_norad_id(id_str).is_err());
}
#[test]
fn test_keplerian_elements_from_tle() {
let line1 = "1 25544U 98067A 21001.50000000 .00001764 00000-0 40967-4 0 9997";
let line2 = "2 25544 51.6461 306.0234 0003417 88.1267 25.5695 15.48919103000003";
let result = keplerian_elements_from_tle(line1, line2);
assert!(result.is_ok());
let (epoch, elements) = result.unwrap();
assert_eq!(epoch.year(), 2021);
assert_eq!(epoch.month(), 1);
assert_eq!(epoch.day(), 1);
assert_eq!(epoch.hour(), 12);
assert_eq!(epoch.minute(), 0);
assert_abs_diff_eq!(epoch.second(), 0.0, epsilon = 1e-6);
let n_rad_per_sec = 15.48919103 * 2.0 * PI / 86400.0;
let a = semimajor_axis(n_rad_per_sec, RADIANS);
assert_abs_diff_eq!(elements[0], a, epsilon = 1.0e-3); assert_abs_diff_eq!(elements[1], 0.0003417, epsilon = 1.0e-7); assert_abs_diff_eq!(elements[2], 51.6461, epsilon = 1.0e-4); assert_abs_diff_eq!(elements[3], 306.0234, epsilon = 1.0e-4); assert_abs_diff_eq!(elements[4], 88.1267, epsilon = 1.0e-4); assert_abs_diff_eq!(elements[5], 25.5695, epsilon = 1.0e-4); }
#[test]
fn test_create_tle_lines() {
let epoch = Epoch::from_datetime(2021, 1, 1, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let semi_major_axis = 6786000.0; let eccentricity = 0.12345; let inclination = 51.6461; let raan = 306.0234; let arg_periapsis = 88.1267; let mean_anomaly = 25.5695;
let mean_motion_rad_per_sec =
(GM_EARTH / (semi_major_axis * semi_major_axis * semi_major_axis)).sqrt();
let mean_motion_revs_per_day = mean_motion_rad_per_sec * 86400.0 / (2.0 * PI);
let (line1, line2) = create_tle_lines(
&epoch,
"25544",
'U',
"98067A",
mean_motion_revs_per_day,
eccentricity,
inclination,
raan,
arg_periapsis,
mean_anomaly,
-0.00001764,
-0.00000067899,
-0.00012345,
0,
999,
12345,
)
.unwrap();
assert_eq!(
line1,
"1 25544U 98067A 21001.50000000 -.00001764 -67899-7 -12345-4 0 09995"
);
assert_eq!(
line2,
"2 25544 51.6461 306.0234 1234500 88.1267 25.5695 15.53037630123450"
);
let (line1, line2) = create_tle_lines(
&epoch,
"25544",
'U',
"98067A",
mean_motion_revs_per_day,
eccentricity,
inclination,
raan,
arg_periapsis,
mean_anomaly,
0.00001764,
0.00000067899,
0.00012345,
0,
999,
12345,
)
.unwrap();
assert_eq!(
line1,
"1 25544U 98067A 21001.50000000 .00001764 67899-7 12345-4 0 09992"
);
assert_eq!(
line2,
"2 25544 51.6461 306.0234 1234500 88.1267 25.5695 15.53037630123450"
);
}
#[test]
fn test_keplerian_elements_to_tle() {
let epoch = Epoch::from_datetime(2021, 1, 1, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let semi_major_axis = 6786000.0; let eccentricity = 0.12345; let inclination = 51.6461; let raan = 306.0234; let arg_periapsis = 88.1267; let mean_anomaly = 25.5695;
let elements = Vector6::new(
semi_major_axis,
eccentricity,
inclination,
raan,
arg_periapsis,
mean_anomaly,
);
let (line1, line2) = keplerian_elements_to_tle(&epoch, &elements, "25544").unwrap();
assert_eq!(
line1,
"1 25544U 21001.50000000 .00000000 00000+0 00000+0 0 00000"
);
assert_eq!(
line2,
"2 25544 51.6461 306.0234 1234500 88.1267 25.5695 15.53037630000005"
);
}
#[rstest]
#[case(0.0, " 00000+0")]
#[case(0.00012345, " 12345-4")]
#[case(-0.00012345, "-12345-4")]
#[case(12345.0, " 12345+4")]
#[case(-12345.0, "-12345+4")]
fn test_format_exponential(#[case] value: f64, #[case] expected: &str) {
assert_eq!(format_exponential(value), expected);
}
#[rstest]
#[case(0, "0")] #[case(1, "1")] #[case(42, "42")] #[case(12345, "12345")] #[case(99999, "99999")] #[case(100000, "A0000")] #[case(100001, "A0001")]
#[case(109999, "A9999")]
#[case(110000, "B0000")]
#[case(111234, "B1234")]
#[case(125678, "C5678")]
#[case(186789, "J6789")] #[case(236789, "P6789")] #[case(339999, "Z9999")] fn test_norad_id_numeric_to_alpha5_valid(#[case] norad_id: u32, #[case] expected: &str) {
assert_eq!(norad_id_numeric_to_alpha5(norad_id).unwrap(), expected);
}
#[rstest]
#[case(340000)] #[case(999999)] fn test_norad_id_numeric_to_alpha5_invalid(#[case] norad_id: u32) {
assert!(norad_id_numeric_to_alpha5(norad_id).is_err());
}
#[rstest]
#[case("A0000", 100000)]
#[case("A0001", 100001)]
#[case("A9999", 109999)]
#[case("B0000", 110000)]
#[case("B1234", 111234)]
#[case("C5678", 125678)]
#[case("J6789", 186789)] #[case("P6789", 236789)] #[case("Z9999", 339999)]
fn test_norad_id_alpha5_to_numeric_valid(#[case] alpha5_id: &str, #[case] expected: u32) {
assert_eq!(norad_id_alpha5_to_numeric(alpha5_id).unwrap(), expected);
}
#[rstest]
#[case("I0001")] #[case("O0001")] #[case("@0001")] #[case("A00012")] #[case("A00")] #[case("")] #[case("AAAAA")] fn test_norad_id_alpha5_to_numeric_invalid(#[case] alpha5_id: &str) {
assert!(norad_id_alpha5_to_numeric(alpha5_id).is_err());
}
#[rstest]
#[case(100000)]
#[case(100001)]
#[case(109999)]
#[case(110000)]
#[case(125678)]
#[case(186789)]
#[case(236789)]
#[case(339999)]
fn test_norad_id_alpha5_numeric_round_trip(#[case] id: u32) {
let alpha5 = norad_id_numeric_to_alpha5(id).unwrap();
let parsed_id = norad_id_alpha5_to_numeric(&alpha5).unwrap();
assert_eq!(
id, parsed_id,
"Round trip failed for ID {}: {} -> {}",
id, alpha5, parsed_id
);
}
#[test]
fn test_keplerian_tle_circularity() {
let original_epoch = Epoch::from_datetime(2021, 1, 1, 12, 0, 0.0, 0.0, TimeSystem::UTC);
let original_elements = Vector6::new(
6786000.0, 0.12345, 51.6461, 306.0234, 88.1267, 25.5695, );
let norad_id = "25544";
let (line1, line2) =
keplerian_elements_to_tle(&original_epoch, &original_elements, norad_id).unwrap();
let (recovered_epoch, recovered_elements) =
keplerian_elements_from_tle(&line1, &line2).unwrap();
assert_eq!(recovered_epoch.year(), original_epoch.year());
assert_eq!(recovered_epoch.month(), original_epoch.month());
assert_eq!(recovered_epoch.day(), original_epoch.day());
assert_eq!(recovered_epoch.hour(), original_epoch.hour());
assert_eq!(recovered_epoch.minute(), original_epoch.minute());
assert_abs_diff_eq!(
recovered_epoch.second(),
original_epoch.second(),
epsilon = 1e-6
);
assert_abs_diff_eq!(recovered_elements[0], original_elements[0], epsilon = 1.0); assert_abs_diff_eq!(recovered_elements[1], original_elements[1], epsilon = 1e-6); assert_abs_diff_eq!(recovered_elements[2], original_elements[2], epsilon = 1e-3); assert_abs_diff_eq!(recovered_elements[3], original_elements[3], epsilon = 1e-3); assert_abs_diff_eq!(recovered_elements[4], original_elements[4], epsilon = 1e-3); assert_abs_diff_eq!(recovered_elements[5], original_elements[5], epsilon = 1e-3); }
#[test]
fn test_tle_keplerian_circularity() {
let original_line1 =
"1 25544U 98067A 21001.50000000 .00001764 00000-0 40967-4 0 9997";
let original_line2 =
"2 25544 51.6461 306.0234 0003417 88.1267 25.5695 15.48919103000003";
let (epoch, elements) =
keplerian_elements_from_tle(original_line1, original_line2).unwrap();
let (recovered_line1, recovered_line2) =
keplerian_elements_to_tle(&epoch, &elements, "25544").unwrap();
let original_norad_id = original_line1[2..7].trim();
let recovered_norad_id = recovered_line1[2..7].trim();
assert_eq!(original_norad_id, recovered_norad_id);
let (_, original_elements) =
keplerian_elements_from_tle(original_line1, original_line2).unwrap();
let (_, recovered_elements) =
keplerian_elements_from_tle(&recovered_line1, &recovered_line2).unwrap();
assert_abs_diff_eq!(recovered_elements[0], original_elements[0], epsilon = 1.0); assert_abs_diff_eq!(recovered_elements[1], original_elements[1], epsilon = 1e-6); assert_abs_diff_eq!(recovered_elements[2], original_elements[2], epsilon = 1e-3); assert_abs_diff_eq!(recovered_elements[3], original_elements[3], epsilon = 1e-3); assert_abs_diff_eq!(recovered_elements[4], original_elements[4], epsilon = 1e-3); assert_abs_diff_eq!(recovered_elements[5], original_elements[5], epsilon = 1e-3); }
#[test]
fn test_epoch_from_tle_basic() {
let line1 = "1 25544U 98067A 21001.50000000 .00001764 00000-0 40967-4 0 9997";
let result = epoch_from_tle(line1);
assert!(result.is_ok());
let epoch = result.unwrap();
assert_eq!(epoch.year(), 2021);
assert_eq!(epoch.month(), 1);
assert_eq!(epoch.day(), 1);
assert_eq!(epoch.hour(), 12);
assert_eq!(epoch.minute(), 0);
assert_abs_diff_eq!(epoch.second(), 0.0, epsilon = 1e-6);
assert_eq!(epoch.time_system, TimeSystem::UTC);
}
#[rstest]
#[case(
"1 25544U 98067A 21001.50000000 .00001764 00000-0 40967-4 0 9997",
2021,
1,
1,
12,
0,
0.0
)]
#[case(
"1 25544U 98067A 21032.25000000 .00001764 00000-0 40967-4 0 9997",
2021,
2,
1,
6,
0,
0.0
)]
#[case(
"1 25544U 98067A 21365.00000000 .00001764 00000-0 40967-4 0 9997",
2021,
12,
31,
0,
0,
0.0
)]
#[case(
"1 25544U 98067A 56001.00000000 .00001764 00000-0 40967-4 0 9997",
2056,
1,
1,
0,
0,
0.0
)] #[case(
"1 25544U 98067A 57001.00000000 .00001764 00000-0 40967-4 0 9997",
1957,
1,
1,
0,
0,
0.0
)] #[case(
"1 25544U 98067A 00001.00000000 .00001764 00000-0 40967-4 0 9997",
2000,
1,
1,
0,
0,
0.0
)] #[case(
"1 25544U 98067A 99365.00000000 .00001764 00000-0 40967-4 0 9997",
1999,
12,
31,
0,
0,
0.0
)] fn test_epoch_from_tle_various_dates(
#[case] line1: &str,
#[case] year: u32,
#[case] month: u8,
#[case] day: u8,
#[case] hour: u8,
#[case] minute: u8,
#[case] second: f64,
) {
let epoch = epoch_from_tle(line1).unwrap();
assert_eq!(epoch.year(), year);
assert_eq!(epoch.month(), month);
assert_eq!(epoch.day(), day);
assert_eq!(epoch.hour(), hour);
assert_eq!(epoch.minute(), minute);
assert_abs_diff_eq!(epoch.second(), second, epsilon = 1e-6);
assert_eq!(epoch.time_system, TimeSystem::UTC);
}
#[test]
fn test_epoch_from_tle_fractional_day() {
let line1 = "1 25544U 98067A 21001.75000000 .00001764 00000-0 40967-4 0 9997";
let epoch = epoch_from_tle(line1).unwrap();
assert_eq!(epoch.year(), 2021);
assert_eq!(epoch.month(), 1);
assert_eq!(epoch.day(), 1);
assert_eq!(epoch.hour(), 18);
assert_eq!(epoch.minute(), 0);
assert_abs_diff_eq!(epoch.second(), 0.0, epsilon = 1e-6);
}
#[test]
fn test_epoch_from_tle_with_seconds() {
let line1 = "1 25544U 98067A 21001.50069444 .00001764 00000-0 40967-4 0 9997";
let epoch = epoch_from_tle(line1).unwrap();
assert_eq!(epoch.year(), 2021);
assert_eq!(epoch.month(), 1);
assert_eq!(epoch.day(), 1);
assert_eq!(epoch.hour(), 12);
assert_eq!(epoch.minute(), 0);
assert_abs_diff_eq!(epoch.second(), 60.0, epsilon = 1.0);
}
#[test]
fn test_epoch_from_tle_leap_year() {
let line1 = "1 25544U 98067A 20366.00000000 .00001764 00000-0 40967-4 0 9997";
let epoch = epoch_from_tle(line1).unwrap();
assert_eq!(epoch.year(), 2020);
assert_eq!(epoch.month(), 12);
assert_eq!(epoch.day(), 31);
assert_eq!(epoch.hour(), 0);
assert_eq!(epoch.minute(), 0);
assert_abs_diff_eq!(epoch.second(), 0.0, epsilon = 1e-6);
}
#[rstest]
#[case("1 25544U 98067A 21001.5000000")] #[case("Too short")] #[case("")] fn test_epoch_from_tle_invalid_lines(#[case] line1: &str) {
assert!(epoch_from_tle(line1).is_err());
}
#[test]
fn test_epoch_from_tle_consistency_with_keplerian() {
let line1 = "1 25544U 98067A 21001.50000000 .00001764 00000-0 40967-4 0 9997";
let line2 = "2 25544 51.6461 306.0234 0003417 88.1267 25.5695 15.48919103000003";
let epoch_direct = epoch_from_tle(line1).unwrap();
let (epoch_from_keplerian, _) = keplerian_elements_from_tle(line1, line2).unwrap();
assert_eq!(epoch_direct.year(), epoch_from_keplerian.year());
assert_eq!(epoch_direct.month(), epoch_from_keplerian.month());
assert_eq!(epoch_direct.day(), epoch_from_keplerian.day());
assert_eq!(epoch_direct.hour(), epoch_from_keplerian.hour());
assert_eq!(epoch_direct.minute(), epoch_from_keplerian.minute());
assert_abs_diff_eq!(
epoch_direct.second(),
epoch_from_keplerian.second(),
epsilon = 1e-6
);
}
}