Skip to main content

celestial_pointing/commands/
lst.rs

1use 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}