1use crate::sgp4::SatRec;
2use crate::Instant;
3use crate::TimeScale;
4
5use crate::sgp4::{SGP4InitArgs, SGP4Source};
6
7mod fitting;
9
10use anyhow::{bail, Context, Result};
11
12const ALPHA5_MATCHING: &str = "ABCDEFGHJKLMNPQRSTUVWXYZ";
14
15#[derive(Clone, Debug, PartialEq, PartialOrd)]
72pub struct TLE {
73 pub name: String,
75 pub intl_desig: String,
77 pub sat_num: i32,
79 pub desig_year: i32,
81 pub desig_launch: i32,
83 pub desig_piece: String,
85 pub epoch: Instant,
87 pub mean_motion_dot: f64,
89 pub mean_motion_dot_dot: f64,
91 pub bstar: f64,
93 pub ephem_type: u8,
95 pub element_num: i32,
97 pub inclination: f64,
99 pub raan: f64,
101 pub eccen: f64,
103 pub arg_of_perigee: f64,
105 pub mean_anomaly: f64,
107 pub mean_motion: f64,
109 pub rev_num: i32,
111
112 pub(crate) satrec: Option<SatRec>,
113}
114
115impl SGP4Source for TLE {
116 fn epoch(&self) -> Instant {
117 self.epoch
118 }
119
120 fn satrec_mut(&mut self) -> &mut Option<SatRec> {
121 &mut self.satrec
122 }
123
124 fn sgp4_init_args(&self) -> anyhow::Result<SGP4InitArgs> {
125 use std::f64::consts::PI;
126
127 const TWOPI: f64 = PI * 2.0;
128
129 Ok(SGP4InitArgs {
130 jdsatepoch: self.epoch.as_jd_with_scale(TimeScale::UTC),
132 bstar: self.bstar,
133 no: self.mean_motion / (1440.0 / TWOPI),
135 ndot: self.mean_motion_dot / (1440.0 * 1440.0 / TWOPI),
136 nddot: self.mean_motion_dot_dot / (1440.0 * 1440.0 * 1440.0 / TWOPI),
137 ecco: self.eccen,
138 inclo: self.inclination.to_radians(),
139 nodeo: self.raan.to_radians(),
140 argpo: self.arg_of_perigee.to_radians(),
141 mo: self.mean_anomaly.to_radians(),
142 })
143 }
144}
145
146impl TLE {
147 pub fn from_lines(lines: &[String]) -> Result<Vec<Self>> {
181 let mut tles: Vec<Self> = Vec::<Self>::new();
182 let empty: &String = &String::new();
183 let mut line0: &String = empty;
184 let mut line1: &String = empty;
185 let mut line2: &String;
186
187 for line in lines {
188 if line.len() < 2 {
189 continue;
190 }
191 if line.chars().nth(0).unwrap() == '1'
192 && line.chars().nth(1).unwrap() == ' '
193 && line.len() == 69
194 {
195 line1 = line;
196 } else if line.chars().nth(0).unwrap() == '2'
197 && line.chars().nth(1).unwrap() == ' '
198 && line.len() == 69
199 {
200 line2 = line;
201 if line0.is_empty() {
202 tles.push(Self::load_2line(line1, line2)?);
203 } else {
204 tles.push(Self::load_3line(line0, line1, line2)?);
205 }
206 line0 = empty;
207 line1 = empty;
208 } else {
209 line0 = line;
210 }
211 }
212
213 Ok(tles)
214 }
215
216 pub fn new() -> Self {
220 Self {
221 name: "none".to_string(),
222 intl_desig: "".to_string(),
223 sat_num: 0,
224 desig_year: 0,
225 desig_launch: 0,
226 desig_piece: "A".to_string(),
227 epoch: Instant::J2000,
228 mean_motion_dot: 0.0,
229 mean_motion_dot_dot: 0.0,
230 bstar: 0.0,
231 ephem_type: b'U',
232 element_num: 0,
233 inclination: 0.0,
234 raan: 0.0,
235 eccen: 0.0,
236 arg_of_perigee: 0.0,
237 mean_anomaly: 0.0,
238 mean_motion: 0.0,
239 rev_num: 0,
240 satrec: None,
241 }
242 }
243
244 pub fn load_3line(line0: &str, line1: &str, line2: &str) -> Result<Self> {
279 if line1.len() < 69 || line2.len() < 69 {
280 bail!(
281 "Invalid TLE line lengths: line1 = {}, line2 = {}",
282 line1.len(),
283 line2.len()
284 );
285 }
286
287 match Self::load_2line(line1, line2) {
288 Ok(mut tle) => {
289 tle.name = {
290 if line0.len() > 2 && line0.chars().nth(0).unwrap() == '0' {
291 line0[2..].to_string()
292 } else {
293 String::from(line0)
294 }
295 };
296 Ok(tle)
297 }
298 Err(e) => Err(e),
299 }
300 }
301
302 pub fn load_2line(line1: &str, line2: &str) -> Result<Self> {
334 if line1.len() < 69 {
335 bail!(
336 "Line 1 too short: expected 69 characters, got {}",
337 line1.len()
338 );
339 }
340 if line2.len() < 69 {
341 bail!(
342 "Line 2 too short: expected 69 characters, got {}",
343 line2.len()
344 );
345 }
346
347 let mut year: u32 = {
348 let mut mstr: String = "1".to_owned();
349 mstr.push_str(&line1[18..20]);
350 let mut s = mstr.parse().context("Could not parse year")?;
351 s -= 100;
352 s
353 };
354 let century = if year >= 57 { 1900 } else { 2000 };
358 year += century;
359 let day_of_year: f64 = line1[20..32]
360 .parse()
361 .context("Could not parse day of year")?;
362
363 let epoch = Instant::from_date(year as i32, 1, 2)
367 .context("Invalid year, month, or day")?
368 .add_utc_days(day_of_year - 2.0);
369
370 Ok(Self {
371 name: "none".to_string(),
372 sat_num: Self::alpha5_to_int(&line1[2..7])
373 .context("Could not parse satellite number")?,
374
375 intl_desig: { line1[9..16].trim().to_string() },
376 desig_year: { line1[9..11].trim().parse().unwrap_or(70) },
377 desig_launch: { line1[11..14].trim().parse().unwrap_or_default() },
378 desig_piece: line1[14..18]
379 .trim()
380 .parse()
381 .context("Could not parse desig_piece")?,
382
383 epoch,
384 mean_motion_dot: {
385 let mut mstr: String = "0".to_owned();
386 mstr.push_str(&line1[34..43]);
387 let mut m = mstr.parse().context("Could not parse mean motion dot")?;
388 if line1.chars().nth(33).unwrap() == '-' {
389 m *= -1.0;
390 }
391 m
392 },
393 mean_motion_dot_dot: {
394 let mut mstr: String = "0.".to_owned();
395 mstr.push_str(&line1[45..50]);
396 mstr.push('E');
397 mstr.push_str(&line1[50..53]);
398 let mut m = mstr
399 .trim()
400 .parse()
401 .context("Coudl not parse mean motion dot dot")?;
402 if line1.chars().nth(44).unwrap() == '-' {
403 m *= -1.0;
404 }
405 m
406 },
407 bstar: {
408 let mut mstr: String = "0.".to_owned();
409 mstr.push_str(&line1[54..59]);
410 mstr.push('E');
411 mstr.push_str(&line1[59..62]);
412 let mut m = mstr
413 .trim()
414 .parse()
415 .context("Could not parse bstar (drag)")?;
416 if line1.chars().nth(53).unwrap() == '-' {
417 m *= -1.0;
418 }
419 m
420 },
421 ephem_type: { line1[62..63].trim().parse().unwrap_or_default() },
422 element_num: line1[64..68]
423 .trim()
424 .parse()
425 .context("Could not parse element number")?,
426
427 inclination: line2[8..16]
428 .trim()
429 .parse()
430 .context("Could not parse inclination")?,
431
432 raan: line2[17..25]
433 .trim()
434 .parse()
435 .context("Could not parse raan")?,
436
437 eccen: {
438 let mut mstr: String = "0.".to_owned();
439 mstr.push_str(&line2[26..33]);
440 mstr.trim()
441 .parse()
442 .context("Could not parse eccentricity")?
443 },
444 arg_of_perigee: line2[34..42]
445 .trim()
446 .parse()
447 .context("Could not parse arg of perigee")?,
448
449 mean_anomaly: line2[42..51]
450 .trim()
451 .parse()
452 .context("Could not parse mean anomaly")?,
453
454 mean_motion: line2[52..63]
455 .trim()
456 .parse()
457 .context("Could not parse mean motion")?,
458
459 rev_num: line2[63..68]
460 .trim()
461 .parse()
462 .context("Could not parse rev num")?,
463 satrec: None,
464 })
465 }
466
467 pub fn to_2line(&self) -> Result<[String; 2]> {
490 let (yy, doy) = self.epoch_to_tle_ydoy()?;
492
493 let sat_alpha5 = Self::int_to_alpha5(self.sat_num)?;
495
496 let (ndot_sign, ndot_body) = tle_formatter::format_ndot(self.mean_motion_dot);
498 let (nddot_sign, nddot_mant, nddot_exp2) =
499 tle_formatter::format_implied(self.mean_motion_dot_dot);
500 let (bstar_sign, bstar_mant, bstar_exp2) = tle_formatter::format_implied(self.bstar);
501
502 let et = if (0..=9).contains(&self.ephem_type) {
504 char::from(b'0' + self.ephem_type)
505 } else {
506 '0'
507 };
508
509 let sat5 = format!("{:<5}", sat_alpha5); let desig = format!("{:<8}", self.intl_desig); let epoch = format!("{:0>2}{:012.8}", yy, doy); let ndot = format!("{}{}", ndot_sign, ndot_body); let nddot = format!("{}{}{}", nddot_sign, nddot_mant, nddot_exp2); let bstar = format!("{}{}{}", bstar_sign, bstar_mant, bstar_exp2); let elem_no = format!("{:>4}", self.element_num.max(0)); let mut l1 = format!("1 {sat5}U {desig} {epoch} {ndot} {nddot} {bstar} {et} {elem_no}");
524
525 let cksum1 = tle_formatter::tle_checksum(&l1);
526 l1.push(char::from(b'0' + cksum1));
527
528 let incl = format!("{:8.4}", self.inclination);
530 let raan = format!("{:8.4}", self.raan);
531 let ecc7 = format!("{:0>7}", (self.eccen.abs() * 1.0e7 + 0.5).floor() as u64);
532 let argp = format!("{:8.4}", self.arg_of_perigee);
533 let mean_anom = format!("{:8.4}", self.mean_anomaly);
534 let n = format!("{:11.8}", self.mean_motion);
535 let rev = format!("{:>5}", self.rev_num.max(0));
536
537 let mut l2 = format!("2 {sat_alpha5:<5} {incl} {raan} {ecc7} {argp} {mean_anom} {n}{rev}");
538
539 if l2.len() != 68 {
541 if l2.len() < 68 {
542 l2.push_str(&" ".repeat(68 - l2.len()));
543 } else {
544 l2.truncate(68);
545 }
546 }
547 let cksum2 = tle_formatter::tle_checksum(&l2);
548 l2.push(char::from(b'0' + cksum2));
549
550 Ok([l1, l2])
551 }
552
553 pub fn to_3line(&self) -> Result<[String; 3]> {
580 let [l1, l2] = self.to_2line()?;
581 Ok([self.name.clone(), l1, l2])
582 }
583
584 fn epoch_to_tle_ydoy(&self) -> Result<(u8, f64)> {
586 let (year, _, _, _, _, _) = self.epoch.as_datetime();
587
588 if !(1957..=2056).contains(&year) {
589 bail!("Year out of range for TLE: {}", year);
590 }
591
592 let doy_int = self.epoch.day_of_year();
594
595 let frac = self.epoch.as_mjd_utc() % 1.0;
599 let doy = (doy_int as f64) + frac;
600 let century = if year >= 1957 { 1900 } else { 2000 };
604 let year = ((year - century) % 100) as u8;
605 Ok((year, doy))
606 }
607
608 pub fn alpha5_to_int(alpha5: &str) -> Result<i32> {
633 match alpha5.chars().nth(0) {
634 Some(c) if c.is_ascii_digit() || c.is_whitespace() => match alpha5.trim().parse() {
637 Ok(i) => Ok(i),
638 Err(e) => bail!("Invalid sat num: {}", e.to_string()),
639 },
640 Some(c) if c.is_alphabetic() => {
641 match ALPHA5_MATCHING
642 .chars()
643 .position(|m| m == c.to_ascii_uppercase())
644 {
645 Some(p) => match alpha5[1..].parse::<i32>() {
646 Ok(i) => Ok((p as i32 + 10) * 10000 + i),
647 Err(e) => bail!("Invalid sat num: {}", e.to_string()),
648 },
649 None => bail!("Invalid first digit in sat num: {}", c),
650 }
651 }
652 Some(c) => bail!("Invalid first digit in sat num: {}", c),
653 None => bail!("Parse error"),
654 }
655 }
656
657 pub fn int_to_alpha5(sat_num: i32) -> Result<String> {
682 match sat_num {
683 i @ 0..=99999 => Ok(format!("{:0>5}", i)),
684 i @ 100000..=339999 => {
685 let c = ALPHA5_MATCHING
686 .chars()
687 .nth(i as usize / 10000 - 10)
688 .unwrap();
689 Ok(format!("{c}{:0>4}", i % 10000))
690 }
691 _i @ 340000.. => bail!("Sat num >= 340000 cannot be represented in alpha5 format"),
692 _ => bail!("Invalid sat num value"),
693 }
694 }
695
696 pub fn to_pretty_string(&self) -> String {
714 format!(
715 r#"
716 TLE: {}
717 NORAD ID: {},
718 Launch Year: {},
719 Epoch: {},
720 Mean Motion Dot: {} revs / day^2,
721 Mean Motion Dot Dot: {} revs / day^3,
722 Drag: {},
723 Inclination: {} deg,
724 RAAN: {} deg,
725 eccen: {},
726 Arg of Perigee: {} deg,
727 Mean Anomaly: {} deg,
728 Mean Motion: {} revs / day
729 Rev #: {}
730 "#,
731 self.name,
732 Self::int_to_alpha5(self.sat_num).unwrap(),
733 match self.desig_year > 50 {
734 true => self.desig_year + 1900,
735 false => self.desig_year + 2000,
736 },
737 self.epoch,
738 self.mean_motion_dot * 2.0,
739 self.mean_motion_dot_dot * 6.0,
740 self.bstar,
741 self.inclination,
742 self.raan,
743 self.eccen,
744 self.arg_of_perigee,
745 self.mean_anomaly,
746 self.mean_motion,
747 self.rev_num,
748 )
749 }
750}
751
752impl Default for TLE {
753 fn default() -> Self {
754 Self::new()
755 }
756}
757
758impl std::fmt::Display for TLE {
759 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
760 write!(f, "{}", self.to_pretty_string())
761 }
762}
763
764mod tle_formatter {
765
766 pub fn format_ndot(v: f64) -> (char, String) {
768 let sign = if v < 0.0 { '-' } else { ' ' };
769 let mut body = format!("{:.8}", v.abs());
770 if let Some(stripped) = body.strip_prefix('0') {
771 body = stripped.to_string(); }
773 if body.len() < 9 {
775 body = format!("{:>9}", body);
776 } else if body.len() > 9 {
777 body.truncate(9);
778 }
779 (sign, body)
780 }
781
782 pub fn format_implied(v: f64) -> (char, String, String) {
785 if v == 0.0 {
786 return (' ', "00000".to_string(), "-0".to_string());
788 }
789 let sign = if v < 0.0 { '-' } else { ' ' };
790 let x = v.abs();
791
792 let mut e10 = x.log10().floor() as i32; let mut mant = (x / 10f64.powi(e10) * 1.0e4).round() as i64;
795
796 if mant == 100_000 {
798 mant = 10_000;
799 e10 += 1;
800 }
801
802 let e = e10 + 1;
805 let mant_s = format!("{:0>5}", mant.max(0));
806
807 let e_clamped = e.clamp(-9, 9);
809 let exp_s = format!("{:+}", e_clamped);
810
811 (sign, mant_s, exp_s)
812 }
813
814 pub fn tle_checksum(s: &str) -> u8 {
816 let mut sum: u32 = 0;
817 for (i, c) in s.chars().enumerate() {
818 if i >= 68 {
819 break;
820 }
821 sum += match c {
822 '0'..='9' => c as u32 - '0' as u32,
823 '-' => 1,
824 _ => 0,
825 };
826 }
827 (sum % 10) as u8
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 #[test]
836 fn testload() -> Result<()> {
837 let line1: &str = "1 26900U 01039A 06106.74503247 .00000045 00000-0 10000-3 0 8290";
838 let line2: &str =
839 "2 26900 0.0164 266.5378 0003319 86.1794 182.2590 1.00273847 16981 9300.";
840 let line0: &str = "0 INTELSAT 902";
841 match TLE::load_3line(line0, line1, line2) {
842 Ok(_t) => {}
843
844 Err(s) => {
845 bail!("load_3line: Err = \"{}\"", s);
846 }
847 }
848 match TLE::load_2line(line1, line2) {
849 Ok(_t) => {}
850 Err(s) => {
851 bail!("load_2line: Err = \"{}\"", s);
852 }
853 }
854 Ok(())
855 }
856
857 #[test]
858 fn test_from_lines() -> Result<()> {
859 let lines = vec![
860 "2023-193D".to_string(),
861 "1 58556U 23193D 25003.79555039 .00279397 31144-4 86159-3 0 9996".to_string(),
862 "2 58556 97.2472 26.1173 0004235 271.4738 88.6051 15.91743157 60937".to_string(),
863 "0 CPOD FLT2 (TYVAK-0033)".to_string(),
864 "1 52780U 22057BB 23036.86744141 .00018086 00000-0 87869-3 0 9991".to_string(),
865 "2 52780 97.5313 154.3283 0011660 53.1934 307.0368 15.18441019 16465".to_string(),
866 "1998-067WV".to_string(),
867 "1 60955U 98067WV 24295.33823779 .06453473 12009-4 26290-2 0 9998".to_string(),
868 "2 60955 51.6166 43.0490 0010894 336.3668 23.6849 16.22453324 8315".to_string(),
869 "2 PATHFINDER".to_string(),
870 "1 45727U 20037E 24323.73967089 .00003818 00000+0 31595-3 0 9995".to_string(),
871 "2 45727 97.7798 139.6782 0011624 329.2427 30.8113 14.99451155239085".to_string(),
872 "0 SHINSEI (MS-F2)".to_string(),
873 "1 5485U 71080A 24324.43728894 .00000099 00000-0 13784-3 0 9992".to_string(),
874 "2 5485 32.0564 70.0187 0639723 198.9447 158.6281 12.74214074476065".to_string(),
875 "OSCAR 7 (AO-7)".to_string(),
876 "1 07530U 74089B 24323.87818483 -.00000039 00000+0 47934-4 0 9997".to_string(),
877 "2 07530 101.9893 320.0351 0012269 147.9195 274.9996 12.53682684288423".to_string(),
878 "1 52743U 22057M 23037.04954473 .00011781 00000-0 61944-3 0 9993".to_string(),
879 "2 52743 97.5265 153.6940 0008594 82.9904 31.3082 15.15793680 38769".to_string(),
880 "0 ISS (ZARYA)".to_string(),
881 "1 B5544U 98067A 24356.58519896 .00014389 00000-0 25222-3 0 9992".to_string(),
882 "2 B5544 51.6403 106.8969 0007877 6.1421 113.2479 15.50801739487615".to_string(), "0 ISS (ZARYA)".to_string(),
884 "1 Z9999U 98067A 24356.58519896 .00014389 00000-0 25222-3 0 9992".to_string(),
885 "2 Z9999 51.6403 106.8969 0007877 6.1421 113.2479 15.50801739487615".to_string(), ];
887
888 let tles = match TLE::from_lines(&lines) {
889 Ok(t) => t,
890 Err(s) => {
891 bail!("load_lines: Err = \"{}\"", s);
892 }
893 };
894
895 if tles.len() != 9 {
896 bail!("load_lines: Err = \"Incorrect number of elements parsed\"");
897 }
898
899 if tles[0].name != "2023-193D" {
900 bail!(
901 "load_lines: Err = \"Error parsing sat name {}\"",
902 tles[0].name
903 );
904 }
905
906 if tles[1].name != "CPOD FLT2 (TYVAK-0033)" {
907 bail!(
908 "load_lines: Err = \"Error parsing sat name {}\"",
909 tles[1].name
910 );
911 }
912
913 if tles[2].name != "1998-067WV" {
914 bail!(
915 "load_lines: Err = \"Error parsing sat name {}\"",
916 tles[2].name
917 );
918 }
919
920 if tles[3].name != "2 PATHFINDER" {
921 bail!(
922 "load_lines: Err = \"Error parsing sat name {}\"",
923 tles[3].name
924 );
925 }
926
927 if tles[4].name != "SHINSEI (MS-F2)" {
928 bail!(
929 "load_lines: Err = \"Error parsing sat name {}\"",
930 tles[4].name
931 );
932 }
933
934 if tles[4].sat_num != 5485 {
935 bail!(
936 "load_lines: Err = \"Error parsing sat num {}\"",
937 tles[4].sat_num
938 );
939 }
940
941 if tles[5].name != "OSCAR 7 (AO-7)" {
942 bail!(
943 "load_lines: Err = \"Error parsing sat name {}\"",
944 tles[5].name
945 );
946 }
947
948 if tles[5].sat_num != 7530 {
949 bail!(
950 "load_lines: Err = \"Error parsing sat num {}\"",
951 tles[5].sat_num
952 );
953 }
954
955 if tles[6].name != "none" {
956 bail!(
957 "load_lines: Err = \"Error parsing sat name {}\"",
958 tles[6].name
959 );
960 }
961
962 Ok(())
963 }
964
965 #[test]
966 fn test_from_invalid_from_lines() -> Result<()> {
967 let res = TLE::from_lines(&[
968 "0 INVALID TLE".to_string(),
969 "1 12345U 67890A 12345.67890123 .00000123 00000-0 12345-6 0 9992".to_string(),
970 "2 12345 51.6403 106.8969 0007877 6.1421 113.2479 15.50801739487615".to_string(),
971 ]);
972 assert!(res.is_err(), "Expected error due to short lines, got OK");
973 assert!(
974 res.unwrap_err()
975 .to_string()
976 .contains("Invalid TLE line lengths"),
977 "Expected error about invalid line lengths."
978 );
979
980 Ok(())
981 }
982
983 #[test]
984 fn test_from_invalid_tle2() -> Result<()> {
985 let res = TLE::load_2line(
986 "1 12345U 67890A 12345.67890123 .00000123 00000-0 12345-6 0 9992",
987 "2 12345 51.6403 106.8969 0007877 6.1421 113.2479",
988 );
989 assert!(res.is_err(), "Expected error due to short line2, got OK");
990 assert!(
991 res.unwrap_err().to_string().contains("too short"),
992 "Expected error about line being too short."
993 );
994
995 Ok(())
996 }
997
998 #[test]
999 fn test_from_invalid_tle3() -> Result<()> {
1000 let res = TLE::load_3line(
1001 "0 INVALID TLE",
1002 "1 12345U 67890A 12345.67890123 .00000123 00000-0 12345-6 0 9992",
1003 "2 12345 51.6403 106.8969 0007877 6.1421 113.2479",
1004 );
1005 assert!(res.is_err(), "Expected error due to short line2, got OK");
1006 assert!(
1007 res.unwrap_err()
1008 .to_string()
1009 .contains("Invalid TLE line lengths"),
1010 "Expected error about invalid line lengths."
1011 );
1012 Ok(())
1013 }
1014
1015 #[test]
1016 fn test_alpha5_to_int() -> Result<()> {
1017 match TLE::alpha5_to_int("00091") {
1019 Ok(91) => {}
1020 Ok(i) => bail!("Error parsing '00091' as 91: got {}", i),
1021 Err(e) => bail!("Error parsing '00091' as 91: {}", e),
1022 }
1023
1024 match TLE::alpha5_to_int(" 982") {
1026 Ok(982) => {}
1027 Ok(i) => bail!("Error parsing ' 982' as 982: got {}", i),
1028 Err(e) => bail!("Error parsing ' 982' as 982: {}", e),
1029 }
1030
1031 match TLE::alpha5_to_int("99993") {
1033 Ok(99993) => {}
1034 Ok(i) => bail!("Error parsing '99993' as 99993: got {}", i),
1035 Err(e) => bail!("Error parsing '99993' as 99993: {}", e),
1036 }
1037
1038 match TLE::alpha5_to_int("S9994") {
1040 Ok(269994) => {}
1041 Ok(i) => bail!("Error parsing 'S9994' as 269994: got {}", i),
1042 Err(e) => bail!("Error parsing 'S9994' as 269994: {}", e),
1043 }
1044
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn test_int_to_alpha5() -> Result<()> {
1050 match TLE::int_to_alpha5(91) {
1051 Ok(ref s) if s == "00091" => {}
1052 Ok(ref s) => bail!("Error converting 91 to '00091': got {}", s),
1053 Err(e) => bail!("Error converting 91 to '00091': {}", e),
1054 }
1055
1056 match TLE::int_to_alpha5(99993) {
1057 Ok(ref s) if s == "99993" => {}
1058 Ok(ref s) => bail!("Error converting 99993 to '99993': got {}", s),
1059 Err(e) => bail!("Error converting 99993 to '99993': {}", e),
1060 }
1061
1062 match TLE::int_to_alpha5(269994) {
1064 Ok(ref s) if s == "S9994" => {}
1065 Ok(ref s) => bail!("Error converting 269994 to 'S9994': got {}", s),
1066 Err(e) => bail!("Error converting 269994 to 'S9994': {}", e),
1067 }
1068
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn test_3line_encoding() -> Result<()> {
1074 let line0 = "ISS (ZARYA)";
1075 let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
1076 let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
1077
1078 let orig = TLE::load_3line(line0, line1, line2)?;
1079
1080 let [l0, l1, l2] = orig.to_3line()?;
1082
1083 assert_eq!(l1, line1, "Line 1 must match original");
1085 assert_eq!(l2, line2, "Line 2 must match original");
1086 assert_eq!(l0, line0, "Line 0 (name) must be preserved");
1087
1088 Ok(())
1089 }
1090
1091 #[test]
1092 fn test_2line_encoding() -> Result<()> {
1093 let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
1094 let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
1095
1096 let orig = TLE::load_2line(line1, line2)?;
1097
1098 let [l1, l2] = orig.to_2line()?;
1100
1101 assert_eq!(l1, line1, "Line 1 must match original");
1103 assert_eq!(l2, line2, "Line 2 must match original");
1104
1105 Ok(())
1106 }
1107
1108 #[test]
1109 fn test_2line_encoding_many_times() -> Result<()> {
1110 let tle_examples = vec![
1111 [
1112 "1 58556U 23193D 25003.79555039 .00279397 31144-4 86159-3 0 9996".to_string(),
1114 "2 58556 97.2472 26.1173 0004235 271.4738 88.6051 15.91743157 60937".to_string(),
1115 ],
1116 [
1117 "1 52780U 22057BB 23036.86744141 .00018086 00000-0 87869-3 0 9991".to_string(),
1119 "2 52780 97.5313 154.3283 0011660 53.1934 307.0368 15.18441019 16465".to_string(),
1120 ],
1121 [
1122 "1 60955U 98067WV 24295.33823779 .06453473 12009-4 26290-2 0 9998".to_string(),
1124 "2 60955 51.6166 43.0490 0010894 336.3668 23.6849 16.22453324 8315".to_string(),
1125 ],
1126 [
1127 "1 45727U 20037E 24323.73967089 .00003818 00000+0 31595-3 0 9995".to_string(),
1129 "2 45727 97.7798 139.6782 0011624 329.2427 30.8113 14.99451155239085".to_string(),
1130 ],
1131 [
1137 "1 07530U 74089B 24323.87818483 -.00000039 00000+0 47934-4 0 9997".to_string(),
1139 "2 07530 101.9893 320.0351 0012269 147.9195 274.9996 12.53682684288423".to_string(),
1140 ],
1141 [
1142 "1 52743U 22057M 23037.04954473 .00011781 00000-0 61944-3 0 9993".to_string(),
1143 "2 52743 97.5265 153.6940 0008594 82.9904 31.3082 15.15793680 38769".to_string(),
1144 ],
1145 [
1146 "1 B5544U 98067A 24356.58519896 .00014389 00000-0 25222-3 0 9992".to_string(),
1148 "2 B5544 51.6403 106.8969 0007877 6.1421 113.2479 15.50801739487613".to_string(),
1149 ],
1150 [
1151 "1 Z9999U 98067A 24356.58519896 .00014389 00000-0 25222-3 0 9992".to_string(),
1153 "2 Z9999 51.6403 106.8969 0007877 6.1421 113.2479 15.50801739487611".to_string(),
1154 ],
1155 ];
1156
1157 for tle in tle_examples {
1158 let tle_loaded = TLE::load_2line(&tle[0], &tle[1])?;
1159 let [l1, l2] = tle_loaded.to_2line()?;
1160
1161 if tle[0].contains(" 00000+0 ") {
1164 let mut expected: String = tle[0].replace(" 00000+0 ", " 00000-0 ");
1165
1166 if let Some(last_char) = expected.chars().last() {
1168 if let Some(digit) = last_char.to_digit(10) {
1169 let new_digit = (digit + 1) % 10; expected.pop(); expected.push(char::from_digit(new_digit, 10).unwrap());
1172 }
1173 }
1174
1175 assert_eq!(l1, expected, "Line 1 must match original");
1176 } else {
1177 assert_eq!(l2, tle[1], "Line 2 must match original");
1178 }
1179 }
1180
1181 Ok(())
1182 }
1183
1184
1185 #[test]
1186 fn test_2line_encoding_with_invalid_past_date() -> Result<()> {
1187 let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
1188 let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
1189
1190 let mut tle = TLE::load_2line(line1, line2)?;
1191 tle.epoch = Instant::from_date(1952, 6, 13)?;
1192
1193 let result = tle.to_2line();
1194
1195 assert!(result.is_err(), "Expected error due to epoch before 1957, got {:?}", result);
1197
1198 Ok(())
1199 }
1200
1201 #[test]
1202 fn test_2line_encoding_with_invalid_future_date() -> Result<()> {
1203 let line1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
1204 let line2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
1205
1206 let mut tle = TLE::load_2line(line1, line2)?;
1207 tle.epoch = Instant::from_date(2057, 6, 13)?;
1208
1209 let result = tle.to_2line();
1210
1211 assert!(result.is_err(), "Expected error due to epoch after 2056, got {:?}", result);
1213
1214 Ok(())
1215 }
1216}