Functional Weave
Code in Rust

fleet.ev-charging-cost@1.0.0

impl/rust.rs

5,164 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 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(),
    ))
}