1#![doc = include_str!("../README.md")]
16#![cfg_attr(not(feature = "std"), no_std)]
17
18use core::{convert::TryFrom, fmt};
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum AirQualityError {
23 OutOfRange,
25}
26
27impl fmt::Display for AirQualityError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 AirQualityError::OutOfRange => f.write_str("Value is out of range for AQI"),
31 }
32 }
33}
34
35#[cfg(feature = "std")]
36impl std::error::Error for AirQualityError {}
37
38pub type Result<R> = core::result::Result<R, AirQualityError>;
40
41#[derive(Copy, Clone, Debug, Eq, PartialEq)]
43pub enum AirQualityLevel {
44 Good,
46 Moderate,
49 UnhealthySensitive,
51 Unhealthy,
53 VeryUnhealthy,
55 Hazardous,
57}
58
59impl fmt::Display for AirQualityLevel {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 use AirQualityLevel::*;
62 match self {
63 Good => f.write_str("Good"),
64 Moderate => f.write_str("Moderate"),
65 UnhealthySensitive => f.write_str("Unhealthy for sensitive groups"),
66 Unhealthy => f.write_str("Unhealthy"),
67 VeryUnhealthy => f.write_str("Very unhealthy"),
68 Hazardous => f.write_str("Hazardous"),
69 }
70 }
71}
72
73macro_rules! def_try_from_aq {
74 ($tpe:ty) => {
75 impl TryFrom<$tpe> for AirQualityLevel {
76 type Error = AirQualityError;
77 fn try_from(v: $tpe) -> Result<Self> {
78 use AirQualityLevel::*;
79 match v {
80 0..=50 => Ok(Good),
81 51..=100 => Ok(Moderate),
82 101..=150 => Ok(UnhealthySensitive),
83 151..=200 => Ok(Unhealthy),
84 201..=300 => Ok(VeryUnhealthy),
85 301..=500 => Ok(Hazardous),
86 _ => Err(AirQualityError::OutOfRange),
87 }
88 }
89 }
90 };
91}
92
93def_try_from_aq!(u16);
94def_try_from_aq!(i16);
95def_try_from_aq!(u32);
96def_try_from_aq!(i32);
97def_try_from_aq!(u64);
98def_try_from_aq!(i64);
99
100#[derive(Copy, Clone, Debug, Eq, PartialEq)]
102pub struct AirQuality {
103 aqi: u32,
105 level: AirQualityLevel,
107}
108
109impl AirQuality {
110 pub fn new(aqi: u32, level: AirQualityLevel) -> Self {
111 Self { aqi, level }
112 }
113
114 pub fn aqi(&self) -> u32 {
115 self.aqi
116 }
117
118 pub fn level(&self) -> AirQualityLevel {
119 self.level
120 }
121}
122
123impl fmt::Display for AirQuality {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(f, "AQI: {}, {}", self.aqi, self.level)
126 }
127}
128
129struct Breakpoint {
130 conc_low: f64,
131 conc_high: f64,
132 aqi_low: u32,
133 aqi_high: u32,
134 level: AirQualityLevel,
135}
136
137const OZONE8_BREAKPOINTS: [Breakpoint; 5] = [
138 Breakpoint {
139 conc_low: 0.000,
140 conc_high: 0.054,
141 aqi_low: 0,
142 aqi_high: 50,
143 level: AirQualityLevel::Good,
144 },
145 Breakpoint {
146 conc_low: 0.055,
147 conc_high: 0.070,
148 aqi_low: 51,
149 aqi_high: 100,
150 level: AirQualityLevel::Moderate,
151 },
152 Breakpoint {
153 conc_low: 0.071,
154 conc_high: 0.085,
155 aqi_low: 101,
156 aqi_high: 150,
157 level: AirQualityLevel::UnhealthySensitive,
158 },
159 Breakpoint {
160 conc_low: 0.086,
161 conc_high: 0.105,
162 aqi_low: 151,
163 aqi_high: 200,
164 level: AirQualityLevel::Unhealthy,
165 },
166 Breakpoint {
167 conc_low: 0.106,
168 conc_high: 0.200,
169 aqi_low: 201,
170 aqi_high: 300,
171 level: AirQualityLevel::VeryUnhealthy,
172 },
173];
174const OZONE1_BREAKPOINTS: [Breakpoint; 5] = [
175 Breakpoint {
176 conc_low: 0.125,
177 conc_high: 0.164,
178 aqi_low: 101,
179 aqi_high: 150,
180 level: AirQualityLevel::UnhealthySensitive,
181 },
182 Breakpoint {
183 conc_low: 0.165,
184 conc_high: 0.204,
185 aqi_low: 151,
186 aqi_high: 200,
187 level: AirQualityLevel::Unhealthy,
188 },
189 Breakpoint {
190 conc_low: 0.205,
191 conc_high: 0.404,
192 aqi_low: 201,
193 aqi_high: 300,
194 level: AirQualityLevel::VeryUnhealthy,
195 },
196 Breakpoint {
197 conc_low: 0.405,
198 conc_high: 0.504,
199 aqi_low: 301,
200 aqi_high: 400,
201 level: AirQualityLevel::Hazardous,
202 },
203 Breakpoint {
204 conc_low: 0.505,
205 conc_high: 0.604,
206 aqi_low: 401,
207 aqi_high: 500,
208 level: AirQualityLevel::Hazardous,
209 },
210];
211const PM25_BREAKPOINTS: [Breakpoint; 7] = [
212 Breakpoint {
213 conc_low: 0.0,
214 conc_high: 12.0,
215 aqi_low: 0,
216 aqi_high: 50,
217 level: AirQualityLevel::Good,
218 },
219 Breakpoint {
220 conc_low: 12.1,
221 conc_high: 35.4,
222 aqi_low: 51,
223 aqi_high: 100,
224 level: AirQualityLevel::Moderate,
225 },
226 Breakpoint {
227 conc_low: 35.5,
228 conc_high: 55.4,
229 aqi_low: 101,
230 aqi_high: 150,
231 level: AirQualityLevel::UnhealthySensitive,
232 },
233 Breakpoint {
234 conc_low: 55.5,
235 conc_high: 150.4,
236 aqi_low: 151,
237 aqi_high: 200,
238 level: AirQualityLevel::Unhealthy,
239 },
240 Breakpoint {
241 conc_low: 150.5,
242 conc_high: 250.4,
243 aqi_low: 201,
244 aqi_high: 300,
245 level: AirQualityLevel::VeryUnhealthy,
246 },
247 Breakpoint {
248 conc_low: 250.5,
249 conc_high: 350.4,
250 aqi_low: 301,
251 aqi_high: 400,
252 level: AirQualityLevel::Hazardous,
253 },
254 Breakpoint {
255 conc_low: 350.5,
256 conc_high: 500.4,
257 aqi_low: 401,
258 aqi_high: 500,
259 level: AirQualityLevel::Hazardous,
260 },
261];
262const PM10_BREAKPOINTS: [Breakpoint; 7] = [
263 Breakpoint {
264 conc_low: 0.0,
265 conc_high: 54.0,
266 aqi_low: 0,
267 aqi_high: 50,
268 level: AirQualityLevel::Good,
269 },
270 Breakpoint {
271 conc_low: 55.0,
272 conc_high: 154.0,
273 aqi_low: 51,
274 aqi_high: 100,
275 level: AirQualityLevel::Moderate,
276 },
277 Breakpoint {
278 conc_low: 155.0,
279 conc_high: 254.0,
280 aqi_low: 101,
281 aqi_high: 150,
282 level: AirQualityLevel::UnhealthySensitive,
283 },
284 Breakpoint {
285 conc_low: 255.0,
286 conc_high: 354.0,
287 aqi_low: 151,
288 aqi_high: 200,
289 level: AirQualityLevel::Unhealthy,
290 },
291 Breakpoint {
292 conc_low: 355.0,
293 conc_high: 424.0,
294 aqi_low: 201,
295 aqi_high: 300,
296 level: AirQualityLevel::VeryUnhealthy,
297 },
298 Breakpoint {
299 conc_low: 425.0,
300 conc_high: 504.0,
301 aqi_low: 301,
302 aqi_high: 400,
303 level: AirQualityLevel::Hazardous,
304 },
305 Breakpoint {
306 conc_low: 505.0,
307 conc_high: 604.0,
308 aqi_low: 401,
309 aqi_high: 500,
310 level: AirQualityLevel::Hazardous,
311 },
312];
313const CO_BREAKPOINTS: [Breakpoint; 7] = [
314 Breakpoint {
315 conc_low: 0.0,
316 conc_high: 4.4,
317 aqi_low: 0,
318 aqi_high: 50,
319 level: AirQualityLevel::Good,
320 },
321 Breakpoint {
322 conc_low: 4.5,
323 conc_high: 9.4,
324 aqi_low: 51,
325 aqi_high: 100,
326 level: AirQualityLevel::Moderate,
327 },
328 Breakpoint {
329 conc_low: 9.5,
330 conc_high: 12.4,
331 aqi_low: 101,
332 aqi_high: 150,
333 level: AirQualityLevel::UnhealthySensitive,
334 },
335 Breakpoint {
336 conc_low: 12.5,
337 conc_high: 15.4,
338 aqi_low: 151,
339 aqi_high: 200,
340 level: AirQualityLevel::Unhealthy,
341 },
342 Breakpoint {
343 conc_low: 15.5,
344 conc_high: 30.4,
345 aqi_low: 201,
346 aqi_high: 300,
347 level: AirQualityLevel::VeryUnhealthy,
348 },
349 Breakpoint {
350 conc_low: 30.5,
351 conc_high: 40.4,
352 aqi_low: 301,
353 aqi_high: 400,
354 level: AirQualityLevel::Hazardous,
355 },
356 Breakpoint {
357 conc_low: 40.5,
358 conc_high: 50.4,
359 aqi_low: 401,
360 aqi_high: 500,
361 level: AirQualityLevel::Hazardous,
362 },
363];
364const SO2_1_BREAKPOINTS: [Breakpoint; 3] = [
365 Breakpoint {
366 conc_low: 0.0,
367 conc_high: 35.0,
368 aqi_low: 0,
369 aqi_high: 50,
370 level: AirQualityLevel::Good,
371 },
372 Breakpoint {
373 conc_low: 36.0,
374 conc_high: 75.0,
375 aqi_low: 51,
376 aqi_high: 100,
377 level: AirQualityLevel::Moderate,
378 },
379 Breakpoint {
380 conc_low: 76.0,
381 conc_high: 185.0,
382 aqi_low: 101,
383 aqi_high: 150,
384 level: AirQualityLevel::UnhealthySensitive,
385 },
386];
387const SO2_24_BREAKPOINTS: [Breakpoint; 7] = [
388 Breakpoint {
389 conc_low: 0.0,
390 conc_high: 35.0,
391 aqi_low: 0,
392 aqi_high: 50,
393 level: AirQualityLevel::Good,
394 },
395 Breakpoint {
396 conc_low: 36.0,
397 conc_high: 75.0,
398 aqi_low: 51,
399 aqi_high: 100,
400 level: AirQualityLevel::Moderate,
401 },
402 Breakpoint {
403 conc_low: 76.0,
404 conc_high: 185.0,
405 aqi_low: 101,
406 aqi_high: 150,
407 level: AirQualityLevel::UnhealthySensitive,
408 },
409 Breakpoint {
410 conc_low: 186.0,
411 conc_high: 304.0,
412 aqi_low: 151,
413 aqi_high: 200,
414 level: AirQualityLevel::Unhealthy,
415 },
416 Breakpoint {
417 conc_low: 305.0,
418 conc_high: 604.0,
419 aqi_low: 201,
420 aqi_high: 300,
421 level: AirQualityLevel::VeryUnhealthy,
422 },
423 Breakpoint {
424 conc_low: 605.0,
425 conc_high: 804.0,
426 aqi_low: 301,
427 aqi_high: 400,
428 level: AirQualityLevel::Hazardous,
429 },
430 Breakpoint {
431 conc_low: 805.0,
432 conc_high: 1004.0,
433 aqi_low: 401,
434 aqi_high: 500,
435 level: AirQualityLevel::Hazardous,
436 },
437];
438const NO2_BREAKPOINTS: [Breakpoint; 7] = [
439 Breakpoint {
440 conc_low: 0.0,
441 conc_high: 53.0,
442 aqi_low: 0,
443 aqi_high: 50,
444 level: AirQualityLevel::Good,
445 },
446 Breakpoint {
447 conc_low: 54.0,
448 conc_high: 100.0,
449 aqi_low: 51,
450 aqi_high: 100,
451 level: AirQualityLevel::Moderate,
452 },
453 Breakpoint {
454 conc_low: 101.0,
455 conc_high: 360.0,
456 aqi_low: 101,
457 aqi_high: 150,
458 level: AirQualityLevel::UnhealthySensitive,
459 },
460 Breakpoint {
461 conc_low: 361.0,
462 conc_high: 649.0,
463 aqi_low: 151,
464 aqi_high: 200,
465 level: AirQualityLevel::Unhealthy,
466 },
467 Breakpoint {
468 conc_low: 650.0,
469 conc_high: 1249.0,
470 aqi_low: 201,
471 aqi_high: 300,
472 level: AirQualityLevel::VeryUnhealthy,
473 },
474 Breakpoint {
475 conc_low: 1250.0,
476 conc_high: 1649.0,
477 aqi_low: 301,
478 aqi_high: 400,
479 level: AirQualityLevel::Hazardous,
480 },
481 Breakpoint {
482 conc_low: 1650.0,
483 conc_high: 2049.0,
484 aqi_low: 401,
485 aqi_high: 500,
486 level: AirQualityLevel::Hazardous,
487 },
488];
489
490fn find_breakpoint(breakpoints: &[Breakpoint], concentration: f64) -> Option<&Breakpoint> {
491 breakpoints.iter().find(|breakpoint| {
492 breakpoint.conc_low <= concentration && concentration <= breakpoint.conc_high
493 })
494}
495
496fn calc_aqi(breakpoints: &[Breakpoint], concentration: f64) -> Result<AirQuality> {
497 if let Some(breakpoint) = find_breakpoint(breakpoints, concentration) {
498 let aqi = ((breakpoint.aqi_high as f64 - breakpoint.aqi_low as f64)
499 / (breakpoint.conc_high - breakpoint.conc_low))
500 * (concentration - breakpoint.conc_low)
501 + (breakpoint.aqi_low as f64);
502 Ok(AirQuality {
503 aqi: round(aqi),
504 level: breakpoint.level,
505 })
506 } else {
507 Err(AirQualityError::OutOfRange)
508 }
509}
510
511fn trunc(value: f64, nplaces: u32) -> f64 {
512 let truncator = 10_u32.pow(nplaces) as f64;
513 ((value * truncator) as u64) as f64 / truncator
514}
515
516pub fn ozone8(concentration: f64) -> Result<AirQuality> {
526 calc_aqi(&OZONE8_BREAKPOINTS, trunc(concentration, 3))
527}
528
529pub fn ozone1(concentration: f64) -> Result<AirQuality> {
539 calc_aqi(&OZONE1_BREAKPOINTS, trunc(concentration, 3))
540}
541
542pub fn pm2_5(concentration: f64) -> Result<AirQuality> {
550 calc_aqi(&PM25_BREAKPOINTS, trunc(concentration, 1))
551}
552
553pub fn pm2_5_epa(concentration: f64, humidity: f64) -> Result<AirQuality> {
568 if (0.0..=1.0).contains(&humidity) {
569 calc_aqi(
570 &PM25_BREAKPOINTS,
571 trunc(0.52 * concentration - 0.085 * humidity + 5.71, 1),
572 )
573 } else {
574 Err(AirQualityError::OutOfRange)
575 }
576}
577
578pub fn pm2_5_lrapa(concentration: f64) -> Result<AirQuality> {
592 if concentration <= 65.0 {
593 calc_aqi(&PM25_BREAKPOINTS, trunc(0.5 * concentration - 0.66, 1))
594 } else {
595 Err(AirQualityError::OutOfRange)
596 }
597}
598
599pub fn pm2_5_aqandu(concentration: f64) -> Result<AirQuality> {
612 calc_aqi(&PM25_BREAKPOINTS, trunc(0.778 * concentration + 2.65, 1))
613}
614
615pub fn pm10(concentration: f64) -> Result<AirQuality> {
623 calc_aqi(&PM10_BREAKPOINTS, concentration as u32 as f64)
624}
625
626pub fn co(concentration: f64) -> Result<AirQuality> {
634 calc_aqi(&CO_BREAKPOINTS, trunc(concentration, 1))
635}
636
637pub fn so2_1(concentration: f64) -> Result<AirQuality> {
647 calc_aqi(&SO2_1_BREAKPOINTS, trunc(concentration, 0))
648}
649
650pub fn so2_24(concentration: f64) -> Result<AirQuality> {
658 calc_aqi(&SO2_24_BREAKPOINTS, trunc(concentration, 0))
659}
660
661pub fn no2(concentration: f64) -> Result<AirQuality> {
669 calc_aqi(&NO2_BREAKPOINTS, trunc(concentration, 0))
670}
671
672fn round(val: f64) -> u32 {
673 #[cfg(feature = "std")]
674 let res = val.round() as u32;
675
676 #[cfg(not(feature = "std"))]
677 let res = {
678 let whole = val as u32;
679 let frac = val - (whole as f64);
680 if frac >= 0.5 {
681 whole + 1
682 } else {
683 whole
684 }
685 };
686
687 res
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn test_pm2_5() {
696 let test_data: [(f64, u32); 22] = [
697 (0.0, 0),
698 (12.0, 50),
699 (12.1, 51),
700 (16.0, 59),
701 (35.4, 100),
702 (35.5, 101),
703 (55.4, 150),
704 (55.5, 151),
705 (85.0, 166),
706 (94.0, 171),
707 (138.0, 194),
708 (150.4, 200),
709 (150.5, 201),
710 (158.0, 208),
711 (175.0, 225),
712 (192.0, 242),
713 (200.0, 250),
714 (250.4, 300),
715 (250.5, 301),
716 (350.4, 400),
717 (350.5, 401),
718 (500.4, 500),
719 ];
720
721 for (conc, aqi) in test_data.iter() {
722 assert_eq!(Ok(*aqi), pm2_5(*conc).map(|aq| aq.aqi));
723 }
724 }
725
726 #[test]
727 fn test_round() {
728 assert_eq!(round(4.5), 5);
729 assert_eq!(round(123.3), 123);
730 assert_eq!(round(84.9), 85);
731 }
732}