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::fleet_emissions_data::GHG_FACTORS; ← this capability’s own data, compiled from data/ghg-factors.json into the same file by fune build
const MAX_SAFE: i128 = 9007199254740991;
fn format_factor(factor: i64) -> String {
format!("{}.{:05}", factor / 100000, factor % 100000)
}
/// Split plain decimal text into its whole and fractional digits, or None.
fn split_decimal(text: &str) -> Option<(&str, &str)> {
let (whole, fraction) = match text.find('.') {
Some(i) => (&text[..i], &text[i + 1..]),
None => (text, ""),
};
let digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
if whole.is_empty() || !digits(whole) || !digits(fraction) || (text.contains('.') && fraction.is_empty()) {
return None;
}
Some((whole, fraction))
}
/// Scope 1 emissions for fuel burned or distance driven, from the UK
/// government's conversion factors for company reporting.
///
/// The quantity is decimal text and the factor an integer in 1e-5 kg, so the
/// product is exact and rounded once, to the gram. Multiplying floats instead
/// drifts in the last place and can round a half-gram the wrong way.
///
/// # Panics
/// Panics on an unknown unit, year or activity, a unit the activity has no
/// factor for, a malformed or negative quantity, or a result beyond 2^53 - 1.
pub fn fleet_emissions(activity: &str, quantity: &str, unit: &str, factor_year: i64) -> FleetEmissions {
if unit != "litres" && unit != "km" && unit != "miles" {
panic!("unknown unit \"{}\": use litres, km or miles", unit);
}
let (whole, fraction) = match split_decimal(quantity) {
Some(parts) => parts,
None => panic!(
"quantity must be plain decimal text, not negative, received \"{}\"",
quantity
),
};
let all_digits = format!("{}{}", whole, fraction);
let significant = all_digits.trim_start_matches('0');
if significant.len() > 15 || fraction.len() > 9 {
panic!(
"quantity must have at most 15 significant digits and 9 decimal places, received \"{}\"",
quantity
);
}
let mut found: Option<i64> = None;
let mut year_known = false;
let mut activity_known = false;
for row in GHG_FACTORS {
if row.factor_year != factor_year {
continue;
}
year_known = true;
if row.activity != activity {
continue;
}
activity_known = true;
if row.unit == unit {
found = Some(row.factor);
}
}
if !year_known {
panic!("no conversion factors for {}: this package carries 2023 to 2026", factor_year);
}
if !activity_known {
panic!("unknown activity \"{}\" in the {} factors", activity, factor_year);
}
let factor = match found {
Some(f) => f,
None => panic!("no conversion factor for {} per {} in {}", activity, unit, factor_year),
};
// grams = quantity x factor x 1e-5 kg x 1000 g/kg = quantity x factor / 100.
// At most 15 digits times a factor below 1e7 fits i128 with room to spare.
let value: i128 = if significant.is_empty() { 0 } else { significant.parse().unwrap() };
let numerator = value * factor as i128;
let denominator = 10i128.pow(fraction.len() as u32) * 100;
let grams = (2 * numerator + denominator) / (2 * denominator);
if grams > MAX_SAFE {
panic!("the result exceeds 2^53 - 1 grams");
}
FleetEmissions {
co2e_grams: grams as i64,
factor: format_factor(factor),
factor_year,
activity: activity.to_string(),
unit: unit.to_string(),
}
}
pub fn fleet_emissions_to_value(e: &FleetEmissions) -> Value {
Value::obj(vec![
("co2eGrams", Value::Int(e.co2e_grams)),
("factor", Value::str(&e.factor)),
("factorYear", Value::Int(e.factor_year)),
("activity", Value::str(&e.activity)),
("unit", Value::str(&e.unit)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let quantity = match &args[1] {
Value::Str(s) => s.as_str(),
other => panic!(
"quantity must be plain decimal text, not negative, received {:?}",
other
),
};
let year = match &args[3] {
Value::Int(i) => *i,
other => panic!("factorYear must be a whole year, received {:?}", other),
};
fleet_emissions_to_value(&fleet_emissions(args[0].as_str(), quantity, args[2].as_str(), year))
}