Functional Weave
Code in Rust

property.floor-area@1.0.2

impl/rust.rs

3,851 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::units_convert::convert_units;  ← from units.convert ^1.0.0 · built alongside by fune

const MAX_ROOMS: usize = 1000;

/// A dimension as an integer number of thousandths.
fn thousandths(room: &str, field: &str, text: &str) -> i64 {
    let bad = || -> ! {
        panic!(
            "room \"{}\": {} must be a decimal below 1000 with at most 3 decimal places, received \"{}\"",
            room, field, text
        )
    };
    let (whole, fraction) = match text.split_once('.') {
        Some((w, f)) => (w, Some(f)),
        None => (text, None),
    };
    if whole.is_empty() || whole.len() > 3 || !whole.bytes().all(|b| b.is_ascii_digit()) {
        bad();
    }
    let mut value = whole.parse::<i64>().unwrap() * 1000;
    if let Some(f) = fraction {
        if f.is_empty() || f.len() > 3 || !f.bytes().all(|b| b.is_ascii_digit()) {
            bad();
        }
        value += format!("{:0<3}", f).parse::<i64>().unwrap();
    }
    if value == 0 {
        panic!("room \"{}\": {} must be greater than zero", room, field);
    }
    value
}

/// An integer number of millionths as the shortest exact decimal.
fn decimal(millionths: i64) -> String {
    let whole = millionths / 1_000_000;
    let fraction = format!("{:06}", millionths % 1_000_000);
    let fraction = fraction.trim_end_matches('0');
    if fraction.is_empty() {
        whole.to_string()
    } else {
        format!("{}.{}", whole, fraction)
    }
}

/// Room areas and their total, exact, from length × width of each room, and
/// the total in square metres and square feet rounded once at the end.
///
/// # Panics
/// Panics on an unknown unit, no rooms or too many, or a dimension that is
/// not a positive decimal below 1000 with at most 3 places.
pub fn floor_area(rooms: &[RoomDimensions], length_unit: &str, decimals: i64) -> FloorArea {
    if length_unit != "m" && length_unit != "ft" {
        panic!("unknown length unit \"{}\": use m or ft", length_unit);
    }
    if rooms.is_empty() {
        panic!("rooms must not be empty");
    }
    if rooms.len() > MAX_ROOMS {
        panic!("at most {} rooms, received {}", MAX_ROOMS, rooms.len());
    }
    let areas: Vec<i64> = rooms
        .iter()
        .map(|r| thousandths(&r.name, "length", &r.length) * thousandths(&r.name, "width", &r.width))
        .collect();
    let total = decimal(areas.iter().sum());
    let from = if length_unit == "m" { "m2" } else { "ft2" };
    FloorArea {
        room_areas: areas.iter().map(|a| decimal(*a)).collect(),
        square_metres: convert_units(&total, from, "m2", decimals),
        square_feet: convert_units(&total, from, "ft2", decimals),
        total,
    }
}

pub fn room_dimensions_from_value(v: &Value) -> RoomDimensions {
    let name = v.get("name").as_str().to_string();
    let text = |field: &str| match v.get(field) {
        Value::Str(s) => s.clone(),
        other => panic!(
            "room \"{}\": {} must be a decimal below 1000 with at most 3 decimal places, received \"{:?}\"",
            name, field, other
        ),
    };
    RoomDimensions { length: text("length"), width: text("width"), name: name.clone() }
}

pub fn floor_area_to_value(area: &FloorArea) -> Value {
    Value::obj(vec![
        ("roomAreas", Value::Arr(area.room_areas.iter().map(|a| Value::str(a)).collect())),
        ("total", Value::str(&area.total)),
        ("squareMetres", Value::str(&area.square_metres)),
        ("squareFeet", Value::str(&area.square_feet)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let rooms: Vec<RoomDimensions> = args[0].as_arr().iter().map(room_dimensions_from_value).collect();
    let decimals = match &args[2] {
        Value::Int(d) => *d,
        other => panic!("decimals must be a whole number from 0 to 12, received {:?}", other),
    };
    floor_area_to_value(&floor_area(&rooms, args[1].as_str(), decimals))
}