use super::*;
use crate::query::AsApiStr;
use crate::{
Aggregation, DailyVar, Error, HourlyVar, Minutely15Var, PressureLevel, SoilMoistureDepth,
SoilTemperatureDepth, TowerLevel,
};
use time::Time;
use time::macros::datetime;
#[test]
fn decodes_hourly_forecast_json() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"elevation": 38.0,
"generationtime_ms": 0.247,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"timezone_abbreviation": "CET",
"hourly_units": {
"time": "iso8601",
"temperature_2m": "degC",
"temperature_80m": "degC",
"wind_speed_10m": "km/h",
"wind_speed_80m": "km/h",
"wind_direction_80m": "degree",
"pressure_msl": "hPa",
"surface_pressure": "hPa",
"precipitation_probability": "%",
"showers": "mm",
"snow_depth": "m",
"cloud_cover_low": "%",
"cloud_cover_mid": "%",
"cloud_cover_high": "%",
"visibility": "m",
"evapotranspiration": "mm",
"et0_fao_evapotranspiration": "mm",
"vapour_pressure_deficit": "kPa",
"is_day": "",
"sunshine_duration": "s",
"uv_index": "",
"uv_index_clear_sky": "",
"wet_bulb_temperature_2m": "degC",
"total_column_integrated_water_vapour": "kg/m2",
"shortwave_radiation": "W/m2",
"direct_radiation": "W/m2",
"direct_normal_irradiance": "W/m2",
"diffuse_radiation": "W/m2",
"global_tilted_irradiance": "W/m2",
"terrestrial_radiation": "W/m2",
"shortwave_radiation_instant": "W/m2",
"direct_radiation_instant": "W/m2",
"direct_normal_irradiance_instant": "W/m2",
"diffuse_radiation_instant": "W/m2",
"global_tilted_irradiance_instant": "W/m2",
"terrestrial_radiation_instant": "W/m2",
"cape": "J/kg",
"lifted_index": "K",
"convective_inhibition": "J/kg",
"freezing_level_height": "m",
"boundary_layer_height": "m",
"temperature_850hPa": "degC",
"relative_humidity_850hPa": "%",
"dew_point_850hPa": "degC",
"cloud_cover_850hPa": "%",
"wind_speed_850hPa": "km/h",
"wind_direction_850hPa": "degree",
"geopotential_height_850hPa": "m"
},
"hourly": {
"time": ["2026-04-21T00:00", "2026-04-21T01:00"],
"temperature_2m": [3.4, 3.6],
"temperature_80m": [2.9, 3.1],
"wind_speed_10m": [8.0, 9.5],
"wind_speed_80m": [14.0, 15.5],
"wind_direction_80m": [240, 245],
"pressure_msl": [1012.3, 1011.8],
"surface_pressure": [1008.1, 1007.6],
"precipitation_probability": [30, 40],
"showers": [0.1, 0.2],
"snow_depth": [0.0, 0.0],
"cloud_cover_low": [25, 30],
"cloud_cover_mid": [10, 12],
"cloud_cover_high": [50, 60],
"visibility": [24000, 22000],
"evapotranspiration": [0.1, 0.2],
"et0_fao_evapotranspiration": [0.0, 0.1],
"vapour_pressure_deficit": [0.4, 0.5],
"is_day": [0, 1],
"sunshine_duration": [0, 1800],
"uv_index": [0.0, 1.2],
"uv_index_clear_sky": [0.0, 1.5],
"wet_bulb_temperature_2m": [1.8, 2.0],
"total_column_integrated_water_vapour": [12.0, 12.5],
"shortwave_radiation": [10.0, 20.0],
"direct_radiation": [5.0, 10.0],
"direct_normal_irradiance": [7.0, 12.0],
"diffuse_radiation": [3.0, 5.0],
"global_tilted_irradiance": [11.0, 21.0],
"terrestrial_radiation": [0.0, 1.0],
"shortwave_radiation_instant": [12.0, 24.0],
"direct_radiation_instant": [6.0, 12.0],
"direct_normal_irradiance_instant": [8.0, 14.0],
"diffuse_radiation_instant": [4.0, 6.0],
"global_tilted_irradiance_instant": [13.0, 25.0],
"terrestrial_radiation_instant": [0.0, 2.0],
"cape": [100.0, 200.0],
"lifted_index": [2.0, 1.5],
"convective_inhibition": [0.0, 10.0],
"freezing_level_height": [1400, 1500],
"boundary_layer_height": [900, 1000],
"temperature_850hPa": [-2.1, -1.8],
"relative_humidity_850hPa": [70, 72],
"dew_point_850hPa": [-4.0, -3.5],
"cloud_cover_850hPa": [30, 35],
"wind_speed_850hPa": [20, 21],
"wind_direction_850hPa": [260, 265],
"geopotential_height_850hPa": [1450, 1460]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let hourly = response.hourly.unwrap();
assert_eq!(hourly.time.len(), 2);
assert_eq!(
hourly.temperature_2m().unwrap().values_f32().unwrap(),
&[Some(3.4), Some(3.6)]
);
assert_eq!(
hourly
.wind_speed_at(TowerLevel::M10)
.unwrap()
.values_f32()
.unwrap(),
&[Some(8.0), Some(9.5)]
);
assert_eq!(
hourly
.temperature_at_tower(TowerLevel::M80)
.unwrap()
.values_f32()
.unwrap(),
&[Some(2.9), Some(3.1)]
);
assert_eq!(
hourly
.wind_speed_at(TowerLevel::M80)
.unwrap()
.values_f32()
.unwrap(),
&[Some(14.0), Some(15.5)]
);
assert_eq!(
hourly
.wind_direction_at(TowerLevel::M80)
.unwrap()
.values_f32()
.unwrap(),
&[Some(240.0), Some(245.0)]
);
assert_eq!(
hourly
.get_var(HourlyVar::TemperatureAtTower(TowerLevel::M80))
.unwrap()
.values_f32()
.unwrap(),
&[Some(2.9), Some(3.1)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::PressureMsl),
&[Some(1012.3), Some(1011.8)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::SurfacePressure),
&[Some(1008.1), Some(1007.6)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::PrecipitationProbability),
&[Some(30.0), Some(40.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::Showers),
&[Some(0.1), Some(0.2)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::SnowDepth),
&[Some(0.0), Some(0.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::CloudCoverLow),
&[Some(25.0), Some(30.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::CloudCoverMid),
&[Some(10.0), Some(12.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::CloudCoverHigh),
&[Some(50.0), Some(60.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::Visibility),
&[Some(24000.0), Some(22000.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::Evapotranspiration),
&[Some(0.1), Some(0.2)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::Et0FaoEvapotranspiration),
&[Some(0.0), Some(0.1)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::VapourPressureDeficit),
&[Some(0.4), Some(0.5)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::IsDay),
&[Some(0.0), Some(1.0)]
);
assert_eq!(hourly.is_day_at(0), Some(false));
assert_eq!(hourly.is_day_at(1), Some(true));
assert_eq!(
hourly_values(&hourly, HourlyVar::SunshineDuration),
&[Some(0.0), Some(1800.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::UvIndex),
&[Some(0.0), Some(1.2)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::UvIndexClearSky),
&[Some(0.0), Some(1.5)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::WetBulbTemperature2m),
&[Some(1.8), Some(2.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::TotalColumnIntegratedWaterVapour),
&[Some(12.0), Some(12.5)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::ShortwaveRadiation),
&[Some(10.0), Some(20.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::DirectRadiation),
&[Some(5.0), Some(10.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::DirectNormalIrradiance),
&[Some(7.0), Some(12.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::DiffuseRadiation),
&[Some(3.0), Some(5.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::GlobalTiltedIrradiance),
&[Some(11.0), Some(21.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::TerrestrialRadiation),
&[Some(0.0), Some(1.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::ShortwaveRadiationInstant),
&[Some(12.0), Some(24.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::DirectRadiationInstant),
&[Some(6.0), Some(12.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::DirectNormalIrradianceInstant),
&[Some(8.0), Some(14.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::DiffuseRadiationInstant),
&[Some(4.0), Some(6.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::GlobalTiltedIrradianceInstant),
&[Some(13.0), Some(25.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::TerrestrialRadiationInstant),
&[Some(0.0), Some(2.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::Cape),
&[Some(100.0), Some(200.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::LiftedIndex),
&[Some(2.0), Some(1.5)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::ConvectiveInhibition),
&[Some(0.0), Some(10.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::FreezingLevelHeight),
&[Some(1400.0), Some(1500.0)]
);
assert_eq!(
hourly_values(&hourly, HourlyVar::BoundaryLayerHeight),
&[Some(900.0), Some(1000.0)]
);
assert_eq!(
hourly
.temperature_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(-2.1), Some(-1.8)]
);
assert_eq!(
hourly
.relative_humidity_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(70.0), Some(72.0)]
);
assert_eq!(
hourly
.dew_point_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(-4.0), Some(-3.5)]
);
assert_eq!(
hourly
.cloud_cover_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(30.0), Some(35.0)]
);
assert_eq!(
hourly
.wind_speed_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(20.0), Some(21.0)]
);
assert_eq!(
hourly
.wind_direction_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(260.0), Some(265.0)]
);
assert_eq!(
hourly
.geopotential_height_at_pressure(crate::PressureLevel::Hpa850)
.unwrap()
.values_f32()
.unwrap(),
&[Some(1450.0), Some(1460.0)]
);
}
#[test]
fn decodes_multi_location_forecast_json_array() {
let json = br#"[
{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"hourly": {
"time": ["2026-04-21T00:00"],
"temperature_2m": [3.4]
}
},
{
"latitude": 47.3769,
"longitude": 8.5417,
"utc_offset_seconds": 3600,
"hourly": {
"time": ["2026-04-21T00:00"],
"temperature_2m": [7.1]
}
}
]"#;
let responses = decode_forecast_json_many(json).unwrap();
assert_eq!(responses.len(), 2);
assert_eq!(responses[0].latitude, 52.52);
assert_eq!(responses[1].latitude, 47.3769);
assert_eq!(
responses[1]
.hourly
.as_ref()
.unwrap()
.temperature_2m()
.unwrap()
.values_f32()
.unwrap(),
&[Some(7.1)]
);
}
#[test]
fn typed_lookup_handles_model_suffixed_columns() {
let json = br#"{
"latitude": 47.3769,
"longitude": 8.5417,
"utc_offset_seconds": 3600,
"hourly": {
"time": ["2026-04-21T00:00"],
"temperature_2m_icon_seamless_eps": [7.1],
"temperature_2m_ncep_gefs025": [6.8]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let hourly = response.hourly.unwrap();
assert!(hourly.get_var(HourlyVar::Temperature2m).is_none());
assert_eq!(
hourly
.get_var_for_model(HourlyVar::Temperature2m, "icon_seamless_eps")
.unwrap()
.values_f32()
.unwrap(),
&[Some(7.1)]
);
assert_eq!(
hourly
.get("temperature_2m_ncep_gefs025")
.unwrap()
.descriptor
.model
.as_deref(),
Some("ncep_gefs025")
);
}
#[test]
fn decoded_response_can_be_serialized() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"hourly": {
"time": ["2026-04-21T00:00"],
"temperature_2m": [3.4]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let serialized = serde_json::to_string(&response).unwrap();
assert!(serialized.contains("\"latitude\":52.52"));
assert!(serialized.contains("\"temperature_2m\""));
}
#[test]
fn decodes_current_pressure_accessors() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 0,
"current": {
"time": "2026-04-21T00:00",
"pressure_msl": 1012.3,
"surface_pressure": 1008.1,
"showers": 0.2,
"is_day": 1
}
}"#;
let response = decode_forecast_json(json).unwrap();
let current = response.current.unwrap();
assert_eq!(
current_values(¤t, crate::CurrentVar::PressureMsl),
&[Some(1012.3)]
);
assert_eq!(
current
.get_var(crate::CurrentVar::PressureMsl)
.unwrap()
.values_f32()
.unwrap(),
&[Some(1012.3)]
);
assert_eq!(
current_values(¤t, crate::CurrentVar::SurfacePressure),
&[Some(1008.1)]
);
assert_eq!(
current_values(¤t, crate::CurrentVar::Showers),
&[Some(0.2)]
);
assert_eq!(
current_values(¤t, crate::CurrentVar::IsDay),
&[Some(1.0)]
);
assert_eq!(current.is_day(), Some(true));
}
#[test]
fn decodes_nullable_numeric_columns() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 0,
"hourly": {
"time": ["2026-04-21T00:00", "2026-04-21T01:00", "2026-04-21T02:00"],
"temperature_2m": [3.4, null, 3.8]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let hourly = response.hourly.unwrap();
assert_eq!(
hourly.temperature_2m().unwrap().values_f32().unwrap(),
&[Some(3.4), None, Some(3.8)]
);
let rows = hourly
.iter_rows()
.map(|row| row.temperature_2m())
.collect::<Vec<_>>();
assert_eq!(rows, vec![Some(3.4), None, Some(3.8)]);
}
#[test]
fn decodes_nullable_string_columns() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 0,
"daily": {
"time": ["2026-04-21", "2026-04-22"],
"sunrise": ["2026-04-21T06:00", null]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let daily = response.daily.unwrap();
assert_eq!(
daily.variables[0].values_str().unwrap(),
&[Some("2026-04-21T06:00".to_owned()), None]
);
}
#[test]
fn decodes_daily_forecast_json() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"timezone_abbreviation": "CET",
"daily_units": {
"time": "iso8601",
"temperature_2m_max": "degC",
"sunrise": "iso8601",
"sunset": "iso8601",
"daylight_duration": "s",
"showers_sum": "mm",
"precipitation_probability_mean": "%",
"pressure_msl_mean": "hPa",
"et0_fao_evapotranspiration": "mm",
"wet_bulb_temperature_2m_mean": "degC"
},
"daily": {
"time": ["2026-04-21"],
"temperature_2m_max": [14.5],
"sunrise": ["2026-04-21T05:58"],
"sunset": ["2026-04-21T20:12"],
"daylight_duration": [51240],
"showers_sum": [0.4],
"precipitation_probability_mean": [35],
"pressure_msl_mean": [1012.0],
"et0_fao_evapotranspiration": [1.7],
"wet_bulb_temperature_2m_mean": [7.5]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let daily = response.daily.unwrap();
assert_eq!(daily.time.len(), 1);
assert_eq!(
daily.temperature_2m_max().unwrap().values_f32().unwrap(),
&[Some(14.5)]
);
assert_eq!(
daily
.get_var(DailyVar::Temperature2mMax)
.unwrap()
.values_f32()
.unwrap(),
&[Some(14.5)]
);
assert_eq!(
daily
.get_var(DailyVar::Sunrise)
.unwrap()
.values_str()
.unwrap(),
&[Some("2026-04-21T05:58".to_owned())]
);
assert_eq!(
daily
.get_var(DailyVar::Sunset)
.unwrap()
.values_str()
.unwrap(),
&[Some("2026-04-21T20:12".to_owned())]
);
assert_eq!(
daily
.get_var(DailyVar::DaylightDuration)
.unwrap()
.values_f32()
.unwrap(),
&[Some(51240.0)]
);
assert_eq!(
daily
.get_var(DailyVar::ShowersSum)
.unwrap()
.values_f32()
.unwrap(),
&[Some(0.4)]
);
assert_eq!(
daily
.get("precipitation_probability_mean")
.unwrap()
.values_f32()
.unwrap(),
&[Some(35.0)]
);
assert_eq!(
daily
.get_var(DailyVar::PressureMslMean)
.unwrap()
.values_f32()
.unwrap(),
&[Some(1012.0)]
);
assert_eq!(
daily
.get_var(DailyVar::Et0FaoEvapotranspiration)
.unwrap()
.values_f32()
.unwrap(),
&[Some(1.7)]
);
assert_eq!(
daily
.get_var(DailyVar::WetBulbTemperature2mMean)
.unwrap()
.values_f32()
.unwrap(),
&[Some(7.5)]
);
}
#[test]
fn decodes_monthly_forecast_json() {
let json = br#"{
"latitude": 47.35363,
"longitude": 8.653846,
"utc_offset_seconds": 7200,
"timezone": "Europe/Zurich",
"monthly_units": {
"time": "iso8601",
"temperature_2m_mean": "degC"
},
"monthly": {
"time": ["2026-05-01", "2026-06-01"],
"temperature_2m_mean": [13.6, 17.4]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let monthly = response.monthly.unwrap();
assert_eq!(monthly.time.len(), 2);
assert_eq!(
monthly
.get("temperature_2m_mean")
.unwrap()
.values_f32()
.unwrap(),
&[Some(13.6), Some(17.4)]
);
}
#[test]
fn decodes_unix_daily_times_as_local_midnight() {
let unix = datetime!(2026-04-20 23:00 UTC).unix_timestamp();
let json = format!(
r#"{{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"daily": {{
"time": [{unix}],
"temperature_2m_max": [14.5]
}}
}}"#
);
let response = decode_forecast_json(json.as_bytes()).unwrap();
let daily_time = response.daily.unwrap().time[0];
assert_eq!(daily_time.date().to_string(), "2026-04-21");
assert_eq!(daily_time.time(), Time::MIDNIGHT);
assert_eq!(daily_time.offset().whole_seconds(), 3600);
}
#[test]
fn decodes_unix_hourly_times_as_offset_instants() {
let unix = datetime!(2026-04-20 23:00 UTC).unix_timestamp();
let json = format!(
r#"{{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"hourly": {{
"time": [{unix}],
"temperature_2m": [3.4]
}}
}}"#
);
let response = decode_forecast_json(json.as_bytes()).unwrap();
let hourly_time = response.hourly.unwrap().time[0];
assert_eq!(hourly_time.date().to_string(), "2026-04-21");
assert_eq!(hourly_time.time(), Time::MIDNIGHT);
assert_eq!(hourly_time.offset().whole_seconds(), 3600);
}
#[test]
fn decodes_minutely_15_forecast_json() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"timezone_abbreviation": "CET",
"minutely_15_units": {
"time": "iso8601",
"temperature_2m": "degC",
"lightning_potential": "J/kg",
"shortwave_radiation_instant": "W/m2"
},
"minutely_15": {
"time": ["2026-04-21T00:00", "2026-04-21T00:15"],
"temperature_2m": [3.4, 3.5],
"lightning_potential": [0.0, 1.0],
"shortwave_radiation_instant": [0.0, 2.0]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let minutely = response.minutely_15.unwrap();
assert_eq!(minutely.time.len(), 2);
assert_eq!(
minutely
.get_var(Minutely15Var::Temperature2m)
.unwrap()
.values_f32()
.unwrap(),
&[Some(3.4), Some(3.5)]
);
assert_eq!(
minutely
.get_var(Minutely15Var::LightningPotential)
.unwrap()
.values_f32()
.unwrap(),
&[Some(0.0), Some(1.0)]
);
assert_eq!(
minutely
.get("shortwave_radiation_instant")
.unwrap()
.values_f32()
.unwrap(),
&[Some(0.0), Some(2.0)]
);
}
#[test]
fn decodes_bare_nan_and_null_numeric_values() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 3600,
"hourly_units": {
"time": "iso8601",
"temperature_2m": "degC"
},
"hourly": {
"time": ["2026-04-21T00:00", "2026-04-21T01:00", "2026-04-21T02:00"],
"temperature_2m": [3.4, null, nan]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let hourly = response.hourly.unwrap();
assert_eq!(
hourly.temperature_2m().unwrap().values_f32().unwrap(),
&[Some(3.4), None, None]
);
}
#[test]
fn decodes_nullable_current_value() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 0,
"current": {
"time": "2026-04-21T00:00",
"temperature_2m": null
}
}"#;
let response = decode_forecast_json(json).unwrap();
let current = response.current.unwrap();
assert_eq!(
current.temperature_2m().unwrap().values_f32().unwrap(),
&[None]
);
}
#[test]
fn rejects_missing_utc_offset_seconds() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"hourly": {
"time": ["2026-04-21T00:00"],
"temperature_2m": [3.4]
}
}"#;
let err = decode_forecast_json(json).unwrap_err();
assert!(matches!(err, Error::InvalidResponse { .. }));
}
#[test]
fn rejects_misaligned_time_series_lengths() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 0,
"hourly": {
"time": ["2026-04-21T00:00", "2026-04-21T01:00"],
"temperature_2m": [3.4]
}
}"#;
let err = decode_forecast_json(json).unwrap_err();
assert!(matches!(err, Error::InvalidResponse { .. }));
}
#[test]
fn row_iterator_reads_common_columns() {
let json = br#"{
"latitude": 52.52,
"longitude": 13.419998,
"utc_offset_seconds": 0,
"hourly": {
"time": ["2026-04-21T00:00"],
"temperature_2m": [3.4],
"precipitation": [0.2],
"is_day": [1]
}
}"#;
let response = decode_forecast_json(json).unwrap();
let hourly = response.hourly.unwrap();
let row = hourly.iter_rows().next().unwrap();
assert_eq!(row.temperature_2m(), Some(3.4));
assert_eq!(row.precipitation(), Some(0.2));
assert_eq!(row.is_day(), Some(true));
assert_eq!(row.get_f32(HourlyVar::Temperature2m), Some(3.4));
assert_eq!(row.get_str(HourlyVar::WeatherCode), None);
}
#[test]
fn hourly_request_tokens_round_trip_to_descriptors() {
assert_descriptor(
HourlyVar::Temperature2m,
Variable::Temperature,
Some(2),
None,
None,
None,
);
assert_descriptor(
HourlyVar::TemperatureAtTower(TowerLevel::M80),
Variable::Temperature,
Some(80),
None,
None,
None,
);
assert_descriptor(
HourlyVar::TemperatureAtTower(TowerLevel::M120),
Variable::Temperature,
Some(120),
None,
None,
None,
);
assert_descriptor(
HourlyVar::TemperatureAtTower(TowerLevel::M180),
Variable::Temperature,
Some(180),
None,
None,
None,
);
assert_descriptor(
HourlyVar::RelativeHumidity2m,
Variable::RelativeHumidity,
Some(2),
None,
None,
None,
);
assert_descriptor(
HourlyVar::DewPoint2m,
Variable::DewPoint,
Some(2),
None,
None,
None,
);
assert_descriptor(
HourlyVar::ApparentTemperature,
Variable::ApparentTemperature,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::WetBulbTemperature2m,
Variable::WetBulbTemperature,
Some(2),
None,
None,
None,
);
assert_descriptor(
HourlyVar::PressureMsl,
Variable::PressureMsl,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::SurfacePressure,
Variable::SurfacePressure,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::CloudCover,
Variable::CloudCover,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::CloudCoverLow,
Variable::CloudCoverLow,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::CloudCoverMid,
Variable::CloudCoverMid,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::CloudCoverHigh,
Variable::CloudCoverHigh,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::PrecipitationProbability,
Variable::PrecipitationProbability,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::Precipitation,
Variable::Precipitation,
None,
None,
None,
None,
);
assert_descriptor(HourlyVar::Rain, Variable::Rain, None, None, None, None);
assert_descriptor(
HourlyVar::Showers,
Variable::Showers,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::Snowfall,
Variable::Snowfall,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::SnowDepth,
Variable::SnowDepth,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::WeatherCode,
Variable::WeatherCode,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::Visibility,
Variable::Visibility,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::Evapotranspiration,
Variable::Evapotranspiration,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::Et0FaoEvapotranspiration,
Variable::Et0FaoEvapotranspiration,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::VapourPressureDeficit,
Variable::VapourPressureDeficit,
None,
None,
None,
None,
);
assert_descriptor(HourlyVar::IsDay, Variable::IsDay, None, None, None, None);
assert_descriptor(
HourlyVar::SunshineDuration,
Variable::SunshineDuration,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::UvIndex,
Variable::UvIndex,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::UvIndexClearSky,
Variable::UvIndexClearSky,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::TotalColumnIntegratedWaterVapour,
Variable::TotalColumnIntegratedWaterVapour,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::ShortwaveRadiation,
Variable::ShortwaveRadiation,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::DirectRadiation,
Variable::DirectRadiation,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::DirectNormalIrradiance,
Variable::DirectNormalIrradiance,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::DiffuseRadiation,
Variable::DiffuseRadiation,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::GlobalTiltedIrradiance,
Variable::GlobalTiltedIrradiance,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::TerrestrialRadiation,
Variable::TerrestrialRadiation,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::ShortwaveRadiationInstant,
Variable::ShortwaveRadiationInstant,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::DirectRadiationInstant,
Variable::DirectRadiationInstant,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::DirectNormalIrradianceInstant,
Variable::DirectNormalIrradianceInstant,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::DiffuseRadiationInstant,
Variable::DiffuseRadiationInstant,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::GlobalTiltedIrradianceInstant,
Variable::GlobalTiltedIrradianceInstant,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::TerrestrialRadiationInstant,
Variable::TerrestrialRadiationInstant,
None,
None,
None,
None,
);
assert_descriptor(HourlyVar::Cape, Variable::Cape, None, None, None, None);
assert_descriptor(
HourlyVar::LiftedIndex,
Variable::LiftedIndex,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::ConvectiveInhibition,
Variable::ConvectiveInhibition,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::FreezingLevelHeight,
Variable::FreezingLevelHeight,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::BoundaryLayerHeight,
Variable::BoundaryLayerHeight,
None,
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindSpeedAtTower(TowerLevel::M10),
Variable::WindSpeed,
Some(10),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindSpeedAtTower(TowerLevel::M80),
Variable::WindSpeed,
Some(80),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindSpeedAtTower(TowerLevel::M120),
Variable::WindSpeed,
Some(120),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindSpeedAtTower(TowerLevel::M180),
Variable::WindSpeed,
Some(180),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindDirectionAtTower(TowerLevel::M10),
Variable::WindDirection,
Some(10),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindDirectionAtTower(TowerLevel::M80),
Variable::WindDirection,
Some(80),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindDirectionAtTower(TowerLevel::M120),
Variable::WindDirection,
Some(120),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindDirectionAtTower(TowerLevel::M180),
Variable::WindDirection,
Some(180),
None,
None,
None,
);
assert_descriptor(
HourlyVar::WindGusts10m,
Variable::WindGust,
Some(10),
None,
None,
None,
);
assert_descriptor(
HourlyVar::SoilTemperature(SoilTemperatureDepth::Cm18),
Variable::SoilTemperature,
None,
None,
Some(18),
None,
);
assert_descriptor(
HourlyVar::SoilMoisture {
from: SoilMoistureDepth::Cm9,
to: SoilMoistureDepth::Cm27,
},
Variable::SoilMoisture,
None,
None,
Some(9),
Some(27),
);
for level in [
PressureLevel::Hpa1000,
PressureLevel::Hpa975,
PressureLevel::Hpa950,
PressureLevel::Hpa925,
PressureLevel::Hpa900,
PressureLevel::Hpa850,
PressureLevel::Hpa800,
PressureLevel::Hpa700,
PressureLevel::Hpa600,
PressureLevel::Hpa500,
PressureLevel::Hpa400,
PressureLevel::Hpa300,
PressureLevel::Hpa250,
PressureLevel::Hpa200,
PressureLevel::Hpa150,
PressureLevel::Hpa100,
PressureLevel::Hpa70,
PressureLevel::Hpa50,
PressureLevel::Hpa30,
] {
assert_descriptor(
HourlyVar::TemperatureAtPressure(level),
Variable::Temperature,
None,
Some(level as i16),
None,
None,
);
assert_descriptor(
HourlyVar::RelativeHumidityAtPressure(level),
Variable::RelativeHumidity,
None,
Some(level as i16),
None,
None,
);
assert_descriptor(
HourlyVar::DewPointAtPressure(level),
Variable::DewPoint,
None,
Some(level as i16),
None,
None,
);
assert_descriptor(
HourlyVar::CloudCoverAtPressure(level),
Variable::CloudCover,
None,
Some(level as i16),
None,
None,
);
assert_descriptor(
HourlyVar::WindSpeedAtPressure(level),
Variable::WindSpeed,
None,
Some(level as i16),
None,
None,
);
assert_descriptor(
HourlyVar::WindDirectionAtPressure(level),
Variable::WindDirection,
None,
Some(level as i16),
None,
None,
);
assert_descriptor(
HourlyVar::GeopotentialHeightAtPressure(level),
Variable::GeopotentialHeight,
None,
Some(level as i16),
None,
None,
);
}
let previous_day = HourlyVar::other("temperature_2m_previous_day1");
let api_name = previous_day.as_api_str();
let descriptor = VariableDescriptor::from_api_name(&api_name);
assert_eq!(descriptor.variable, Variable::Temperature);
assert_eq!(descriptor.altitude, Some(2));
assert_eq!(descriptor.previous_day, Some(1));
assert_eq!(descriptor.ensemble_member, None);
let member = HourlyVar::other("temperature_2m_member39");
let api_name = member.as_api_str();
let descriptor = VariableDescriptor::from_api_name(&api_name);
assert_eq!(descriptor.variable, Variable::Temperature);
assert_eq!(descriptor.altitude, Some(2));
assert_eq!(descriptor.previous_day, None);
assert_eq!(descriptor.ensemble_member, Some(39));
let descriptor = VariableDescriptor::from_api_name("river_discharge_mean");
assert_eq!(descriptor.variable, Variable::RiverDischarge);
assert_eq!(descriptor.aggregation, Some(Aggregation::Mean));
let descriptor = VariableDescriptor::from_api_name("temperature_2m_max_member01_ecmwf_ec46");
assert_eq!(descriptor.variable, Variable::Temperature);
assert_eq!(descriptor.altitude, Some(2));
assert_eq!(descriptor.aggregation, Some(Aggregation::Maximum));
assert_eq!(descriptor.ensemble_member, Some(1));
}
#[test]
fn daily_request_tokens_round_trip_to_descriptors() {
for (var, variable, altitude, aggregation) in [
(DailyVar::WeatherCode, Variable::WeatherCode, None, None),
(
DailyVar::Temperature2mMax,
Variable::Temperature,
Some(2),
Some(Aggregation::Maximum),
),
(
DailyVar::Temperature2mMean,
Variable::Temperature,
Some(2),
Some(Aggregation::Mean),
),
(
DailyVar::Temperature2mMin,
Variable::Temperature,
Some(2),
Some(Aggregation::Minimum),
),
(
DailyVar::ApparentTemperatureMax,
Variable::ApparentTemperature,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::ApparentTemperatureMean,
Variable::ApparentTemperature,
None,
Some(Aggregation::Mean),
),
(
DailyVar::ApparentTemperatureMin,
Variable::ApparentTemperature,
None,
Some(Aggregation::Minimum),
),
(DailyVar::Sunrise, Variable::SunEvent, None, None),
(DailyVar::Sunset, Variable::SunEvent, None, None),
(
DailyVar::DaylightDuration,
Variable::DaylightDuration,
None,
None,
),
(
DailyVar::SunshineDuration,
Variable::SunshineDuration,
None,
None,
),
(
DailyVar::UvIndexMax,
Variable::UvIndex,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::UvIndexClearSkyMax,
Variable::UvIndexClearSky,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::PrecipitationSum,
Variable::Precipitation,
None,
Some(Aggregation::Sum),
),
(
DailyVar::RainSum,
Variable::Rain,
None,
Some(Aggregation::Sum),
),
(
DailyVar::ShowersSum,
Variable::Showers,
None,
Some(Aggregation::Sum),
),
(
DailyVar::SnowfallSum,
Variable::Snowfall,
None,
Some(Aggregation::Sum),
),
(
DailyVar::PrecipitationHours,
Variable::PrecipitationHours,
None,
None,
),
(
DailyVar::PrecipitationProbabilityMax,
Variable::PrecipitationProbability,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::PrecipitationProbabilityMean,
Variable::PrecipitationProbability,
None,
Some(Aggregation::Mean),
),
(
DailyVar::PrecipitationProbabilityMin,
Variable::PrecipitationProbability,
None,
Some(Aggregation::Minimum),
),
(
DailyVar::WindSpeed10mMax,
Variable::WindSpeed,
Some(10),
Some(Aggregation::Maximum),
),
(
DailyVar::WindSpeed10mMean,
Variable::WindSpeed,
Some(10),
Some(Aggregation::Mean),
),
(
DailyVar::WindSpeed10mMin,
Variable::WindSpeed,
Some(10),
Some(Aggregation::Minimum),
),
(
DailyVar::WindGusts10mMax,
Variable::WindGust,
Some(10),
Some(Aggregation::Maximum),
),
(
DailyVar::WindGusts10mMean,
Variable::WindGust,
Some(10),
Some(Aggregation::Mean),
),
(
DailyVar::WindGusts10mMin,
Variable::WindGust,
Some(10),
Some(Aggregation::Minimum),
),
(
DailyVar::WindDirection10mDominant,
Variable::WindDirection,
Some(10),
Some(Aggregation::Dominant),
),
(
DailyVar::ShortwaveRadiationSum,
Variable::ShortwaveRadiation,
None,
Some(Aggregation::Sum),
),
(
DailyVar::Et0FaoEvapotranspiration,
Variable::Et0FaoEvapotranspiration,
None,
None,
),
(
DailyVar::CapeMean,
Variable::Cape,
None,
Some(Aggregation::Mean),
),
(
DailyVar::CapeMax,
Variable::Cape,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::CapeMin,
Variable::Cape,
None,
Some(Aggregation::Minimum),
),
(
DailyVar::CloudCoverMean,
Variable::CloudCover,
None,
Some(Aggregation::Mean),
),
(
DailyVar::CloudCoverMax,
Variable::CloudCover,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::CloudCoverMin,
Variable::CloudCover,
None,
Some(Aggregation::Minimum),
),
(
DailyVar::DewPoint2mMean,
Variable::DewPoint,
Some(2),
Some(Aggregation::Mean),
),
(
DailyVar::DewPoint2mMax,
Variable::DewPoint,
Some(2),
Some(Aggregation::Maximum),
),
(
DailyVar::DewPoint2mMin,
Variable::DewPoint,
Some(2),
Some(Aggregation::Minimum),
),
(
DailyVar::GrowingDegreeDaysBase0Limit50,
Variable::GrowingDegreeDays,
None,
None,
),
(
DailyVar::LeafWetnessProbabilityMean,
Variable::LeafWetnessProbability,
None,
Some(Aggregation::Mean),
),
(
DailyVar::RelativeHumidity2mMean,
Variable::RelativeHumidity,
Some(2),
Some(Aggregation::Mean),
),
(
DailyVar::RelativeHumidity2mMax,
Variable::RelativeHumidity,
Some(2),
Some(Aggregation::Maximum),
),
(
DailyVar::RelativeHumidity2mMin,
Variable::RelativeHumidity,
Some(2),
Some(Aggregation::Minimum),
),
(
DailyVar::SnowfallWaterEquivalentSum,
Variable::SnowfallWaterEquivalent,
None,
Some(Aggregation::Sum),
),
(
DailyVar::PressureMslMean,
Variable::PressureMsl,
None,
Some(Aggregation::Mean),
),
(
DailyVar::PressureMslMax,
Variable::PressureMsl,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::PressureMslMin,
Variable::PressureMsl,
None,
Some(Aggregation::Minimum),
),
(
DailyVar::SurfacePressureMean,
Variable::SurfacePressure,
None,
Some(Aggregation::Mean),
),
(
DailyVar::SurfacePressureMax,
Variable::SurfacePressure,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::SurfacePressureMin,
Variable::SurfacePressure,
None,
Some(Aggregation::Minimum),
),
(
DailyVar::UpdraftMax,
Variable::Updraft,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::VisibilityMean,
Variable::Visibility,
None,
Some(Aggregation::Mean),
),
(
DailyVar::VisibilityMin,
Variable::Visibility,
None,
Some(Aggregation::Minimum),
),
(
DailyVar::VisibilityMax,
Variable::Visibility,
None,
Some(Aggregation::Maximum),
),
(
DailyVar::WetBulbTemperature2mMean,
Variable::WetBulbTemperature,
Some(2),
Some(Aggregation::Mean),
),
(
DailyVar::WetBulbTemperature2mMax,
Variable::WetBulbTemperature,
Some(2),
Some(Aggregation::Maximum),
),
(
DailyVar::WetBulbTemperature2mMin,
Variable::WetBulbTemperature,
Some(2),
Some(Aggregation::Minimum),
),
(
DailyVar::VapourPressureDeficitMax,
Variable::VapourPressureDeficit,
None,
Some(Aggregation::Maximum),
),
] {
assert_descriptor_with_aggregation(var, variable, altitude, None, None, None, aggregation);
}
}
#[test]
fn minutely_15_request_tokens_round_trip_to_descriptors() {
for (var, variable, altitude) in [
(Minutely15Var::Temperature2m, Variable::Temperature, Some(2)),
(
Minutely15Var::RelativeHumidity2m,
Variable::RelativeHumidity,
Some(2),
),
(Minutely15Var::DewPoint2m, Variable::DewPoint, Some(2)),
(
Minutely15Var::ApparentTemperature,
Variable::ApparentTemperature,
None,
),
(Minutely15Var::Precipitation, Variable::Precipitation, None),
(Minutely15Var::Rain, Variable::Rain, None),
(Minutely15Var::Showers, Variable::Showers, None),
(Minutely15Var::Snowfall, Variable::Snowfall, None),
(
Minutely15Var::SnowfallHeight,
Variable::SnowfallHeight,
None,
),
(
Minutely15Var::FreezingLevelHeight,
Variable::FreezingLevelHeight,
None,
),
(
Minutely15Var::SunshineDuration,
Variable::SunshineDuration,
None,
),
(Minutely15Var::WeatherCode, Variable::WeatherCode, None),
(
Minutely15Var::WindSpeedAtTower(TowerLevel::M10),
Variable::WindSpeed,
Some(10),
),
(
Minutely15Var::WindSpeedAtTower(TowerLevel::M80),
Variable::WindSpeed,
Some(80),
),
(
Minutely15Var::WindDirectionAtTower(TowerLevel::M10),
Variable::WindDirection,
Some(10),
),
(
Minutely15Var::WindDirectionAtTower(TowerLevel::M80),
Variable::WindDirection,
Some(80),
),
(Minutely15Var::WindGusts10m, Variable::WindGust, Some(10)),
(Minutely15Var::Visibility, Variable::Visibility, None),
(Minutely15Var::Cape, Variable::Cape, None),
(
Minutely15Var::LightningPotential,
Variable::LightningPotential,
None,
),
(Minutely15Var::IsDay, Variable::IsDay, None),
(
Minutely15Var::ShortwaveRadiation,
Variable::ShortwaveRadiation,
None,
),
(
Minutely15Var::DirectRadiation,
Variable::DirectRadiation,
None,
),
(
Minutely15Var::DirectNormalIrradiance,
Variable::DirectNormalIrradiance,
None,
),
(
Minutely15Var::DiffuseRadiation,
Variable::DiffuseRadiation,
None,
),
(
Minutely15Var::GlobalTiltedIrradiance,
Variable::GlobalTiltedIrradiance,
None,
),
(
Minutely15Var::TerrestrialRadiation,
Variable::TerrestrialRadiation,
None,
),
(
Minutely15Var::ShortwaveRadiationInstant,
Variable::ShortwaveRadiationInstant,
None,
),
(
Minutely15Var::DirectRadiationInstant,
Variable::DirectRadiationInstant,
None,
),
(
Minutely15Var::DirectNormalIrradianceInstant,
Variable::DirectNormalIrradianceInstant,
None,
),
(
Minutely15Var::DiffuseRadiationInstant,
Variable::DiffuseRadiationInstant,
None,
),
(
Minutely15Var::GlobalTiltedIrradianceInstant,
Variable::GlobalTiltedIrradianceInstant,
None,
),
(
Minutely15Var::TerrestrialRadiationInstant,
Variable::TerrestrialRadiationInstant,
None,
),
] {
assert_descriptor(var, variable, altitude, None, None, None);
}
}
#[test]
fn daily_aggregation_descriptors_are_parsed() {
let max = VariableDescriptor::from_api_name("temperature_2m_max");
assert_eq!(max.variable, Variable::Temperature);
assert_eq!(max.altitude, Some(2));
assert_eq!(max.aggregation, Some(Aggregation::Maximum));
let dominant = VariableDescriptor::from_api_name("wind_direction_10m_dominant");
assert_eq!(dominant.variable, Variable::WindDirection);
assert_eq!(dominant.altitude, Some(10));
assert_eq!(dominant.aggregation, Some(Aggregation::Dominant));
for (api_name, aggregation) in [
("river_discharge_median", Aggregation::Median),
("river_discharge_p10", Aggregation::P10),
("river_discharge_p25", Aggregation::P25),
("river_discharge_p50", Aggregation::Median),
("river_discharge_p75", Aggregation::P75),
("river_discharge_p90", Aggregation::P90),
("river_discharge_mean_seamless_v4", Aggregation::Mean),
("river_discharge_median_forecast_v4", Aggregation::Median),
("river_discharge_p25_consolidated_v3", Aggregation::P25),
("river_discharge_p75_seamless_v3", Aggregation::P75),
] {
let descriptor = VariableDescriptor::from_api_name(api_name);
assert_eq!(descriptor.variable, Variable::RiverDischarge);
assert_eq!(descriptor.aggregation, Some(aggregation));
}
let model_mean =
VariableDescriptor::from_api_name("shortwave_radiation_ncep_hgefs025_ensemble_mean");
assert_eq!(model_mean.variable, Variable::ShortwaveRadiation);
assert_eq!(model_mean.aggregation, None);
let flood_member = VariableDescriptor::from_api_name("river_discharge_member01_seamless_v4");
assert_eq!(flood_member.variable, Variable::RiverDischarge);
assert_eq!(flood_member.aggregation, None);
assert_eq!(flood_member.ensemble_member, Some(1));
}
#[test]
fn descriptor_extracts_unlisted_altitudes_and_pressure_levels() {
let wind = VariableDescriptor::from_api_name("wind_speed_40m");
assert_eq!(wind.variable, Variable::WindSpeed);
assert_eq!(wind.altitude, Some(40));
assert_eq!(wind.pressure_level, None);
let temp = VariableDescriptor::from_api_name("temperature_975hPa");
assert_eq!(temp.variable, Variable::Temperature);
assert_eq!(temp.altitude, None);
assert_eq!(temp.pressure_level, Some(975));
}
fn hourly_values(data: &HourlyData, var: HourlyVar) -> &[Option<f32>] {
data.get_var(var).unwrap().values_f32().unwrap()
}
fn current_values(data: &CurrentData, var: crate::CurrentVar) -> &[Option<f32>] {
data.get_var(var).unwrap().values_f32().unwrap()
}
fn assert_descriptor(
var: impl AsApiStr,
variable: Variable,
altitude: Option<i16>,
pressure_level: Option<i16>,
depth: Option<i16>,
depth_to: Option<i16>,
) {
assert_descriptor_with_aggregation(
var,
variable,
altitude,
pressure_level,
depth,
depth_to,
None,
);
}
fn assert_descriptor_with_aggregation(
var: impl AsApiStr,
variable: Variable,
altitude: Option<i16>,
pressure_level: Option<i16>,
depth: Option<i16>,
depth_to: Option<i16>,
aggregation: Option<Aggregation>,
) {
let api_name = var.as_api_str();
let descriptor = VariableDescriptor::from_api_name(&api_name);
assert_ne!(descriptor.variable, Variable::Unknown, "{api_name}");
assert_eq!(descriptor.variable, variable, "{api_name}");
assert_eq!(descriptor.altitude, altitude, "{api_name}");
assert_eq!(descriptor.pressure_level, pressure_level, "{api_name}");
assert_eq!(descriptor.depth, depth, "{api_name}");
assert_eq!(descriptor.depth_to, depth_to, "{api_name}");
assert_eq!(descriptor.aggregation, aggregation, "{api_name}");
assert_eq!(descriptor.previous_day, None, "{api_name}");
assert_eq!(descriptor.ensemble_member, None, "{api_name}");
}