Air quality index in pure MoonBit: the Chinese HJ 633-2026 and US EPA breakpoint tables, a day of hourly readings averaged into the indicators the standards describe, a period summarised, the dominant pollutant, and the EPA NowCast.
///|
let table = @moonaqi.hj633_2026(@moonaqi.Pm25, @moonaqi.Daily).unwrap()
let sub = @moonaqi.iaqi(table, 60.0) // Some(100)
///|
let readings = [
@moonaqi.Reading::of(@moonaqi.Pm25, @moonaqi.Daily, 60.0),
@moonaqi.Reading::of(@moonaqi.No2, @moonaqi.Daily, 40.0),
]
let air = @moonaqi.aqi(readings).unwrap()
air.index() // 100
air.category().name() // "Good"
air.dominant() // [Pm25]IAQI = (I_high - I_low) / (C_high - C_low) * (C - C_low) + I_low///|
let iaqi = @moonaqi.iaqi(
[
@moonaqi.Breakpoint::new(0.0, 35.0, 0, 50),
@moonaqi.Breakpoint::new(35.0, 75.0, 50, 100),
@moonaqi.Breakpoint::new(75.0, 115.0, 100, 150),
],
55.0,
)| Pollutant | One hour | Eight hours | Daily |
|---|---|---|---|
| PM2.5 | — | — | ✓ |
| PM10 | — | — | ✓ |
| SO₂ | ✓ | — | ✓ |
| NO₂ | ✓ | — | ✓ |
| CO | ✓ | — | ✓ |
| O₃ | ✓ | ✓ | — |
///|
let air = @moonaqi.aqi([
@moonaqi.Reading::of(@moonaqi.Pm25, @moonaqi.Daily, 60.0),
@moonaqi.Reading::of(@moonaqi.No2, @moonaqi.Daily, 40.0),
]).unwrap()| China | EPA | |
|---|---|---|
| Rounding | carried up | nearest integer |
| Before the lookup | as measured | truncated to 1–3 places |
| Units | µg/m³, mg/m³ | µg/m³, ppm, ppb |
///|
let air = @moonaqi.aqi_with(@moonaqi.Epa, [
@moonaqi.Reading::new(@moonaqi.Pm25, @moonaqi.Daily, 20.0),
@moonaqi.Reading::new(@moonaqi.O3, @moonaqi.EightHour, 0.07),
]).unwrap()///|
let nowcast = @moonaqi.nowcast_pm25([Some(30.0), Some(20.0), Some(10.0)])///|
let day = @moonaqi.Day::new("STATION")
///|
// One pass per hour, recording every pollutant, so that gaps in one series
// cannot shift another's hours.
for hour = 0; hour < 24; hour = hour + 1 {
day.record(@moonaqi.Pm25, Some(35.0))
day.record(@moonaqi.O3, Some(if hour < 12 { 100.0 } else { 160.0 }))
}
///|
let rules = @moonaqi.DayRules::new(
@moonaqi.Window::new(24, 20), // a day over 24 hours, at least 20 reported
@moonaqi.Window::new(8, 6), // ozone over its highest 8, at least 6
)
///|
day.aqi(@moonaqi.China, rules).unwrap()///|
let summary = @moonaqi.Summary::new()
///|
summary.add("STATION A", day.aqi(@moonaqi.China, rules).unwrap())
///|
summary.days() // days counted
summary.good_days() // index at or below 100
summary.heavy_days() // index above 200
summary.good_rate() // the share of the period, as a fraction of one
summary.worst_index() // and the station it came from
summary.most_frequent_primary()| Index | Band | 中文 | Colour |
|---|---|---|---|
| 0–50 | Excellent | 优 | Green 绿色 |
| 51–100 | Good | 良 | Yellow 黄色 |
| 101–150 | Lightly polluted | 轻度污染 | Orange 橙色 |
| 151–200 | Moderately polluted | 中度污染 | Red 红色 |
| 201–300 | Heavily polluted | 重度污染 | Purple 紫色 |
| 301+ | Severely polluted | 严重污染 | Maroon 褐红色 |
$ moon run cmd/main
daily report (日报)
pollutant concentration IAQI
PM2.5 115 ug/m3 150
PM10 120 ug/m3 100
SO2 50 ug/m3 50
NO2 40 ug/m3 50
CO 4 mg/m3 100
O3 160 ug/m3 100
AQI 150 Lightly polluted / 轻度污染
primary pollutant PM2.5
over the limit PM2.5$ moon run cmd/main -- --station STATION \
--series PM2.5=35x24 --series NO2=,40x23 --series O3=100x12,160x12
moonaqi — a day of hours, averaged and indexed
station STATION (HJ 633—2026)
rules a day over 24 hours with at least 20 reported, ozone over its highest 8 with at least 6
PM2.5 24 hours, 24 reported 24-hour average 35 ug/m3
NO2 24 hours, 23 reported 24-hour average 40 ug/m3
O3 24 hours, 24 reported 8-hour maximum 160 ug/m3
AQI 100 Good / 良 Yellow / 黄色
primary pollutant O3$ moon run cmd/main -- --json
{
"report": "daily",
"aqi": 150,
"category": "Lightly polluted",
"chinese_category": "轻度污染",
...
}| What | Where it comes from |
|---|---|
| China's breakpoints, categories and rounding | HJ 633—2026《环境空气质量指数(AQI)技术规定》, Ministry of Ecology and Environment of China, in force from 2026-03-01. 表3 holds the breakpoint values, §4.2.6 says the index is carried up rather than rounded, and 表3's footnotes say what to report above 800 µg/m³. |
| China's superseded breakpoints | HJ 633—2012, which the 2026 standard replaced on the same date. |
| The EPA's breakpoints | The US EPA Air Quality System code table aqi_breakpoints, where the EPA publishes the current values. |
| The EPA's truncation, rounding and band colours | EPA 454/B-18-007, Technical Assistance Document for the Reporting of Daily Air Quality — the Air Quality Index (AQI). |
| The NowCast algorithm | The EPA's own implementation, nowcast_pm25, in the US EPA sensortoolkit repository. |
| The averaging rule | The window lengths are the standards'; the minimum number of reported hours is a parameter, because each authority sets it and has revised it. |
moon testpub(all) enum AqiCategory {
Excellent
Good
LightlyPolluted
ModeratelyPolluted
HeavilyPolluted
SeverelyPolluted
}pub(all) enum Averaging {
OneHour
EightHour
Daily
}pub struct Breakpoint {
concentration_low : Double
concentration_high : Double
index_low : Int
index_high : Int
}fn Breakpoint::new(concentration_low : Double, concentration_high : Double, index_low : Int, index_high : Int) -> Breakpointpub(all) enum Pollutant {
Pm25
Pm10
So2
No2
Co
O3
}pub(all) enum Report {
Daily
RealTime
}pub struct Rgb {
red : Int
green : Int
blue : Int
}pub(all) enum Rounding {
CarryUp
Nearest
}pub(all) enum Standard {
China
China2012
Epa
}pub struct Window {
window : Int
minimum_valid : Int
}fn breakpoints(standard : Standard, pollutant : Pollutant, averaging : Averaging) -> Array[Breakpoint]?Install
Download zipAir quality index in pure MoonBit: the Chinese HJ 633-2026 and US EPA breakpoint tables, a day of hourly readings averaged into the indicators the standards describe, a period summarised, the dominant pollutant, and the EPA NowCast.