Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use std::collections::HashSet;
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::round_div; ← from math.round-div ^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
// watt-hours x thousandths of a minor unit per kWh is millionths of a minor unit.
const DIVISOR: i64 = 1_000_000;
const MAX_SAFE: i128 = 9_007_199_254_740_991;
fn is_clock(s: &str) -> bool {
let b = s.as_bytes();
b.len() == 5
&& b[2] == b':'
&& b[0].is_ascii_digit()
&& b[1].is_ascii_digit()
&& ((b[0] <= b'1') || (b[0] == b'2' && b[1] <= b'3'))
&& ((b[3] == b'0' && b[4] == b'0') || (b[3] == b'3' && b[4] == b'0'))
}
fn is_slot(s: &str) -> bool {
let b = s.as_bytes();
b.len() == 16
&& b[4] == b'-'
&& b[7] == b'-'
&& b[10] == b'T'
&& [0, 1, 2, 3, 5, 6, 8, 9].iter().all(|&i| b[i].is_ascii_digit())
&& is_clock(&s[11..])
}
fn covers(rate: &TariffRate, start: &str) -> bool {
if rate.start.len() == 5 {
let time = &start[11..];
if rate.start == rate.end {
return true;
}
if rate.start < rate.end {
return rate.start.as_str() <= time && time < rate.end.as_str();
}
return time >= rate.start.as_str() || time < rate.end.as_str();
}
rate.start.as_str() <= start && start < rate.end.as_str()
}
/// Cost half-hourly usage against time-of-use rates. Each half hour must fall
/// in exactly one rate; each rate name becomes a line costed exactly and
/// rounded once.
///
/// # Panics
/// Panics on a malformed rate or usage entry, a duplicated half hour, a half
/// hour covered by no rate or by two, or a cost too large to be exact.
pub fn time_of_use_cost(usage: &[HalfHourUsage], rates: &[TariffRate], currency: &str, mode: &str) -> TimeOfUseCost {
let mut names: Vec<String> = Vec::new();
for r in rates {
let daily = is_clock(&r.start) && is_clock(&r.end);
let dated = is_slot(&r.start) && is_slot(&r.end);
if !daily && !dated {
panic!(
"rate {}: start and end must both be HH:MM or both YYYY-MM-DDTHH:MM on the hour or half hour, received {} and {}",
r.name, r.start, r.end
);
}
if dated && r.start >= r.end {
panic!("rate {}: start must be before end, received {} and {}", r.name, r.start, r.end);
}
if !names.contains(&r.name) {
names.push(r.name.clone());
}
}
let mut exact: Vec<i128> = vec![0; names.len()];
let mut energy: Vec<i64> = vec![0; names.len()];
let mut seen: HashSet<&str> = HashSet::new();
let mut watt_hours = 0;
for u in usage {
if !is_slot(&u.start) {
panic!("usage start must be YYYY-MM-DDTHH:MM on the hour or half hour, received {}", u.start);
}
if u.watt_hours < 0 {
panic!("usage wattHours must be a whole number of 0 or more, received {} at {}", u.watt_hours, u.start);
}
if !seen.insert(u.start.as_str()) {
panic!("usage for the half hour starting {} appears twice", u.start);
}
let matches: Vec<&TariffRate> = rates.iter().filter(|r| covers(r, &u.start)).collect();
if matches.is_empty() {
panic!("no rate covers the half hour starting {}", u.start);
}
if matches.len() > 1 {
panic!(
"rates {} and {} both cover the half hour starting {}",
matches[0].name, matches[1].name, u.start
);
}
let r = matches[0];
let i = names.iter().position(|n| *n == r.name).unwrap();
let part = u.watt_hours as i128 * r.rate as i128;
let next = exact[i] + part;
if next.abs() > MAX_SAFE || part.abs() > MAX_SAFE {
panic!("usage cost too large to calculate exactly");
}
exact[i] = next;
energy[i] += u.watt_hours;
watt_hours += u.watt_hours;
}
let mut total = 0;
let mut lines = Vec::new();
for (i, name) in names.iter().enumerate() {
let minor = round_div(exact[i] as i64, DIVISOR, mode);
total += minor;
lines.push(TimeOfUseLine {
name: name.clone(),
watt_hours: energy[i],
cost: money(minor, currency),
});
}
TimeOfUseCost {
lines,
watt_hours,
total: money(total, currency),
}
}
fn whole(v: &Value, message: &str) -> i64 {
if let Value::Float(f) = v {
panic!("{}, received {}", message, f);
}
v.as_i64()
}
pub fn half_hour_usage_from_value(v: &Value) -> HalfHourUsage {
HalfHourUsage {
start: v.get("start").as_str().to_string(),
watt_hours: whole(v.get("wattHours"), "usage wattHours must be a whole number of 0 or more"),
}
}
pub fn tariff_rate_from_value(v: &Value) -> TariffRate {
let name = v.get("name").as_str().to_string();
let message = format!("rate {}: rate must be a whole number of thousandths of a minor unit", name);
TariffRate {
rate: whole(v.get("rate"), &message),
name,
start: v.get("start").as_str().to_string(),
end: v.get("end").as_str().to_string(),
}
}
pub fn time_of_use_cost_to_value(c: &TimeOfUseCost) -> Value {
Value::obj(vec![
(
"lines",
Value::Arr(
c.lines
.iter()
.map(|l| {
Value::obj(vec![
("name", Value::str(&l.name)),
("wattHours", Value::Int(l.watt_hours)),
("cost", money_to_value(&l.cost)),
])
})
.collect(),
),
),
("wattHours", Value::Int(c.watt_hours)),
("total", money_to_value(&c.total)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let usage: Vec<HalfHourUsage> = args[0].as_arr().iter().map(half_hour_usage_from_value).collect();
let rates: Vec<TariffRate> = args[1].as_arr().iter().map(tariff_rate_from_value).collect();
time_of_use_cost_to_value(&time_of_use_cost(&usage, &rates, args[2].as_str(), args[3].as_str()))
}