Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
Paul van Tilburg | b61eb63cdf | |
Paul van Tilburg | e5e006dc00 | |
Paul van Tilburg | a0c4e0da77 | |
Paul van Tilburg | 34b63ec94d | |
Paul van Tilburg | 5a23e83b7f | |
Paul van Tilburg | e2d1a1d9df | |
Paul van Tilburg | 29b79c720d |
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2022-5-07
|
||||
## [0.2.1] - 2002-05-08
|
||||
|
||||
### Added
|
||||
|
||||
* Add tests for the merge functionality of the combined provider (PAQI)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Filter out old item/samples in combined provider (PAQI)
|
||||
|
||||
## [0.2.0] - 2022-05-07
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -18,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
Initial release.
|
||||
|
||||
[Unreleased]: https://git.luon.net/paul/sinoptik/compare/v0.2.0...HEAD
|
||||
[Unreleased]: https://git.luon.net/paul/sinoptik/compare/v0.2.1...HEAD
|
||||
[0.2.1]: https://git.luon.net/paul/sinoptik/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://git.luon.net/paul/sinoptik/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://git.luon.net/paul/sinoptik/commits/tag/v0.1.0
|
||||
|
|
|
@ -2129,7 +2129,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sinoptik"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"assert_float_eq",
|
||||
"assert_matches",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sinoptik"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
authors = [
|
||||
"Admar Schoonen <admar@luon.net",
|
||||
"Paul van Tilburg <paul@luon.net>"
|
||||
|
|
|
@ -148,8 +148,8 @@ pub(crate) async fn forecast(
|
|||
providers::combined::get(position, metric, maps_handle).await
|
||||
{
|
||||
forecast.paqi = Some(paqi);
|
||||
forecast.aqi_max = aqi_max;
|
||||
forecast.pollen_max = pollen_max;
|
||||
forecast.aqi_max = Some(aqi_max);
|
||||
forecast.pollen_max = Some(pollen_max);
|
||||
}
|
||||
}
|
||||
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(position, metric).await,
|
||||
|
|
|
@ -294,7 +294,7 @@ impl MapsRefresh for MapsHandle {
|
|||
/// A Buienradar map sample.
|
||||
///
|
||||
/// This represents a value at a given time.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub(crate) struct Sample {
|
||||
/// The time(stamp) of the forecast.
|
||||
|
@ -308,6 +308,13 @@ pub(crate) struct Sample {
|
|||
pub(crate) score: u8,
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(time: DateTime<Utc>, score: u8) -> Self {
|
||||
Self { time, score }
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a scoring histogram for the map key.
|
||||
fn map_key_histogram() -> MapKeyHistogram {
|
||||
MAP_KEY
|
||||
|
|
|
@ -36,7 +36,7 @@ struct Row {
|
|||
}
|
||||
|
||||
/// The Buienradar API precipitation data item.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(crate = "rocket::serde", try_from = "Row")]
|
||||
pub(crate) struct Item {
|
||||
/// The time(stamp) of the forecast.
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::position::Position;
|
|||
use crate::Metric;
|
||||
|
||||
/// The combined data item.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub(crate) struct Item {
|
||||
/// The time(stamp) of the forecast.
|
||||
|
@ -25,6 +25,13 @@ pub(crate) struct Item {
|
|||
value: f32,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(time: DateTime<Utc>, value: f32) -> Self {
|
||||
Self { time, value }
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges pollen samples and AQI items into combined items.
|
||||
///
|
||||
/// The merging drops items from either the pollen samples or from the AQI items if they are not
|
||||
|
@ -39,14 +46,15 @@ pub(crate) struct Item {
|
|||
fn merge(
|
||||
pollen_samples: Vec<BuienradarSample>,
|
||||
aqi_items: Vec<LuchtmeetnetItem>,
|
||||
) -> Option<(
|
||||
Vec<Item>,
|
||||
Option<BuienradarSample>,
|
||||
Option<LuchtmeetnetItem>,
|
||||
)> {
|
||||
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> {
|
||||
let mut pollen_samples = pollen_samples;
|
||||
let mut aqi_items = aqi_items;
|
||||
|
||||
// Only retain samples/items that have timestamps that are at least half an hour ago.
|
||||
let now = Utc::now();
|
||||
pollen_samples.retain(|smp| smp.time.signed_duration_since(now).num_seconds() > -1800);
|
||||
aqi_items.retain(|item| item.time.signed_duration_since(now).num_seconds() > -1800);
|
||||
|
||||
// Align the iterators based on the (hourly) timestamps!
|
||||
let pollen_first_time = pollen_samples.first()?.time;
|
||||
let aqi_first_time = aqi_items.first()?.time;
|
||||
|
@ -73,14 +81,18 @@ fn merge(
|
|||
}
|
||||
|
||||
// Find the maximum sample/item of each series.
|
||||
// Note: Unwrapping is possible because each series has at least an item otherwise `.first`
|
||||
// would have failed above.
|
||||
let pollen_max = pollen_samples
|
||||
.iter()
|
||||
.max_by_key(|sample| sample.score)
|
||||
.cloned();
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let aqi_max = aqi_items
|
||||
.iter()
|
||||
.max_by_key(|item| (item.value * 1_000.0) as u32)
|
||||
.cloned();
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
// Combine the samples with items by taking the maximum of pollen sample score and AQI item
|
||||
// value.
|
||||
|
@ -123,11 +135,7 @@ pub(crate) async fn get(
|
|||
position: Position,
|
||||
metric: Metric,
|
||||
maps_handle: &MapsHandle,
|
||||
) -> Option<(
|
||||
Vec<Item>,
|
||||
Option<BuienradarSample>,
|
||||
Option<LuchtmeetnetItem>,
|
||||
)> {
|
||||
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> {
|
||||
if metric != Metric::PAQI {
|
||||
return None;
|
||||
};
|
||||
|
@ -136,3 +144,110 @@ pub(crate) async fn get(
|
|||
|
||||
merge(pollen_items?, aqi_items?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Duration, Timelike};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let t_now = Utc::now()
|
||||
.with_second(0)
|
||||
.unwrap()
|
||||
.with_nanosecond(0)
|
||||
.unwrap();
|
||||
let t_m2 = t_now.checked_sub_signed(Duration::days(1)).unwrap();
|
||||
let t_m1 = t_now.checked_sub_signed(Duration::hours(2)).unwrap();
|
||||
let t_0 = t_now.checked_add_signed(Duration::minutes(12)).unwrap();
|
||||
let t_1 = t_now.checked_add_signed(Duration::minutes(72)).unwrap();
|
||||
let t_2 = t_now.checked_add_signed(Duration::minutes(132)).unwrap();
|
||||
|
||||
let pollen_samples = Vec::from([
|
||||
BuienradarSample::new(t_m2, 4),
|
||||
BuienradarSample::new(t_m1, 5),
|
||||
BuienradarSample::new(t_0, 1),
|
||||
BuienradarSample::new(t_1, 3),
|
||||
BuienradarSample::new(t_2, 2),
|
||||
]);
|
||||
let aqi_items = Vec::from([
|
||||
LuchtmeetnetItem::new(t_m2, 4.0),
|
||||
LuchtmeetnetItem::new(t_m1, 5.0),
|
||||
LuchtmeetnetItem::new(t_0, 1.1),
|
||||
LuchtmeetnetItem::new(t_1, 2.9),
|
||||
LuchtmeetnetItem::new(t_2, 2.4),
|
||||
]);
|
||||
|
||||
// Perform a normal merge.
|
||||
let merged = super::merge(pollen_samples.clone(), aqi_items.clone());
|
||||
assert!(merged.is_some());
|
||||
let (paqi, max_pollen, max_aqi) = merged.unwrap();
|
||||
assert_eq!(
|
||||
paqi,
|
||||
Vec::from([
|
||||
Item::new(t_0, 1.1),
|
||||
Item::new(t_1, 3.0),
|
||||
Item::new(t_2, 2.4),
|
||||
])
|
||||
);
|
||||
assert_eq!(max_pollen, BuienradarSample::new(t_1, 3));
|
||||
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_1, 2.9));
|
||||
|
||||
// The pollen samples are shifted, i.e. one hour in the future.
|
||||
let shifted_pollen_samples = pollen_samples[2..]
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
item.time = item.time.checked_add_signed(Duration::hours(1)).unwrap();
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let merged = super::merge(shifted_pollen_samples, aqi_items.clone());
|
||||
assert!(merged.is_some());
|
||||
let (paqi, max_pollen, max_aqi) = merged.unwrap();
|
||||
assert_eq!(paqi, Vec::from([Item::new(t_1, 2.9), Item::new(t_2, 3.0),]));
|
||||
assert_eq!(max_pollen, BuienradarSample::new(t_2, 3));
|
||||
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_1, 2.9));
|
||||
|
||||
// The AQI items are shifted, i.e. one hour in the future.
|
||||
let shifted_aqi_items = aqi_items[2..]
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
item.time = item.time.checked_add_signed(Duration::hours(1)).unwrap();
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let merged = super::merge(pollen_samples.clone(), shifted_aqi_items);
|
||||
assert!(merged.is_some());
|
||||
let (paqi, max_pollen, max_aqi) = merged.unwrap();
|
||||
assert_eq!(paqi, Vec::from([Item::new(t_1, 3.0), Item::new(t_2, 2.9),]));
|
||||
assert_eq!(max_pollen, BuienradarSample::new(t_1, 3));
|
||||
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_2, 2.9));
|
||||
|
||||
// Merging fails because the samples/items are too far (6 hours) apart.
|
||||
let shifted_aqi_items = aqi_items
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
item.time = item.time.checked_add_signed(Duration::hours(6)).unwrap();
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let merged = super::merge(pollen_samples.clone(), shifted_aqi_items);
|
||||
assert_eq!(merged, None);
|
||||
|
||||
// The pollen samples list is empty, or everything is too old.
|
||||
let merged = super::merge(Vec::new(), aqi_items.clone());
|
||||
assert_eq!(merged, None);
|
||||
let merged = super::merge(pollen_samples[0..2].to_vec(), aqi_items.clone());
|
||||
assert_eq!(merged, None);
|
||||
|
||||
// The AQI items list is empty, or everything is too old.
|
||||
let merged = super::merge(pollen_samples.clone(), Vec::new());
|
||||
assert_eq!(merged, None);
|
||||
let merged = super::merge(pollen_samples, aqi_items[0..2].to_vec());
|
||||
assert_eq!(merged, None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ struct Container {
|
|||
}
|
||||
|
||||
/// The Luchtmeetnet API data item.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub(crate) struct Item {
|
||||
/// The time(stamp) of the forecast.
|
||||
|
@ -40,6 +40,13 @@ pub(crate) struct Item {
|
|||
pub(crate) value: f32,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(time: DateTime<Utc>, value: f32) -> Self {
|
||||
Self { time, value }
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the Luchtmeetnet forecasted items for the provided position and metric.
|
||||
///
|
||||
/// It supports the following metrics:
|
||||
|
|
Loading…
Reference in New Issue