Compare commits

...

7 Commits

Author SHA1 Message Date
Paul van Tilburg b61eb63cdf
Update the changelog 2022-05-08 14:04:05 +02:00
Paul van Tilburg e5e006dc00
Bump the version to 0.2.1 2022-05-08 14:02:47 +02:00
Paul van Tilburg a0c4e0da77
Don't use Option for the max sample/item return values
If the sample/item series are empty, the function already returns
`None`, so the tuple values are always `Some(_)` which makes the
`Option` type redundant.
2022-05-08 14:01:22 +02:00
Paul van Tilburg 34b63ec94d
Compact merge tests by using constructors
Add the constructors to sample/item structs for testing purposes.
2022-05-08 14:00:12 +02:00
Paul van Tilburg 5a23e83b7f
Extend tests for combined provider merge function
* Check that merging fails if the samples/items are too far apart
* Check that nothing is returned if either of the lists is empty
* Check that if either series is shifted, they are merged correctly
2022-05-08 13:48:09 +02:00
Paul van Tilburg e2d1a1d9df
Filter out old item/samples in combined provider
* Only retain samples/items that have timestamps that are at least half
  an hour ago
* Add a test to verify that the merge discards them
2022-05-08 12:53:32 +02:00
Paul van Tilburg 29b79c720d
Derive PartialEq for most item/sample structs 2022-05-08 12:53:09 +02:00
8 changed files with 162 additions and 22 deletions

View File

@ -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

2
Cargo.lock generated
View File

@ -2129,7 +2129,7 @@ dependencies = [
[[package]]
name = "sinoptik"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"assert_float_eq",
"assert_matches",

View File

@ -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>"

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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: