Functional Weave
Code in Python

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,
  };
}