use super::funejson::Value; use super::math_round_div::round_div; use super::math_round_float::round_float; 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) -> 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 = 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 { 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 = 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())) }