Parse Buienradar as CSV file using serde
* Add the `csv` crate as a dependency * Use the `Row` struct as intermediate object * Turn the `parse_value` function into the `convert_value` function that cannot fail
This commit is contained in:
parent
4232263a45
commit
79dac18655
35
Cargo.lock
generated
35
Cargo.lock
generated
|
@ -144,6 +144,18 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.9.1"
|
version = "3.9.1"
|
||||||
|
@ -347,6 +359,28 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"csv-core",
|
||||||
|
"itoa 0.4.8",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deflate"
|
name = "deflate"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -2011,6 +2045,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"csv",
|
||||||
"geocoding",
|
"geocoding",
|
||||||
"image",
|
"image",
|
||||||
"reqwest 0.11.9",
|
"reqwest 0.11.9",
|
||||||
|
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
csv = "1.1.6"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
chrono-tz = "0.6.1"
|
chrono-tz = "0.6.1"
|
||||||
color-eyre = "0.5.6"
|
color-eyre = "0.5.6"
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
//! The Buienradar data provider.
|
//! The Buienradar data provider.
|
||||||
//!
|
//!
|
||||||
//! For more information about Buienradar, see: <https://www.buienradar.nl/overbuienradar/contact>
|
//! For more information about Buienradar, see: <https://www.buienradar.nl/overbuienradar/contact>
|
||||||
//! and <https://www.buienradar.nl/overbuienradar/gratis-weerdata>
|
//! and <https://www.buienradar.nl/overbuienradar/gratis-weerdata>.
|
||||||
|
|
||||||
use chrono::offset::TimeZone;
|
use chrono::offset::TimeZone;
|
||||||
use chrono::serde::ts_seconds;
|
use chrono::serde::ts_seconds;
|
||||||
use chrono::{DateTime, Local, NaiveDate, NaiveTime, Utc};
|
use chrono::{DateTime, Local, NaiveTime, ParseError, Utc};
|
||||||
use chrono_tz::Europe;
|
use chrono_tz::Europe;
|
||||||
|
use csv::ReaderBuilder;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::Metric;
|
use crate::Metric;
|
||||||
|
|
||||||
/// The base URL for the Buienradar API.
|
/// The base URL for the Buienradar API.
|
||||||
const BUIENRADAR_BASE_URL: &str = "https://gpsgadget.buienradar.nl/data/raintext";
|
const BUIENRADAR_BASE_URL: &str = "https://gpsgadget.buienradar.nl/data/raintext";
|
||||||
|
|
||||||
/// The Buienradar API precipitation data item.
|
/// A row in the precipitation text output.
|
||||||
#[derive(Debug, Serialize)]
|
///
|
||||||
|
/// This is an intermediate type used to represent rows of the output.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
|
struct Row {
|
||||||
|
/// The precipitation value in the range `0..=255`.
|
||||||
|
value: u16,
|
||||||
|
|
||||||
|
/// The time in the `HH:MM` format.
|
||||||
|
time: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Buienradar API precipitation data item.
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde", try_from = "Row")]
|
||||||
pub(crate) struct Item {
|
pub(crate) struct Item {
|
||||||
/// The time(stamp) of the forecast.
|
/// The time(stamp) of the forecast.
|
||||||
#[serde(serialize_with = "ts_seconds::serialize")]
|
#[serde(serialize_with = "ts_seconds::serialize")]
|
||||||
|
@ -25,54 +39,47 @@ pub(crate) struct Item {
|
||||||
|
|
||||||
/// The forecasted value.
|
/// The forecasted value.
|
||||||
///
|
///
|
||||||
/// The unit is FIXME?
|
/// Its unit is mm/h.
|
||||||
value: f32,
|
value: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a line of the Buienradar precipitation text interface into an item.
|
impl TryFrom<Row> for Item {
|
||||||
///
|
type Error = ParseError;
|
||||||
// Each line has the format: `val|HH:MM`, for example: `362|12:30`.
|
|
||||||
fn parse_item(line: &str, today: &NaiveDate) -> Option<Item> {
|
|
||||||
line.split_once('|')
|
|
||||||
.map(|(v, t)| {
|
|
||||||
let time = parse_time(t, today)?;
|
|
||||||
let value = parse_value(v)?;
|
|
||||||
|
|
||||||
Some(Item { time, value })
|
fn try_from(row: Row) -> Result<Self, Self::Error> {
|
||||||
})
|
let time = parse_time(&row.time)?;
|
||||||
.flatten()
|
let value = convert_value(row.value);
|
||||||
|
|
||||||
|
Ok(Item { time, value })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a time string to date/time in the UTC time zone.
|
/// Parses a time string to date/time in the UTC time zone.
|
||||||
///
|
///
|
||||||
/// The provided time has the format `HH:MM` and is considered to be in the Europe/Amsterdam
|
/// The provided time has the format `HH:MM` and is considered to be in the Europe/Amsterdam
|
||||||
/// time zone.
|
/// time zone.
|
||||||
///
|
fn parse_time(t: &str) -> Result<DateTime<Utc>, ParseError> {
|
||||||
/// Returns [`None`] if the time cannot be parsed.
|
// First, get the naive time.
|
||||||
fn parse_time(t: &str, today: &NaiveDate) -> Option<DateTime<Utc>> {
|
let ntime = NaiveTime::parse_from_str(t, "%H:%M")?;
|
||||||
// First, get the naive date/time.
|
// FIXME: This might actually be the day before when started on a machine that
|
||||||
let ntime = NaiveTime::parse_from_str(t, "%H:%M").ok()?;
|
// doesn't run in the Europe/Amsterdam time zone.
|
||||||
let ndtime = today.and_time(ntime);
|
let ndtime = Local::today().naive_local().and_time(ntime);
|
||||||
// Then, interpret the naive date/time in the Europe/Amsterdam time zone and convert it to the
|
// Then, interpret the naive date/time in the Europe/Amsterdam time zone and convert it to the
|
||||||
// UTC time zone.
|
// UTC time zone.
|
||||||
let ldtime = Europe::Amsterdam.from_local_datetime(&ndtime).unwrap();
|
let ldtime = Europe::Amsterdam.from_local_datetime(&ndtime).unwrap();
|
||||||
let dtime = ldtime.with_timezone(&Utc);
|
let dtime = ldtime.with_timezone(&Utc);
|
||||||
|
|
||||||
Some(dtime)
|
Ok(dtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a precipitation value into an intensity value in mm/h.
|
/// Converts a precipitation value into an precipitation intensity value in mm/h.
|
||||||
///
|
///
|
||||||
/// For the conversion formula, see: <https://www.buienradar.nl/overbuienradar/gratis-weerdata>.
|
/// For the conversion formula, see: <https://www.buienradar.nl/overbuienradar/gratis-weerdata>.
|
||||||
///
|
fn convert_value(v: u16) -> f32 {
|
||||||
/// Returns [`None`] if the value cannot be parsed.
|
|
||||||
fn parse_value(v: &str) -> Option<f32> {
|
|
||||||
let value = v.parse::<f32>().ok()?;
|
|
||||||
let base: f32 = 10.0;
|
let base: f32 = 10.0;
|
||||||
let value = base.powf((value - 109.0) / 32.0);
|
let value = base.powf((v as f32 - 109.0) / 32.0);
|
||||||
let value = (value * 10.0).round() / 10.0;
|
|
||||||
|
|
||||||
Some(value)
|
(value * 10.0).round() / 10.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the Buienradar forecasted precipitation items for the provided position.
|
/// Retrieves the Buienradar forecasted precipitation items for the provided position.
|
||||||
|
@ -97,10 +104,10 @@ pub(crate) async fn get(lat: f64, lon: f64, metric: Metric) -> Option<Vec<Item>>
|
||||||
Ok(res) => res.text().await.ok()?,
|
Ok(res) => res.text().await.ok()?,
|
||||||
Err(_err) => return None,
|
Err(_err) => return None,
|
||||||
};
|
};
|
||||||
let today = Local::today().naive_utc();
|
|
||||||
|
|
||||||
output
|
let mut rdr = ReaderBuilder::new()
|
||||||
.lines()
|
.has_headers(false)
|
||||||
.map(|line| parse_item(line, &today))
|
.delimiter(b'|')
|
||||||
.collect()
|
.from_reader(output.as_bytes());
|
||||||
|
rdr.deserialize().collect::<Result<_, _>>().ok()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue