Functional Weave
Code in Rust

energy.tariff-time-of-use@1.0.1

impl/rust.rs

6,204 bytes · the Rust implementation · view raw

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()))
}