This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
Ranges In, A Bound You Can Trust Outtransform← all gifts

Interval-math

Point it at a JSONL stream of operations — each a {op, a:[lo,hi], b:[lo,hi]} pairing two intervals with + - * or / — and it computes the resulting interval, emitting {"lo":L,"hi":H} one per record in input order. An interval [lo,hi] means 'some real number in this range'; the result is GUARANTEED to contain every (x op y) for x in a, y in b (the containment property). Multiply uses all FOUR corner products, so [-2,3]*[-5,4] is [-15,12], not the naive [10,12]. Division by an interval strictly on one side of zero multiplies by the reciprocal interval; dividing by an interval that SPANS zero is refused (unbounded result). It drops into a pipe anywhere ranges/tolerances/error-bars need combining with an honest bound, without an interval-arithmetic library.

The honest edge
Does the four arithmetic ops (+ - * /) on real intervals. NOT a full interval library — no power/exponent, roots, or transcendental functions (sin/exp/log), no interval union/intersection/hull. NO outward-directed rounding: bounds are plain IEEE-754 doubles (a result whose true endpoint is not exactly representable is the nearest double, which may be a hair inside the guaranteed bound) — for certified soundness use a rational/directed-rounding library. Division by an interval containing zero is refused, not split.
Run it
printf '%s\n' '{"op":"*","a":[-1,2],"b":[3,4]}' | node interval-math.js # op + - * /; exit 0 ok | 2 input error test_interval-math.js (28/28: the CONTAINMENT invariant as a self-witnessing oracle (sample a 20-pt grid of x in a, y in b; every x op y must lie in the returned [lo,hi]) + TIGHTNESS (bounds == sampled extremes) over 10 op/interval cases + frozen hand goldens (all-four-corner sign-crossing multiply == [-15,12], subtraction flip, reciprocal division) + division-by-interval-spanning-zero refusals + input-honesty hard errors (unknown/missing op, mis-shaped/non-finite/lo>hi interval, non-JSON) + the naive-two-corner-multiply mutation bite) + out-of-band conformance conform_interval-math.cjs (18/18 GREEN, subprocess-driven, signed b77c4b3dbc9d8798) Zero dependencies, Node or browser, deterministic
The code — every file that ships
interval-math.js238 lineson GitHub →
#!/usr/bin/env node
/* interval-math.js — interval arithmetic (+ - * /) over a JSONL stream of operations.
   Dependency-free, deterministic. Runs in Node or a browser. MIT.

   WHAT IT IS. Give it a stream of operations — one JSON record per line (JSONL),
   each naming two intervals and an operator — and it computes the resulting interval
   and emits it. An interval [lo, hi] stands for "some real number between lo and hi,
   inclusive"; interval arithmetic computes the tightest interval GUARANTEED to
   contain the true result whatever the inputs actually were. Same stream in,
   byte-identical stream out, on every machine and every run. It is a TRANSFORM: one
   record in, one record out, in the same order.

   THE OPERATION RECORD. Each non-blank line is one JSON object:

       {"op": "+", "a": [1, 2], "b": [3, 4]}   -> {"lo":4,"hi":6}
       {"op": "*", "a": [-1, 2], "b": [3, 4]}  -> {"lo":-4,"hi":8}
       {"op": "/", "a": [1, 1], "b": [2, 4]}   -> {"lo":0.25,"hi":0.5}

   "op" is one of "+", "-", "*", "/". "a" and "b" are each a two-element array
   [lo, hi] of finite JSON numbers with lo <= hi. The output is {"lo":L, "hi":H}.

   THE CONTAINMENT GUARANTEE (the whole point). For every operation the result [L,H]
   is computed so that for ALL x in a and ALL y in b, (x op y) lies in [L, H]. This
   is the load-bearing invariant: interval arithmetic is only correct if it never
   loses a possible result.

     +   [a,b] + [c,d] = [a+c, b+d]
     -   [a,b] - [c,d] = [a-d, b-c]              (subtract the OTHER interval flipped)
     *   [a,b] * [c,d] = [min(P), max(P)]  where P = {a*c, a*d, b*c, b*d}
                                            (ALL FOUR corner products — a naive
                                             [a*c, b*d] is WRONG whenever a sign
                                             crosses zero)
     /   [a,b] / [c,d] = [a,b] * [1/d, 1/c]      (multiply by the reciprocal interval)
                                            ONLY when [c,d] does NOT contain 0 —

   DIVISION BY AN INTERVAL SPANNING ZERO IS A HARD ERROR. If b = [c,d] has c <= 0 <= d
   the reciprocal interval is unbounded (the result would be (-inf, +inf) or a split),
   so this gift REFUSES it (exit 2) rather than emit a bound it cannot honor. Dividing
   by an interval strictly on one side of zero (c>0 or d<0) is fine.

   NUMERIC HONESTY. Bounds are IEEE-754 doubles. Every endpoint must be a FINITE JSON
   number; a NaN/Infinity/overflow (e.g. 1e999 -> Infinity) or an interval with
   lo > hi is a HARD ERROR (exit 2) naming the line — never a silent skip. See the
   printed edge for the rounding caveat: this is EXACT for representable endpoints and
   representable results, but does NOT do outward-directed rounding, so a result whose
   true endpoint is not exactly representable in a double is stored as the nearest
   double (which may be a hair inside the mathematically-guaranteed bound). For
   verified/certified interval arithmetic use a rational or a directed-rounding
   library; this gift is the honest zero-dep representation, not a proof engine.

   USAGE
     printf '%s\n' '{"op":"+","a":[1,2],"b":[3,4]}' | node interval-math.js
     printf '%s\n' '{"op":"*","a":[-1,2],"b":[3,4]}' | node interval-math.js
     node interval-math.js ops.jsonl
     node interval-math.js --help

   Each non-blank line is one operation record; blank lines are skipped; a trailing
   \r (CRLF files) is trimmed. Output is one compact JSON object ({"lo":L,"hi":H})
   per record, one per line, in input order.

   Exit codes: 0 success · 2 input error (a line that is not a valid operation record,
   an unknown op, a non-finite or mis-ordered interval, or a division by an interval
   spanning zero). Always a clean one-line message on stderr, never a stack trace.

   Released under MIT. Its edge is printed in the README: this does the four
   arithmetic ops (+ - * /) on real intervals. It is NOT a full interval library — no
   power/exponent, roots, or transcendental functions (sin/exp/log), no interval
   union/intersection/hull, and NO outward-directed rounding (bounds are plain
   doubles). Division by an interval containing zero is refused, not split.
*/
"use strict";

var OPS = { "+": true, "-": true, "*": true, "/": true };

// Validate an interval value: a 2-element array of finite numbers with lo <= hi.
function checkInterval(v, which, lineNo) {
  if (!Array.isArray(v) || v.length !== 2) {
    throw new Error("line " + lineNo + " field " + JSON.stringify(which) +
      " must be a two-element [lo,hi] array");
  }
  var lo = v[0], hi = v[1];
  if (typeof lo !== "number" || typeof hi !== "number" || !isFinite(lo) || !isFinite(hi)) {
    throw new Error("line " + lineNo + " field " + JSON.stringify(which) +
      " endpoints must be finite numbers");
  }
  if (lo > hi) {
    throw new Error("line " + lineNo + " field " + JSON.stringify(which) +
      " has lo > hi (" + lo + " > " + hi + ")");
  }
  return [lo, hi];
}

// Compute one interval operation. Throws (clean, line-named) on divide-by-spanning-0.
function apply(op, a, b, lineNo) {
  var al = a[0], ah = a[1], bl = b[0], bh = b[1];
  switch (op) {
    case "+":
      return [al + bl, ah + bh];
    case "-":
      return [al - bh, ah - bl];
    case "*": {
      var p = [al * bl, al * bh, ah * bl, ah * bh];
      return [Math.min(p[0], p[1], p[2], p[3]), Math.max(p[0], p[1], p[2], p[3])];
    }
    case "/": {
      if (bl <= 0 && bh >= 0) {
        throw new Error("line " + lineNo +
          " divides by an interval spanning zero [" + bl + "," + bh + "] (unbounded result; refused)");
      }
      // reciprocal of [bl,bh] (which does not contain 0) is [1/bh, 1/bl]
      var rl = 1 / bh, rh = 1 / bl;
      var q = [al * rl, al * rh, ah * rl, ah * rh];
      return [Math.min(q[0], q[1], q[2], q[3]), Math.max(q[0], q[1], q[2], q[3])];
    }
    default:
      throw new Error("line " + lineNo + " unknown op " + JSON.stringify(op) +
        " (expected + - * /)");
  }
}

// Convert one parsed record -> {lo,hi}. Throws on a malformed record.
function computeRecord(rec, lineNo) {
  if (rec === null || typeof rec !== "object" || Array.isArray(rec)) {
    throw new Error("line " + lineNo + " is not an operation object");
  }
  var op = rec.op;
  if (typeof op !== "string" || !OPS[op]) {
    throw new Error("line " + lineNo + " has a missing or unknown op " +
      JSON.stringify(op) + " (expected + - * /)");
  }
  var a = checkInterval(rec.a, "a", lineNo);
  var b = checkInterval(rec.b, "b", lineNo);
  var r = apply(op, a, b, lineNo);
  return { lo: r[0], hi: r[1] };
}

// The public transform: JSONL text -> { lines: [json per record..], count }.
function transform(text) {
  var rawLines = String(text).split("\n");
  var out = [];
  var i, line, rec, result;
  for (i = 0; i < rawLines.length; i++) {
    line = rawLines[i];
    if (line.charCodeAt(line.length - 1) === 0x0d) line = line.slice(0, -1); // trim \r
    if (line.length === 0) continue; // blank line

    try { rec = JSON.parse(line); }
    catch (e) {
      throw new Error("line " + (i + 1) + " is not valid JSON: " + JSON.stringify(line.slice(0, 40)));
    }
    result = computeRecord(rec, i + 1);
    out.push(JSON.stringify(result));
  }
  return { lines: out, count: out.length };
}

/* ---- exports (browser + Node) ------------------------------------ */
if (typeof window !== "undefined") {
  window.ForestGifts = window.ForestGifts || {};
  window.ForestGifts.intervalMath = transform;
  window.ForestGifts.intervalOp = function (op, a, b) { return apply(op, checkInterval(a, "a", 1), checkInterval(b, "b", 1), 1); };
}
if (typeof module !== "undefined" && module.exports) {
  module.exports = { transform: transform, apply: apply, computeRecord: computeRecord, checkInterval: checkInterval };
}

/* ---- CLI (runs only when invoked directly, never on require) ------ */
function run(text) {
  var r = transform(text);
  var body = r.lines.length ? r.lines.join("\n") + "\n" : "";
  return { out: body, count: r.count };
}

function main(argv) {
  var args = argv.slice(2);
  if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
    process.stdout.write(
      "interval-math.js — interval arithmetic (+ - * /) over a JSONL stream.\n\n" +
      "  printf '%s\\n' '{\"op\":\"+\",\"a\":[1,2],\"b\":[3,4]}' | node interval-math.js   -> {\"lo\":4,\"hi\":6}\n" +
      "  printf '%s\\n' '{\"op\":\"*\",\"a\":[-1,2],\"b\":[3,4]}' | node interval-math.js  -> {\"lo\":-4,\"hi\":8}\n" +
      "  node interval-math.js ops.jsonl\n" +
      "  node interval-math.js --help\n\n" +
      "Each non-blank line is one operation object {op, a:[lo,hi], b:[lo,hi]}. The\n" +
      "result [L,H] is guaranteed to contain every (x op y) for x in a, y in b. Output\n" +
      "is one {\"lo\":L,\"hi\":H} per record, in input order.\n\n" +
      "Edge: the four arithmetic ops on real intervals. NOT a full interval library\n" +
      "(no powers/roots/transcendentals, no union/intersection, no outward rounding).\n" +
      "Division by an interval spanning zero is a hard error, not split.\n"
    );
    return 0;
  }

  var files = [];
  var i;
  try {
    for (i = 0; i < args.length; i++) {
      if (args[i].charAt(0) === "-" && args[i] !== "-") { throw new Error("unknown option " + args[i]); }
      else { files.push(args[i]); }
    }
  } catch (e) {
    process.stderr.write("interval-math: " + e.message + "\n");
    return 2;
  }

  function emit(text) {
    try {
      var r = run(text);
      process.stdout.write(r.out);
      return 0;
    } catch (e) {
      process.stderr.write("interval-math: " + e.message + "\n");
      return 2;
    }
  }

  if (files.length > 0) {
    var fs = require("fs");
    var text;
    try { text = fs.readFileSync(files[0], "utf8"); }
    catch (e) {
      process.stderr.write("interval-math: cannot read " + files[0] +
        " (" + (e.code === "EISDIR" ? "is a directory" : (e.code || "read error")) + ")\n");
      return 2;
    }
    return emit(text);
  }

  var chunks = [];
  process.stdin.on("data", function (d) { chunks.push(d); });
  process.stdin.on("end", function () {
    process.exitCode = emit(Buffer.concat(chunks).toString("utf8"));
  });
  return 0;
}

if (typeof require !== "undefined" && require.main === module) {
  process.exitCode = main(process.argv);
}
test_interval-math.js171 lineson GitHub →
#!/usr/bin/env node
/* test_interval-math.js — golden battery for the interval-math gift.

   Out-of-band and self-verifying. The oracle here is not an external library — it is
   the CONTAINMENT INVARIANT, which is self-witnessing:

     (1) CONTAINMENT. For each op and interval pair, sample a dense grid of x in a and
         y in b, compute (x op y) directly, and assert EVERY sampled result lies
         within the gift's returned [lo,hi]. A correct interval result cannot fail
         this; an under-wide (buggy) result will. This needs no external authority —
         the definition of interval arithmetic IS the test.

     (2) TIGHTNESS (a companion, so containment isn't passed by returning [-inf,inf]).
         For + - * / on continuous inputs the true result range endpoints are attained
         at interval corners, so the gift's [lo,hi] must EQUAL [min,max] of the sampled
         direct results to within a small epsilon — i.e. the interval is not just
         containing but tight.

     (3) FROZEN hand goldens — the sign-crossing multiply (the corner case), the
         subtraction flip, reciprocal division, and the division-spanning-zero refusal.

   A planted mutation (the bite, §BITE) MUST be caught: the naive multiply
   [a*c, b*d] (only two corners) is WRONG on a sign-crossing interval; the oracle
   (containment) must reject it.

   Run:  node test_interval-math.js   -> exit 0 GREEN / non-zero RED
*/
"use strict";
var im = require("./interval-math.js");

var pass = 0, fail = 0;
function ok(name, cond) {
  if (cond) { pass++; }
  else { fail++; console.log("  FAIL  " + name); }
}
function J(v) { return JSON.stringify(v); }
function giftOp(op, a, b) {
  var r = im.transform(J({ op: op, a: a, b: b }) + "\n");
  return JSON.parse(r.lines[0]); // {lo,hi}
}

// direct scalar op
function scalar(op, x, y) {
  if (op === "+") return x + y;
  if (op === "-") return x - y;
  if (op === "*") return x * y;
  if (op === "/") return x / y;
  throw new Error("bad op");
}
// sample N points across [lo,hi] inclusive
function grid(lo, hi, n) {
  if (lo === hi) return [lo];
  var out = [];
  for (var i = 0; i <= n; i++) out.push(lo + (hi - lo) * i / n);
  return out;
}

/* ---- (1)+(2) containment AND tightness over a spread of ops+intervals ----- */
(function () {
  var cases = [
    ["+", [1, 2], [3, 4]],
    ["-", [1, 2], [3, 4]],
    ["*", [-1, 2], [3, 4]],       // sign-crossing a
    ["*", [-3, -1], [-4, -2]],    // both negative
    ["*", [-2, 3], [-5, 4]],      // both sign-crossing (the hard corner case)
    ["*", [0, 5], [2, 3]],
    ["/", [1, 1], [2, 4]],
    ["/", [-6, 6], [2, 3]],       // numerator spans 0, denom positive
    ["/", [1, 2], [-4, -2]],      // denom strictly negative
    ["+", [-5, -5], [5, 5]],      // degenerate (points)
  ];
  var containFail = 0, tightFail = 0;
  cases.forEach(function (c) {
    var op = c[0], a = c[1], b = c[2];
    var got = giftOp(op, a, b);
    var xs = grid(a[0], a[1], 20), ys = grid(b[0], b[1], 20);
    var minR = Infinity, maxR = -Infinity, allIn = true;
    for (var i = 0; i < xs.length; i++) for (var j = 0; j < ys.length; j++) {
      var v = scalar(op, xs[i], ys[j]);
      if (v < minR) minR = v;
      if (v > maxR) maxR = v;
      if (v < got.lo - 1e-9 || v > got.hi + 1e-9) allIn = false;
    }
    if (!allIn) { containFail++; console.log("    CONTAIN " + op + " " + J(a) + J(b) + " -> " + J(got)); }
    // tightness: the gift's bounds equal the sampled extremes to epsilon
    if (Math.abs(got.lo - minR) > 1e-6 || Math.abs(got.hi - maxR) > 1e-6) {
      tightFail++; console.log("    TIGHT " + op + " " + J(a) + J(b) + " gift=" + J(got) + " sampled=[" + minR + "," + maxR + "]");
    }
  });
  ok("containment: every sampled x∘y lies in the gift interval (" + cases.length + " cases)", containFail === 0);
  ok("tightness: gift bounds == sampled extremes (" + cases.length + " cases)", tightFail === 0);
})();

/* ---- (3) frozen hand goldens --------------------------------------------- */
function G(name, op, a, b, lo, hi) {
  var g = giftOp(op, a, b);
  ok(name, g.lo === lo && g.hi === hi);
}
G("add", "+", [1, 2], [3, 4], 4, 6);
G("sub flips the other interval", "-", [1, 2], [3, 4], -3, -1);
G("mul sign-crossing (all four corners)", "*", [-1, 2], [3, 4], -4, 8);
G("mul both negative", "*", [-3, -1], [-4, -2], 2, 12);
G("mul both sign-crossing", "*", [-2, 3], [-5, 4], -15, 12);
G("div by positive interval", "/", [1, 1], [2, 4], 0.25, 0.5);
G("div numerator spans zero", "/", [-6, 6], [2, 3], -3, 3);
G("div by strictly-negative interval", "/", [1, 2], [-4, -2], -1, -0.25);

/* ---- (4) division-by-spanning-zero is a HARD ERROR ------------------------ */
function throws(name, fn) {
  var t = false;
  try { fn(); } catch (e) { t = true; }
  ok(name, t);
}
throws("div by [-1,1] (spans 0) refused", function () { giftOp("/", [1, 2], [-1, 1]); });
throws("div by [0,2] (touches 0 at lo) refused", function () { giftOp("/", [1, 2], [0, 2]); });
throws("div by [-2,0] (touches 0 at hi) refused", function () { giftOp("/", [1, 2], [-2, 0]); });
throws("div by [0,0] refused", function () { giftOp("/", [1, 2], [0, 0]); });

/* ---- (5) transform shape + input honesty --------------------------------- */
(function () {
  var text = '{"op":"+","a":[1,2],"b":[3,4]}\n{"op":"*","a":[0,1],"b":[2,2]}\n';
  var r = im.transform(text);
  ok("one output per input, in order", J(r.lines) === J(['{"lo":4,"hi":6}', '{"lo":0,"hi":2}']));
  ok("count == records", r.count === 2);
})();
ok("blank lines skipped", im.transform('{"op":"+","a":[1,1],"b":[1,1]}\n\n').count === 1);
ok("CRLF trimmed", J(im.transform('{"op":"+","a":[1,1],"b":[2,2]}\r\n').lines) === J(['{"lo":3,"hi":3}']));
(function () {
  var t = '{"op":"*","a":[-2,3],"b":[-5,4]}\n';
  ok("determinism: two runs identical", im.transform(t).lines.join() === im.transform(t).lines.join());
})();

throws("unknown op is a hard error", function () { giftOp("^", [1, 2], [3, 4]); });
throws("missing op is a hard error", function () { im.transform('{"a":[1,2],"b":[3,4]}\n'); });
throws("interval not a 2-array", function () { im.transform('{"op":"+","a":[1,2,3],"b":[3,4]}\n'); });
throws("interval lo>hi is a hard error", function () { im.transform('{"op":"+","a":[2,1],"b":[3,4]}\n'); });
throws("non-finite endpoint (1e999->Infinity) is a hard error", function () { im.transform('{"op":"+","a":[1e999,1e999],"b":[3,4]}\n'); });
throws("non-object record is a hard error", function () { im.transform('[1,2]\n'); });
throws("non-JSON line is a hard error", function () { im.transform('not json\n'); });
ok("hard error names the line", (function () {
  try { im.transform('{"op":"+","a":[1,1],"b":[2,2]}\n{"op":"?","a":[1,1],"b":[2,2]}\n'); return false; }
  catch (e) { return /line 2/.test(e.message); }
})());

/* ---- §BITE — the naive-multiply mutation must be caught ------------------- */
/* The classic wrong build is mul = [a*c, b*d] (only 2 corners). It agrees with the
   correct gift when signs don't cross, but is WRONG on a sign-crossing interval. We
   build that mutant and assert containment (the oracle) rejects it on a crossing
   case — so a regression to naive-corners fails test (1). */
(function () {
  function naiveMul(a, b) { return { lo: a[0] * b[0], hi: a[1] * b[1] }; }
  var a = [-2, 3], b = [-5, 4];
  var gift = giftOp("*", a, b);            // correct: [-15, 12]
  var mutant = naiveMul(a, b);             // naive: [10, 12]  (WRONG, misses -15 and lower)
  // containment check on the mutant: does a sampled product escape it?
  var xs = grid(a[0], a[1], 20), ys = grid(b[0], b[1], 20);
  var mutantEscapes = false, giftContains = true;
  for (var i = 0; i < xs.length; i++) for (var j = 0; j < ys.length; j++) {
    var v = xs[i] * ys[j];
    if (v < mutant.lo - 1e-9 || v > mutant.hi + 1e-9) mutantEscapes = true;
    if (v < gift.lo - 1e-9 || v > gift.hi + 1e-9) giftContains = false;
  }
  ok("BITE: naive-corners mutant fails containment where the gift holds",
     mutantEscapes === true && giftContains === true &&
     gift.lo === -15 && gift.hi === 12);
})();

/* ---- report --------------------------------------------------------------- */
console.log("");
console.log("interval-math battery: " + pass + " passed, " + fail + " failed");
process.exit(fail === 0 ? 0 : 1);
Take the whole folder → MIT Zero dependencies, Node or browser, deterministic