#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SignalPhase {
Green,
Red,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalTiming {
pub cycle_s: f64,
pub offset_s: f64,
pub green_phases_s: Vec<f64>,
}
impl SignalTiming {
pub fn new(cycle_s: f64, offset_s: f64, green_phases_s: Vec<f64>) -> Self {
Self {
cycle_s,
offset_s,
green_phases_s,
}
}
pub fn phase_at(&self, sim_time_s: f64) -> (SignalPhase, f64) {
if self.cycle_s <= 0.0 {
return (SignalPhase::Green, 0.0);
}
let total_green: f64 = self.green_phases_s.iter().sum();
if total_green <= 0.0 {
return (SignalPhase::Red, self.cycle_s);
}
if total_green >= self.cycle_s {
return (SignalPhase::Green, 0.0);
}
let effective = ((sim_time_s - self.offset_s) % self.cycle_s + self.cycle_s) % self.cycle_s;
if effective < total_green {
let remaining_green = total_green - effective;
(SignalPhase::Green, remaining_green)
} else {
let remaining_red = self.cycle_s - effective;
(SignalPhase::Red, remaining_red)
}
}
pub fn total_green_s(&self) -> f64 {
self.green_phases_s.iter().sum()
}
pub fn total_red_s(&self) -> f64 {
(self.cycle_s - self.total_green_s()).max(0.0)
}
pub fn green_ratio(&self) -> f64 {
if self.cycle_s <= 0.0 {
0.0
} else {
(self.total_green_s() / self.cycle_s).clamp(0.0, 1.0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn green_at_start_of_cycle() {
let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
let (phase, remaining) = timing.phase_at(0.0);
assert_eq!(phase, SignalPhase::Green);
assert!((remaining - 30.0).abs() < 1e-6);
}
#[test]
fn red_in_second_half() {
let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
let (phase, remaining) = timing.phase_at(45.0);
assert_eq!(phase, SignalPhase::Red);
assert!((remaining - 15.0).abs() < 1e-6);
}
#[test]
fn wraps_around_cycle() {
let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
let (phase, _) = timing.phase_at(65.0);
assert_eq!(phase, SignalPhase::Green);
let (phase, _) = timing.phase_at(95.0);
assert_eq!(phase, SignalPhase::Red);
}
#[test]
fn offset_shifts_phase() {
let timing = SignalTiming::new(60.0, 10.0, vec![30.0]);
let (phase, _) = timing.phase_at(10.0);
assert_eq!(phase, SignalPhase::Green);
let (phase, _) = timing.phase_at(5.0);
assert_eq!(phase, SignalPhase::Red);
}
#[test]
fn zero_cycle_always_green() {
let timing = SignalTiming::new(0.0, 0.0, vec![30.0]);
let (phase, _) = timing.phase_at(100.0);
assert_eq!(phase, SignalPhase::Green);
}
#[test]
fn all_green_cycle() {
let timing = SignalTiming::new(60.0, 0.0, vec![60.0]);
for t in [0.0, 30.0, 59.9] {
let (phase, _) = timing.phase_at(t);
assert_eq!(phase, SignalPhase::Green);
}
}
#[test]
fn zero_green_always_red() {
let timing = SignalTiming::new(60.0, 0.0, vec![]);
let (phase, _) = timing.phase_at(0.0);
assert_eq!(phase, SignalPhase::Red);
}
#[test]
fn multiple_green_phases_summed() {
let timing = SignalTiming::new(60.0, 0.0, vec![10.0, 10.0]);
let (phase, _) = timing.phase_at(15.0);
assert_eq!(phase, SignalPhase::Green);
let (phase, _) = timing.phase_at(25.0);
assert_eq!(phase, SignalPhase::Red);
}
#[test]
fn green_ratio_computed_correctly() {
let timing = SignalTiming::new(60.0, 0.0, vec![30.0]);
assert!((timing.green_ratio() - 0.5).abs() < 1e-6);
assert!((timing.total_red_s() - 30.0).abs() < 1e-6);
}
}