celestial_pointing/commands/
lst.rs1use super::{Command, CommandOutput};
2use crate::error::{Error, Result};
3use crate::session::Session;
4use celestial_core::Angle;
5
6pub struct Lst;
7
8impl Command for Lst {
9 fn name(&self) -> &str {
10 "LST"
11 }
12 fn description(&self) -> &str {
13 "Set or show local sidereal time"
14 }
15
16 fn execute(&self, session: &mut Session, args: &[&str]) -> Result<CommandOutput> {
17 if args.is_empty() {
18 return show_lst(session);
19 }
20 if args[0].eq_ignore_ascii_case("CLEAR") {
21 session.lst_override = None;
22 return Ok(CommandOutput::Text("LST override cleared".to_string()));
23 }
24 let angle = parse_lst_args(args)?;
25 session.lst_override = Some(angle);
26 Ok(CommandOutput::Text(format_lst(angle)))
27 }
28}
29
30fn show_lst(session: &Session) -> Result<CommandOutput> {
31 match session.current_lst() {
32 Ok(lst) => Ok(CommandOutput::Text(format_lst(lst))),
33 Err(_) => Ok(CommandOutput::Text("No LST set".to_string())),
34 }
35}
36
37fn format_lst(lst: Angle) -> String {
38 let h = lst.hours();
39 let hh = libm::floor(h) as u32;
40 let mm = libm::floor((h - hh as f64) * 60.0) as u32;
41 let ss = (h - hh as f64) * 3600.0 - mm as f64 * 60.0;
42 format!("LST = {:02}h {:02}m {:06.3}s", hh, mm, ss)
43}
44
45fn parse_lst_args(args: &[&str]) -> Result<Angle> {
46 match args.len() {
47 1 => parse_decimal_hours(args[0]),
48 3 => parse_hms(args[0], args[1], args[2]),
49 _ => Err(Error::Parse(
50 "LST expects decimal hours (e.g. 14.5) or h m s (e.g. 14 30 00)".to_string(),
51 )),
52 }
53}
54
55fn parse_decimal_hours(s: &str) -> Result<Angle> {
56 let hours: f64 = s
57 .parse()
58 .map_err(|_| Error::Parse(format!("invalid LST value: {}", s)))?;
59 validate_hours(hours)?;
60 Ok(Angle::from_hours(hours))
61}
62
63fn parse_hms(h: &str, m: &str, s: &str) -> Result<Angle> {
64 let hh: f64 = h
65 .parse()
66 .map_err(|_| Error::Parse(format!("invalid hours: {}", h)))?;
67 let mm: f64 = m
68 .parse()
69 .map_err(|_| Error::Parse(format!("invalid minutes: {}", m)))?;
70 let ss: f64 = s
71 .parse()
72 .map_err(|_| Error::Parse(format!("invalid seconds: {}", s)))?;
73 let hours = hh + mm / 60.0 + ss / 3600.0;
74 validate_hours(hours)?;
75 Ok(Angle::from_hours(hours))
76}
77
78fn validate_hours(hours: f64) -> Result<()> {
79 if !(0.0..24.0).contains(&hours) {
80 return Err(Error::Parse(format!(
81 "LST must be in range [0, 24), got {}",
82 hours
83 )));
84 }
85 Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn show_when_no_lst_set() {
94 let mut session = Session::new();
95 let result = Lst.execute(&mut session, &[]).unwrap();
96 match result {
97 CommandOutput::Text(s) => assert_eq!(s, "No LST set"),
98 _ => panic!("expected Text output"),
99 }
100 }
101
102 #[test]
103 fn set_decimal_hours() {
104 let mut session = Session::new();
105 Lst.execute(&mut session, &["14.5"]).unwrap();
106 let lst = session.current_lst().unwrap();
107 assert_eq!(lst.hours(), 14.5);
108 }
109
110 #[test]
111 fn set_hms() {
112 let mut session = Session::new();
113 Lst.execute(&mut session, &["14", "30", "00"]).unwrap();
114 let lst = session.current_lst().unwrap();
115 assert_eq!(lst.hours(), 14.5);
116 }
117
118 #[test]
119 fn show_after_set() {
120 let mut session = Session::new();
121 Lst.execute(&mut session, &["14", "30", "00"]).unwrap();
122 let result = Lst.execute(&mut session, &[]).unwrap();
123 match result {
124 CommandOutput::Text(s) => assert!(s.starts_with("LST = 14h 30m")),
125 _ => panic!("expected Text output"),
126 }
127 }
128
129 #[test]
130 fn clear_override() {
131 let mut session = Session::new();
132 Lst.execute(&mut session, &["14.5"]).unwrap();
133 Lst.execute(&mut session, &["CLEAR"]).unwrap();
134 assert!(session.lst_override.is_none());
135 assert!(session.current_lst().is_err());
136 }
137
138 #[test]
139 fn clear_case_insensitive() {
140 let mut session = Session::new();
141 Lst.execute(&mut session, &["14.5"]).unwrap();
142 Lst.execute(&mut session, &["clear"]).unwrap();
143 assert!(session.lst_override.is_none());
144 }
145
146 #[test]
147 fn reject_out_of_range() {
148 let mut session = Session::new();
149 assert!(Lst.execute(&mut session, &["25.0"]).is_err());
150 assert!(Lst.execute(&mut session, &["-1.0"]).is_err());
151 }
152
153 #[test]
154 fn reject_invalid_input() {
155 let mut session = Session::new();
156 assert!(Lst.execute(&mut session, &["abc"]).is_err());
157 }
158
159 #[test]
160 fn reject_wrong_arg_count() {
161 let mut session = Session::new();
162 assert!(Lst.execute(&mut session, &["14", "30"]).is_err());
163 }
164}