use std::collections::HashSet; use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_to_value}; // 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 = 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 = vec![0; names.len()]; let mut energy: Vec = 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 = args[0].as_arr().iter().map(half_hour_usage_from_value).collect(); let rates: Vec = 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())) }