123 lines
3.7 KiB
Rust
123 lines
3.7 KiB
Rust
use std::fmt::{Debug, Display};
|
|
|
|
use anyhow::{Context, Result};
|
|
use chrono::{DateTime, Local, TimeZone, Utc};
|
|
use serde::{de, Deserialize, Deserializer};
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct Departure {
|
|
line_name: String,
|
|
direction: String,
|
|
#[serde(default, deserialize_with = "deserialize_realtime")]
|
|
real_time: Option<DateTime<Local>>,
|
|
#[serde(deserialize_with = "deserialize_timestamp")]
|
|
scheduled_time: DateTime<Local>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct Response {
|
|
pub departures: Vec<Departure>,
|
|
}
|
|
|
|
/// Deserialize a timestamp in the form of '/Date(1671713820000-0000)/'
|
|
fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<DateTime<Local>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
let timestamp: i64 = s
|
|
.trim_start_matches("/Date(")
|
|
.trim_end_matches(")/")
|
|
.split('-')
|
|
.next()
|
|
.ok_or_else(|| de::Error::custom("Invalid timestamp format"))?
|
|
.parse()
|
|
.map_err(de::Error::custom)?;
|
|
let result = Utc.timestamp_millis_opt(timestamp).unwrap();
|
|
|
|
let local_tz = chrono::offset::Local::now().timezone();
|
|
Ok(result.with_timezone(&local_tz))
|
|
}
|
|
|
|
fn deserialize_realtime<'de, D>(deserializer: D) -> Result<Option<DateTime<Local>>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
match deserialize_timestamp(deserializer) {
|
|
Ok(inner) => Ok(Some(inner)),
|
|
Err(_) => Ok(None),
|
|
}
|
|
}
|
|
|
|
impl Display for Departure {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(&format!("{} {}\n", &self.line_name, &self.direction))?;
|
|
|
|
let scheduled_time = self.scheduled_time.format("%H:%M");
|
|
f.write_str(&format!("departure: {}", &scheduled_time))?;
|
|
|
|
if let Some(real_time) = self.real_time {
|
|
let actual_time = real_time.format("%H:%M");
|
|
if real_time != self.scheduled_time {
|
|
f.write_str(&format!(" | delayed: {actual_time} "))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn format_departures(response: Response) -> String {
|
|
let departures: Vec<String> = response.departures.iter().map(|d| d.to_string()).collect();
|
|
departures.join("\n\n")
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct PointFinderResponse {
|
|
#[serde(deserialize_with = "deserialize_points", rename = "Points")]
|
|
pub point: Point,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct Point {
|
|
pub stopid: String,
|
|
pub name: String,
|
|
pub area: String,
|
|
}
|
|
|
|
impl From<(&str, &str, &str)> for Point {
|
|
fn from(value: (&str, &str, &str)) -> Self {
|
|
Self {
|
|
stopid: value.0.into(),
|
|
name: value.1.into(),
|
|
area: value.2.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Deserialize a Point Response in the form of
|
|
/// '\"33000742|||Helmholtzstraße|5655904|4621157|0||\"'
|
|
fn extract_data_from_response(line: &str) -> Result<Point> {
|
|
let mut iter = line.trim_start_matches("\\\"").split_terminator('|');
|
|
|
|
let stopid = iter.next().context("getting stopid from line")?;
|
|
let area = iter.nth(1).context("getting area from line")?;
|
|
let name = iter.next().context("getting stop name from line")?;
|
|
Ok((stopid, name, area).into())
|
|
}
|
|
|
|
fn deserialize_points<'de, D>(deserializer: D) -> Result<Point, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let stops: Vec<String> = Vec::deserialize(deserializer)?;
|
|
let first_stop_line = stops
|
|
.first()
|
|
.ok_or_else(|| de::Error::custom("could not get first stop"))?;
|
|
let point = extract_data_from_response(first_stop_line).map_err(de::Error::custom)?;
|
|
Ok(point)
|
|
}
|