Functional Weave
Code in TypeScript

construction.concrete-volume@1.0.0

impl/rust.rs

5,650 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::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;
const MM3_PER_LITRE: i64 = 1_000_000;
const PI: f64 = 3.141592653589793;

fn millimetres(element: usize, name: &str, value: Option<f64>) -> i64 {
    let v = match value {
        Some(v) if v.is_finite() && v > 0.0 && v <= 1000.0 => v,
        Some(v) => panic!(
            "element {}: {} must be a finite number greater than 0 and at most 1000 metres, received {}",
            element, name, v
        ),
        None => panic!(
            "element {}: {} must be a finite number greater than 0 and at most 1000 metres, received null",
            element, name
        ),
    };
    let mm = (round_float(v, 3) * 1000.0).round() as i64;
    if mm < 1 {
        panic!("element {}: {} must be at least 1 millimetre, received {}", element, name, v);
    }
    mm
}

fn too_large() -> ! {
    panic!("the total volume is too large (more than 9 million cubic metres)");
}

/// Concrete volume for a set of pours, and the quantity to order.
///
/// Dimensions are taken to the nearest millimetre and rectangular volumes are
/// exact integers of cubic millimetres; a round column is pi d^2 / 4 h in
/// floating point, rounded to a whole cubic millimetre. Working in metres and
/// rounding up the float is the naive way: 3 x 1 x 0.1 is 0.30000000000000004,
/// which orders 0.4 m3.
///
/// # Panics
/// Panics on an empty list, a missing or out-of-range dimension, a count
/// below 1, wastage outside 0..=10000, or a total beyond 2^53 - 1 mm3.
pub fn concrete_volume(elements: &[ConcreteElement], wastage_basis_points: i64) -> ConcreteVolume {
    if elements.is_empty() {
        panic!("elements must not be empty");
    }
    if !(0..=10000).contains(&wastage_basis_points) {
        panic!("wastageBasisPoints must be a whole number from 0 to 10000, received {}", wastage_basis_points);
    }

    let mut volumes: Vec<f64> = Vec::with_capacity(elements.len());
    let mut total: i128 = 0;
    for (i, el) in elements.iter().enumerate() {
        let n = i + 1;
        if el.count < 1 {
            panic!("element {}: count must be a whole number of at least 1, received {}", n, el.count);
        }
        let one: i128 = match el.kind.as_str() {
            "slab" | "strip-footing" | "pad" => {
                if el.length.is_none() || el.width.is_none() || el.depth.is_none() {
                    panic!("element {}: {} needs length, width and depth", n, el.kind);
                }
                millimetres(n, "length", el.length) as i128
                    * millimetres(n, "width", el.width) as i128
                    * millimetres(n, "depth", el.depth) as i128
            }
            "column" => {
                if el.diameter.is_none() || el.height.is_none() {
                    panic!("element {}: column needs diameter and height", n);
                }
                let d = millimetres(n, "diameter", el.diameter) as f64;
                let h = millimetres(n, "height", el.height) as f64;
                round_float(((PI * d * d) / 4.0) * h, 0) as i128
            }
            other => panic!("element {}: unknown kind \"{}\"", n, other),
        };
        // i128 could carry on, but TypeScript cannot, and all three must agree.
        if one > MAX_SAFE {
            too_large();
        }
        let volume = one * el.count as i128;
        if volume > MAX_SAFE {
            too_large();
        }
        total += volume;
        if total > MAX_SAFE {
            too_large();
        }
        volumes.push(round_div(volume as i64, MM3_PER_LITRE, "half-up") as f64 / 1000.0);
    }

    let total = total as i64;
    // Up to the whole litre, add the wastage, then up to the next 100 litres.
    let litres_up = round_div(total, MM3_PER_LITRE, "up");
    let tenths = round_div(litres_up * (10000 + wastage_basis_points), 100 * 10000, "up");
    ConcreteVolume {
        element_cubic_metres: volumes,
        cubic_metres: round_div(total, MM3_PER_LITRE, "half-up") as f64 / 1000.0,
        order_cubic_metres: tenths as f64 / 10.0,
    }
}

fn optional_float(v: &Value) -> Option<f64> {
    if v.is_null() {
        None
    } else {
        Some(v.as_f64())
    }
}

pub fn concrete_element_from_value(v: &Value, index: usize) -> ConcreteElement {
    if let Value::Float(f) = v.get("count") {
        panic!("element {}: count must be a whole number of at least 1, received {}", index + 1, f);
    }
    ConcreteElement {
        kind: v.get("kind").as_str().to_string(),
        length: optional_float(v.get("length")),
        width: optional_float(v.get("width")),
        depth: optional_float(v.get("depth")),
        diameter: optional_float(v.get("diameter")),
        height: optional_float(v.get("height")),
        count: v.get("count").as_i64(),
    }
}

pub fn concrete_volume_to_value(c: &ConcreteVolume) -> Value {
    Value::obj(vec![
        ("elementCubicMetres", Value::Arr(c.element_cubic_metres.iter().map(|&v| Value::Float(v)).collect())),
        ("cubicMetres", Value::Float(c.cubic_metres)),
        ("orderCubicMetres", Value::Float(c.order_cubic_metres)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let elements: Vec<ConcreteElement> = args[0]
        .as_arr()
        .iter()
        .enumerate()
        .map(|(i, v)| concrete_element_from_value(v, i))
        .collect();
    if let Value::Float(f) = &args[1] {
        panic!("wastageBasisPoints must be a whole number from 0 to 10000, received {}", f);
    }
    concrete_volume_to_value(&concrete_volume(&elements, args[1].as_i64()))
}