use super::funejson::Value; use super::math_round_div_big::round_div_big; use super::money_amount::{money, money_to_value}; 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 = 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 = 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(), )) }