use std::collections::BTreeMap;
use chrono::NaiveDate;
use scraper::Html;
use super::parse;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct RateEntry {
pub current_effective: Option<NaiveDate>,
pub current: Option<f64>,
pub previous_effective: Option<NaiveDate>,
pub previous: Option<f64>,
}
impl RateEntry {
pub fn value(&self) -> Option<f64> {
self.current.or(self.previous)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ElectricityRates {
pub tiered: BTreeMap<Tier, RateEntry>,
pub tou: BTreeMap<TouPeriod, RateEntry>,
pub ulo: BTreeMap<UloPeriod, RateEntry>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Fees {
pub admin: Option<RateEntry>,
pub meter: Option<RateEntry>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Rates {
pub electricity: ElectricityRates,
pub fees: Fees,
pub sections: BTreeMap<String, BTreeMap<String, RateEntry>>,
pub unknown: BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Tier {
First,
Second,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TouPeriod {
OffPeak,
MidPeak,
OnPeak,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum UloPeriod {
UltraLowOvernight,
WeekendOffPeak,
MidPeak,
OnPeak,
}
impl Rates {
pub fn parse(html: &str) -> Self {
parse::rates(&Html::parse_document(html))
}
pub fn parse_page(html: &str) -> Option<Self> {
parse::page(&Html::parse_document(html))
}
pub fn is_empty(&self) -> bool {
let e = &self.electricity;
e.tiered.is_empty()
&& e.tou.is_empty()
&& e.ulo.is_empty()
&& self.fees.admin.is_none()
&& self.fees.meter.is_none()
&& self.sections.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
const PAGE: &str = include_str!("../../../../tests/fixtures/rates_page.html");
#[test]
fn parses_the_observed_page() {
let rates = Rates::parse(PAGE);
let tou = &rates.electricity.tou;
assert_eq!(
tou.get(&TouPeriod::OffPeak).map(|r| r.current),
Some(Some(9.8)),
"{rates:?}"
);
assert_eq!(
tou.get(&TouPeriod::OnPeak).map(|r| r.previous),
Some(Some(15.8))
);
let ulo = &rates.electricity.ulo;
assert_eq!(
ulo.get(&UloPeriod::UltraLowOvernight).map(|r| r.current),
Some(Some(3.9))
);
assert_eq!(
rates
.electricity
.tiered
.get(&Tier::Second)
.and_then(|r| r.current),
Some(14.2)
);
assert_eq!(
rates.fees.admin.as_ref().and_then(|f| f.current),
Some(11.90)
);
assert_eq!(
rates.fees.meter.as_ref().and_then(|f| f.previous),
Some(8.31)
);
assert!(rates.unknown.is_empty(), "{:?}", rates.unknown);
}
#[test]
fn empty_and_foreign_html_yield_defaults() {
assert_eq!(Rates::parse(""), Rates::default());
assert_eq!(
Rates::parse("<html><body><p>hi</p></body></html>"),
Rates::default()
);
}
#[test]
fn restyled_or_partial_rows_do_not_fail() {
let html = r#"
<div class="rates-holder">
<h2>Electricity Energy Rates</h2>
<table class="rates1"><tr><th></th><th>Effective Date</th><th>Rate</th><th>Effective Date</th><th>Rate</th></tr>
<tr><td>Time-Of-Use (TOU)</td><td></td><td></td><td></td><td></td></tr>
<tr><td>Off-Peak</td><td>—</td><td>9.8</td><td></td><td>—</td></tr>
<tr><td>Surcharge</td><td>x</td><td>1</td><td>y</td><td>2</td></tr>
</table>
<h2>Provident’s Administration and Delivery Charges</h2>
<table class="rates1 rates2"></table>
</div>"#;
let rates = Rates::parse(html);
assert_eq!(
rates
.electricity
.tou
.get(&TouPeriod::OffPeak)
.map(|r| r.current),
Some(Some(9.8))
);
assert!(
rates
.electricity
.tou
.get(&TouPeriod::OffPeak)
.unwrap()
.previous
.is_none()
);
assert!(rates.fees.admin.is_none());
assert_eq!(rates.unknown.keys().collect::<Vec<_>>(), ["Surcharge"]);
}
}