Functional Weave
Code in Rust

fleet.emissions@1.0.1

impl/rust.rs

4,419 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::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))
}