use super::funejson::Value; use super::math_round_div::round_div; use super::math_round_float::round_float; // A 215 x 65 mm brick face plus a 10 mm joint is 225 x 75 mm of wall. const FACE_MM2: i64 = 225 * 75; // Mortar per mm2 of wall face, in mm3: each skin fills the joints around a // brick, (16875 - 215 x 65) / 16875 of the face, 102.5 mm deep, which is // 2378/135 mm; each collar joint between skins is 10 mm = 1350/135 mm. const SKIN_MORTAR: i64 = 2378; const COLLAR_MORTAR: i64 = 1350; const MORTAR_DIVISOR: i64 = 135; const MM3_PER_LITRE: i64 = 1_000_000; fn millimetres(value: f64, message: &str, max: i64) -> i64 { if !value.is_finite() || value <= 0.0 || value > max as f64 { panic!( "{} must be a finite number greater than 0 and at most {} metres, received {}", message, max, value ); } (round_float(value, 3) * 1000.0).round() as i64 } /// Bricks and mortar for a wall of UK standard bricks, openings subtracted. /// /// The count is the net face area over one brick's 225 x 75 mm share of it, /// 59.26 per m2 per skin (the trade's "60 a metre"), times the skins, plus /// wastage, rounded up. Mortar is the joint volume that geometry implies. /// Everything is whole millimetres and integer arithmetic. /// /// # Panics /// Panics on a dimension, skin count or wastage out of range, or openings /// larger than the wall. pub fn brick_count( wall_length: f64, wall_height: f64, openings: &[Opening], skins: i64, wastage_basis_points: i64, ) -> BrickCount { let length = millimetres(wall_length, "wallLength", 1000); let height = millimetres(wall_height, "wallHeight", 100); if !(1..=4).contains(&skins) { panic!("skins must be a whole number from 1 to 4, received {}", skins); } if !(0..=10000).contains(&wastage_basis_points) { panic!("wastageBasisPoints must be a whole number from 0 to 10000, received {}", wastage_basis_points); } let mut net = length * height; for (i, opening) in openings.iter().enumerate() { let w = millimetres(opening.width, &format!("opening {}: width", i + 1), 1000); let h = millimetres(opening.height, &format!("opening {}: height", i + 1), 1000); net -= w * h; } if net < 0 { panic!("openings are larger than the wall"); } let bricks = round_div(net * skins * (10000 + wastage_basis_points), FACE_MM2 * 10000, "up"); let per_mm2 = skins * SKIN_MORTAR + (skins - 1) * COLLAR_MORTAR; // Up to the whole litre first, then the wastage, so no product passes 2^53. let litres = round_div(net * per_mm2, MORTAR_DIVISOR * MM3_PER_LITRE, "up"); let mortar = round_div(litres * (10000 + wastage_basis_points), 10000, "up"); BrickCount { net_area_square_metres: net as f64 / 1_000_000.0, bricks, mortar_cubic_metres: mortar as f64 / 1000.0, } } pub fn brick_count_to_value(b: &BrickCount) -> Value { Value::obj(vec![ ("netAreaSquareMetres", Value::Float(b.net_area_square_metres)), ("bricks", Value::Int(b.bricks)), ("mortarCubicMetres", Value::Float(b.mortar_cubic_metres)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let openings: Vec = args[2] .as_arr() .iter() .map(|v| Opening { width: v.get("width").as_f64(), height: v.get("height").as_f64() }) .collect(); if let Value::Float(f) = &args[3] { panic!("skins must be a whole number from 1 to 4, received {}", f); } if let Value::Float(f) = &args[4] { panic!("wastageBasisPoints must be a whole number from 0 to 10000, received {}", f); } brick_count_to_value(&brick_count( args[0].as_f64(), args[1].as_f64(), &openings, args[3].as_i64(), args[4].as_i64(), )) }