///|
/// The breakpoint tables of HJ 633—2026, 表3.
///
/// Each table is one column of 表3 turned into rows, one per step.
///
/// Two columns stop at 800 µg/m³ and the standard gives the sub-index above
/// that rather than carrying the column on: SO₂ over one hour is reported as
/// 200, O₃ over eight hours as 300. That is what `iaqi` already does above a
/// table, so the columns are stored truncated.
///|
/// The pollutants the standard's tables cover.
pub(all) enum Pollutant {
/// PM2.5, fine particulate matter.
Pm25
/// PM10, coarse particulate matter.
Pm10
/// Sulphur dioxide.
So2
/// Nitrogen dioxide.
No2
/// Carbon monoxide, the one pollutant measured in milligrams.
Co
/// Ozone.
O3
}
///|
/// Two pollutants are the same when they are the same pollutant.
pub extend Pollutant with Eq::{not_equal, equal}
///|
pub impl Eq for Pollutant with fn equal(self, other) {
match (self, other) {
(Pm25, Pm25) => true
(Pm10, Pm10) => true
(So2, So2) => true
(No2, No2) => true
(Co, Co) => true
(O3, O3) => true
_ => false
}
}
///|
/// The averaging window a concentration is reported over.
pub(all) enum Averaging {
/// One hour, for the real-time report.
OneHour
/// Eight hours, the window the daily report judges ozone over.
EightHour
/// A day, for the daily report.
Daily
}
///|
/// Every pollutant, in the order the standard's table lists them.
pub fn Pollutant::all() -> Array[Pollutant] {
[Pm25, Pm10, So2, No2, Co, O3]
}
///|
/// Every averaging window.
pub fn Averaging::all() -> Array[Averaging] {
[OneHour, EightHour, Daily]
}
///|
/// The name the standard uses.
pub fn Pollutant::name(self : Pollutant) -> String {
match self {
Pm25 => "PM2.5"
Pm10 => "PM10"
So2 => "SO2"
No2 => "NO2"
Co => "CO"
O3 => "O3"
}
}
///|
/// The unit a concentration is measured in: mg/m3 for carbon monoxide, ug/m3
/// for the rest.
pub fn Pollutant::unit(self : Pollutant) -> String {
match self {
Co => "mg/m3"
_ => "ug/m3"
}
}
///|
/// The eight index points every column of 表3 is read at.
fn index_axis() -> Array[Int] {
[0, 50, 100, 150, 200, 300, 400, 500]
}
///|
/// Turn one concentration column of 表3 into rows. `indices` must be as long
/// as `concentrations`.
fn column(
concentrations : Array[Double],
indices : Array[Int],
) -> Array[Breakpoint] {
let rows : Array[Breakpoint] = []
for i = 0; i + 1 < concentrations.length(); i = i + 1 {
rows.push(
Breakpoint::new(
concentrations[i],
concentrations[i + 1],
indices[i],
indices[i + 1],
),
)
}
rows
}
///|
/// The breakpoint table for a pollutant and averaging window — 表3 of
/// HJ 633—2026.
///
/// `None` for a pair the standard publishes no column for.
pub fn hj633_2026(
pollutant : Pollutant,
averaging : Averaging,
) -> Array[Breakpoint]? {
let index = index_axis()
match (pollutant, averaging) {
(Pm25, Daily) =>
Some(column([0.0, 35.0, 60.0, 115.0, 150.0, 250.0, 350.0, 500.0], index))
(Pm10, Daily) =>
Some(column([0.0, 50.0, 120.0, 250.0, 350.0, 420.0, 500.0, 600.0], index))
(So2, Daily) =>
Some(
column([0.0, 50.0, 150.0, 475.0, 800.0, 1600.0, 2100.0, 2620.0], index),
)
(No2, Daily) =>
Some(column([0.0, 40.0, 80.0, 180.0, 280.0, 565.0, 750.0, 940.0], index))
(Co, Daily) =>
Some(column([0.0, 2.0, 4.0, 14.0, 24.0, 36.0, 48.0, 60.0], index))
(O3, EightHour) =>
Some(
column([0.0, 100.0, 160.0, 215.0, 265.0, 800.0], [
0, 50, 100, 150, 200, 300,
]),
)
(So2, OneHour) =>
Some(column([0.0, 150.0, 500.0, 650.0, 800.0], [0, 50, 100, 150, 200]))
(No2, OneHour) =>
Some(
column(
[0.0, 100.0, 200.0, 700.0, 1200.0, 2340.0, 3090.0, 3840.0],
index,
),
)
(Co, OneHour) =>
Some(column([0.0, 5.0, 10.0, 35.0, 60.0, 90.0, 120.0, 150.0], index))
(O3, OneHour) =>
Some(
column([0.0, 160.0, 200.0, 300.0, 400.0, 800.0, 1000.0, 1200.0], index),
)
_ => None
}
}