1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub const SPEED_OF_LIGHT_MPS: f64 = 299_792_458.0;
6
7pub const VISIBLE_MIN_NM: f64 = 380.0;
9
10pub const VISIBLE_MAX_NM: f64 = 780.0;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum SpectralBand {
16 Ultraviolet,
17 Visible,
18 Infrared,
19}
20
21impl SpectralBand {
22 #[must_use]
24 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::Ultraviolet => "ultraviolet",
27 Self::Visible => "visible",
28 Self::Infrared => "infrared",
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct SpectralSample {
36 pub wavelength_nm: f64,
37 pub intensity: f64,
38}
39
40impl SpectralSample {
41 #[must_use]
43 pub const fn new(wavelength_nm: f64, intensity: f64) -> Self {
44 Self {
45 wavelength_nm,
46 intensity,
47 }
48 }
49
50 #[must_use]
52 pub fn band(self) -> Option<SpectralBand> {
53 classify_wavelength_nm(self.wavelength_nm)
54 }
55}
56
57#[must_use]
59pub fn classify_wavelength_nm(wavelength_nm: f64) -> Option<SpectralBand> {
60 if !wavelength_nm.is_finite() || wavelength_nm <= 0.0 {
61 return None;
62 }
63
64 if wavelength_nm < VISIBLE_MIN_NM {
65 Some(SpectralBand::Ultraviolet)
66 } else if wavelength_nm <= VISIBLE_MAX_NM {
67 Some(SpectralBand::Visible)
68 } else {
69 Some(SpectralBand::Infrared)
70 }
71}
72
73#[must_use]
75pub fn is_visible_wavelength_nm(wavelength_nm: f64) -> bool {
76 matches!(
77 classify_wavelength_nm(wavelength_nm),
78 Some(SpectralBand::Visible)
79 )
80}
81
82#[must_use]
84pub fn clamp_visible_wavelength_nm(wavelength_nm: f64) -> Option<f64> {
85 if !wavelength_nm.is_finite() || wavelength_nm <= 0.0 {
86 return None;
87 }
88
89 Some(wavelength_nm.clamp(VISIBLE_MIN_NM, VISIBLE_MAX_NM))
90}
91
92#[must_use]
94pub fn wavelength_to_frequency_hz(wavelength_nm: f64) -> Option<f64> {
95 if !wavelength_nm.is_finite() || wavelength_nm <= 0.0 {
96 return None;
97 }
98
99 Some(SPEED_OF_LIGHT_MPS / (wavelength_nm * 1.0e-9))
100}
101
102pub mod prelude {
104 pub use super::{
105 SPEED_OF_LIGHT_MPS, SpectralBand, SpectralSample, VISIBLE_MAX_NM, VISIBLE_MIN_NM,
106 clamp_visible_wavelength_nm, classify_wavelength_nm, is_visible_wavelength_nm,
107 wavelength_to_frequency_hz,
108 };
109}
110
111#[cfg(test)]
112mod tests {
113 use super::{
114 SpectralBand, SpectralSample, VISIBLE_MAX_NM, VISIBLE_MIN_NM, clamp_visible_wavelength_nm,
115 classify_wavelength_nm, is_visible_wavelength_nm, wavelength_to_frequency_hz,
116 };
117
118 #[test]
119 fn classifies_common_wavelength_ranges() {
120 assert_eq!(
121 classify_wavelength_nm(350.0),
122 Some(SpectralBand::Ultraviolet)
123 );
124 assert_eq!(classify_wavelength_nm(532.0), Some(SpectralBand::Visible));
125 assert_eq!(classify_wavelength_nm(900.0), Some(SpectralBand::Infrared));
126 assert_eq!(classify_wavelength_nm(0.0), None);
127 }
128
129 #[test]
130 fn detects_visible_wavelengths() {
131 assert!(is_visible_wavelength_nm(VISIBLE_MIN_NM));
132 assert!(is_visible_wavelength_nm(VISIBLE_MAX_NM));
133 assert!(!is_visible_wavelength_nm(300.0));
134 }
135
136 #[test]
137 fn clamps_into_visible_range() {
138 assert_eq!(clamp_visible_wavelength_nm(300.0), Some(VISIBLE_MIN_NM));
139 assert_eq!(clamp_visible_wavelength_nm(500.0), Some(500.0));
140 assert_eq!(clamp_visible_wavelength_nm(900.0), Some(VISIBLE_MAX_NM));
141 }
142
143 #[test]
144 fn converts_wavelength_to_frequency() {
145 let frequency = wavelength_to_frequency_hz(500.0).expect("500 nm should be valid");
146
147 assert!((frequency - 5.995_849_16e14).abs() < 1.0e7);
148 }
149
150 #[test]
151 fn sample_reports_its_band() {
152 let sample = SpectralSample::new(450.0, 0.75);
153
154 assert_eq!(sample.band(), Some(SpectralBand::Visible));
155 }
156}