construction.concrete-volume@1.0.0
impl/typescript.ts
3,715 bytes · the TypeScript 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.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { roundFloat } from "./math_round_float.ts"; ← from math.round-float ^1.0.0 · built alongside by fune
import { type ConcreteElement, type ConcreteVolume } from "./construction_concrete_volume_types.ts";
const MAX_SAFE = 9007199254740991;
const MM3_PER_LITRE = 1000000;
const PI = 3.141592653589793;
function millimetres(element: number, name: string, value: number | null): number {
if (value === null || value === undefined || typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1000) {
throw new RangeError(`element ${element}: ${name} must be a finite number greater than 0 and at most 1000 metres, received ${value}`);
}
const mm = Math.round(roundFloat(value, 3) * 1000);
if (mm < 1) {
throw new RangeError(`element ${element}: ${name} must be at least 1 millimetre, received ${value}`);
}
return mm;
}
function tooLarge(): never {
throw new RangeError("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.
*/
export function concreteVolume(elements: readonly ConcreteElement[], wastageBasisPoints: number): ConcreteVolume {
if (elements.length === 0) {
throw new RangeError("elements must not be empty");
}
if (!Number.isInteger(wastageBasisPoints) || wastageBasisPoints < 0 || wastageBasisPoints > 10000) {
throw new RangeError(`wastageBasisPoints must be a whole number from 0 to 10000, received ${wastageBasisPoints}`);
}
const volumes: number[] = [];
let total = 0;
elements.forEach((el, i) => {
const n = i + 1;
if (!Number.isInteger(el.count) || el.count < 1) {
throw new RangeError(`element ${n}: count must be a whole number of at least 1, received ${el.count}`);
}
let one: number;
if (el.kind === "slab" || el.kind === "strip-footing" || el.kind === "pad") {
if (el.length === null || el.width === null || el.depth === null) {
throw new RangeError(`element ${n}: ${el.kind} needs length, width and depth`);
}
one = millimetres(n, "length", el.length) * millimetres(n, "width", el.width) * millimetres(n, "depth", el.depth);
// Beyond 2^53 a JavaScript product is no longer exact; the other
// languages could carry on, but all three must agree.
if (one > MAX_SAFE) tooLarge();
} else if (el.kind === "column") {
if (el.diameter === null || el.height === null) {
throw new RangeError(`element ${n}: column needs diameter and height`);
}
const d = millimetres(n, "diameter", el.diameter);
const h = millimetres(n, "height", el.height);
one = roundFloat(((PI * d * d) / 4) * h, 0);
if (one > MAX_SAFE) tooLarge();
} else {
throw new RangeError(`element ${n}: unknown kind "${el.kind}"`);
}
const volume = one * el.count;
if (volume > MAX_SAFE) tooLarge();
total += volume;
if (total > MAX_SAFE) tooLarge();
volumes.push(roundDiv(volume, MM3_PER_LITRE, "half-up") / 1000);
});
// Up to the whole litre, add the wastage, then up to the next 100 litres.
const litresUp = roundDiv(total, MM3_PER_LITRE, "up");
const tenths = roundDiv(litresUp * (10000 + wastageBasisPoints), 100 * 10000, "up");
return {
elementCubicMetres: volumes,
cubicMetres: roundDiv(total, MM3_PER_LITRE, "half-up") / 1000,
orderCubicMetres: tenths / 10,
};
}