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))
}