use super::funejson::Value; use super::units_convert::convert_units; 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::().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::().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 = 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 = 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)) }