Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div_big::round_div_big; ← from math.round-div-big ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
const DAY: i64 = 1440;
fn minute_of_day(name: &str, text: &str) -> i64 {
let b = text.as_bytes();
let ok = b.len() == 5
&& b[2] == b':'
&& b.iter().enumerate().all(|(i, c)| i == 2 || c.is_ascii_digit())
&& ((b[0] - b'0') * 10 + (b[1] - b'0')) < 24
&& b[3] <= b'5';
if !ok {
panic!("{} must be HH:MM, 00:00 to 23:59, received \"{}\"", name, text);
}
(((b[0] - b'0') as i64) * 10 + (b[1] - b'0') as i64) * 60 + ((b[3] - b'0') as i64) * 10 + (b[4] - b'0') as i64
}
fn whole(name: &str, value: i64, lo: i64, hi: i64, rule: &str) {
if value < lo || value > hi {
panic!("{} must be {}, received {}", name, rule, value);
}
}
/// The cost of one charge on a time-of-use tariff.
///
/// The energy drawn is more than the energy added (the charger and battery
/// lose some), and it flows at the charger's power from the start time, so it
/// falls into the tariff's windows in order. Everything is counted in exact
/// units of 1/(60 x efficiency) Wh, where a minute of charging is a whole
/// number, so the only rounding is the final one.
///
/// # Panics
/// Panics on an out-of-range number, a malformed time, an empty or unsorted
/// tariff, a negative price, a bad currency or an unknown mode.
pub fn ev_charging_cost(
energy_added_wh: i64,
efficiency_basis_points: i64,
charger_power_watts: i64,
start_time: &str,
tariff: &[TariffWindow],
currency: &str,
mode: &str,
) -> EvChargingCost {
whole("energyAddedWh", energy_added_wh, 0, 1000000, "a whole number from 0 to 1000000");
whole("efficiencyBasisPoints", efficiency_basis_points, 1, 10000, "a whole number from 1 to 10000");
whole("chargerPowerWatts", charger_power_watts, 1, 1000000, "a whole number from 1 to 1000000");
let mut t = minute_of_day("startTime", start_time);
if tariff.is_empty() {
panic!("tariff must have at least one window");
}
let mut starts: Vec<i64> = Vec::new();
for window in tariff {
let start = minute_of_day("tariff start", &window.start);
if let Some(last) = starts.last() {
if start <= *last {
panic!("tariff windows must be in ascending order of start time");
}
}
whole("pricePerKwhTenths", window.price_per_kwh_tenths, 0, 9007199254740991, "a whole number, 0 or more");
starts.push(start);
}
money(0, currency);
let eff = efficiency_basis_points as i128;
let per_minute = charger_power_watts as i128 * eff;
let mut remaining = energy_added_wh as i128 * 600000;
let total = remaining;
let mut cost_numerator: i128 = 0;
while remaining > 0 {
let mut index = starts.len() - 1;
for (i, s) in starts.iter().enumerate() {
if *s <= t {
index = i;
}
}
let next = if index + 1 < starts.len() { starts[index + 1] } else { starts[0] };
let length = if next > t { next - t } else { next + DAY - t };
let take = remaining.min(length as i128 * per_minute);
cost_numerator = cost_numerator
.checked_add(take.checked_mul(tariff[index].price_per_kwh_tenths as i128).expect("the charging cost is too large"))
.expect("the charging cost is too large");
remaining -= take;
t = (t + length) % DAY;
}
let unit = 60 * eff;
let cost = round_div_big(&cost_numerator.to_string(), &(unit * 10000).to_string(), mode);
EvChargingCost {
energy_drawn_wh: round_div_big(&total.to_string(), &unit.to_string(), "half-up").parse().unwrap(),
minutes: round_div_big(&total.to_string(), &per_minute.to_string(), "up").parse().unwrap(),
cost: money(cost.parse().expect("the charging cost is too large"), currency),
}
}
pub fn ev_charging_cost_to_value(c: &EvChargingCost) -> Value {
Value::obj(vec![
("energyDrawnWh", Value::Int(c.energy_drawn_wh)),
("minutes", Value::Int(c.minutes)),
("cost", money_to_value(&c.cost)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
for (i, name) in [(0usize, "energyAddedWh"), (1, "efficiencyBasisPoints"), (2, "chargerPowerWatts")] {
if let Value::Float(f) = &args[i] {
panic!("{} must be a whole number, received {}", name, f);
}
}
let tariff: Vec<TariffWindow> = args[4]
.as_arr()
.iter()
.map(|w| {
if let Value::Float(f) = w.get("pricePerKwhTenths") {
panic!("pricePerKwhTenths must be a whole number, 0 or more, received {}", f);
}
TariffWindow {
start: w.get("start").as_str().to_string(),
price_per_kwh_tenths: w.get("pricePerKwhTenths").as_i64(),
}
})
.collect();
ev_charging_cost_to_value(&ev_charging_cost(
args[0].as_i64(),
args[1].as_i64(),
args[2].as_i64(),
args[3].as_str(),
&tariff,
args[5].as_str(),
args[6].as_str(),
))
}