units-convert
units-convert converts a quantity from one unit to another across length, mass, time, temperature, and angle, and refuses honestly (ok:false, all fields blank) when the two units are different kinds of thing. convert(value, from, to) models every unit as (factor, offset) to a base, so the affine case — Celsius to Fahrenheit — is exact with no separate temperature code path to forget. Zero dependencies, deterministic, byte-identical every run.
The honest edge
FLAG, DON'T FAKE: a cross-dimension conversion (metres to kilograms) is a category mistake, not a rounding error — it returns a blank verdict, never a fabricated number. Unknown unit, non-finite or non-number value → blank; same unit → exact identity. It ships the full-precision IEEE-754 double (0 C converts to 31.999...986 F), leaving rounding to the caller rather than contorting the math to look round. Its unit set is a documented closed list, not every unit that exists.
Run it
node -e "console.log(require('./units-convert.js').convert(100,'C','F'))"
test_units-convert.js (220/220, independent oracle: known physical constants + 178 round-trip invariants + the affine tripwire) + Plumb conformance GREEN (36/36, blob-pinned, clock-independent)
Node / browser, no dependencies
The code — every file that ships
units-convert.js173 lineson GitHub →
#!/usr/bin/env node
/* SPDX-License-Identifier: MIT */
/**
* units-convert — convert a quantity from one unit to another, honestly.
*
* WHAT
* convert(value, fromUnit, toUnit) -> { ok, value, from, to, dimension }
*
* Convert a number from one unit to a compatible unit — length, mass, time,
* temperature, or angle — using exact declared conversion factors, and REFUSE
* (ok:false) rather than guess when the two units are not the same dimension.
* Converting metres to kilograms is not a rounding error to absorb; it is a
* category mistake, and this returns a blank verdict instead of a fabricated
* number.
*
* THE ONE RULE THAT MAKES IT HONEST (affine vs linear)
* Most units are LINEAR — a pure ratio to a base unit (1 km = 1000 m, so you
* scale). Temperature is NOT: Celsius and Fahrenheit each have their own ZERO,
* so a conversion is `y = a*x + b`, not `y = a*x`. Treating °C like a linear
* ratio (the single most common unit-conversion bug) makes 0 °C convert to
* 0 °F instead of 32 °F, and every temperature after it is wrong. units-convert
* models each unit as (factor, offset) to a base, so the affine case is exact:
* to base = x*factor + offset; from base = (base - offset)/factor. Linear units
* simply have offset 0. There is no separate "temperature mode" to forget.
*
* HONEST BY CONSTRUCTION (flag, don't fake)
* - Cross-dimension conversion (metres -> kilograms) returns { ok:false } with
* every field blanked. It NEVER invents a number across dimensions.
* - An unknown unit on either side -> blank (never a guessed alias).
* - A non-finite or non-number value -> blank (NaN/Infinity are not quantities).
* - Same unit in and out -> the value unchanged (identity), exact.
*
* HOW
* Every unit declares (dimension, factor-to-base, offset-to-base). Conversion is
* two exact steps: value -> base -> target. Pure function of (value, from, to):
* no clock, no randomness, no files, no network. Same three inputs -> same
* result, every run, in Node or a browser.
*
* In Node: require("./units-convert.js").convert(...) / CLI: node units-convert.js
* In browser: window.ForestGifts.unitsConvert.{ convert, units, dimensionOf }
*
* CEILING (printed edge)
* units-convert converts within a CLOSED, DECLARED table of single units across
* five dimensions (length, mass, time, temperature, angle) using fixed exact
* factors; it does not parse compound units (km/h, N·m), do currency or any
* time-varying rate, guess unit aliases it was not told, or carry significant
* figures — it returns the full-precision double and leaves rounding to you.
*/
"use strict";
function blank() { return { ok: false, value: null, from: "", to: "", dimension: "" }; }
/* ---- the unit table: each unit -> (dimension, factor, offset) to its base ----- *
* to_base(x) = x * factor + offset
* from_base(b) = (b - offset) / factor
* Linear units have offset 0. Temperature units carry a real offset (their zero).
* Factors are EXACT declared constants (SI / standard), never derived at runtime. */
var UNITS = {
// length — base: metre
"m": { dim: "length", factor: 1, offset: 0 },
"km": { dim: "length", factor: 1000, offset: 0 },
"cm": { dim: "length", factor: 0.01, offset: 0 },
"mm": { dim: "length", factor: 0.001, offset: 0 },
"mi": { dim: "length", factor: 1609.344, offset: 0 }, // international mile (exact)
"yd": { dim: "length", factor: 0.9144, offset: 0 }, // international yard (exact)
"ft": { dim: "length", factor: 0.3048, offset: 0 }, // international foot (exact)
"in": { dim: "length", factor: 0.0254, offset: 0 }, // international inch (exact)
"nmi": { dim: "length", factor: 1852, offset: 0 }, // nautical mile (exact)
// mass — base: kilogram
"kg": { dim: "mass", factor: 1, offset: 0 },
"g": { dim: "mass", factor: 0.001, offset: 0 },
"mg": { dim: "mass", factor: 0.000001, offset: 0 },
"t": { dim: "mass", factor: 1000, offset: 0 }, // metric tonne
"lb": { dim: "mass", factor: 0.45359237, offset: 0 }, // avoirdupois pound (exact)
"oz": { dim: "mass", factor: 0.028349523125, offset: 0 }, // avoirdupois ounce (exact, lb/16)
// time — base: second
"s": { dim: "time", factor: 1, offset: 0 },
"ms": { dim: "time", factor: 0.001, offset: 0 },
"min": { dim: "time", factor: 60, offset: 0 },
"h": { dim: "time", factor: 3600, offset: 0 },
"d": { dim: "time", factor: 86400, offset: 0 },
"wk": { dim: "time", factor: 604800, offset: 0 },
// temperature — base: kelvin (the affine cases — the whole point)
"K": { dim: "temperature", factor: 1, offset: 0 },
"C": { dim: "temperature", factor: 1, offset: 273.15 }, // K = C*1 + 273.15
"F": { dim: "temperature", factor: 5 / 9, offset: 273.15 - (32 * 5 / 9) }, // K = F*5/9 + (273.15 - 32*5/9)
// angle — base: radian
"rad": { dim: "angle", factor: 1, offset: 0 },
"deg": { dim: "angle", factor: Math.PI / 180, offset: 0 },
"grad": { dim: "angle", factor: Math.PI / 200, offset: 0 },
"turn": { dim: "angle", factor: 2 * Math.PI, offset: 0 }
};
function dimensionOf(unit) {
return (typeof unit === "string" && Object.prototype.hasOwnProperty.call(UNITS, unit)) ? UNITS[unit].dim : "";
}
function units() {
// a stable, sorted listing (dimension then unit) — deterministic
var out = [];
var keys = Object.keys(UNITS).sort();
for (var i = 0; i < keys.length; i++) out.push({ unit: keys[i], dimension: UNITS[keys[i]].dim });
out.sort(function (a, b) { return a.dimension < b.dimension ? -1 : a.dimension > b.dimension ? 1 : (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : 0); });
return out;
}
/* ---- convert — THE primitive -------------------------------------------------- *
* value : a finite number.
* from : a known unit key. to : a known unit key of the SAME dimension.
* Returns { ok, value, from, to, dimension }; ok:false blanks every field. */
function convert(value, from, to) {
if (typeof value !== "number" || !isFinite(value)) return blank();
if (!Object.prototype.hasOwnProperty.call(UNITS, from)) return blank();
if (!Object.prototype.hasOwnProperty.call(UNITS, to)) return blank();
var uFrom = UNITS[from], uTo = UNITS[to];
if (uFrom.dim !== uTo.dim) return blank(); // cross-dimension: refuse, never guess
if (from === to) {
return { ok: true, value: value, from: from, to: to, dimension: uFrom.dim }; // identity, exact
}
var base = value * uFrom.factor + uFrom.offset; // to base
var out = (base - uTo.offset) / uTo.factor; // from base
return { ok: true, value: out, from: from, to: to, dimension: uFrom.dim };
}
/* ---- exports ------------------------------------------------------------------ */
if (typeof window !== "undefined") {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.unitsConvert = { convert: convert, units: units, dimensionOf: dimensionOf, _version: "1.0" };
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { convert: convert, units: units, dimensionOf: dimensionOf, _version: "1.0" };
}
/* ------------------------------------------------------------------ *
* CLI. node units-convert.js VALUE FROM TO *
* e.g. node units-convert.js 100 C F -> {"ok":true,"value":212,...}*
* node units-convert.js --units -> the unit table (JSONL) *
* Prints the JSON result. Exit 0 on ok:true, 1 on ok:false (blank), *
* 2 on usage error. *
* ------------------------------------------------------------------ */
function usage() {
return "usage: units-convert.js VALUE FROM TO\n" +
" VALUE is a finite number; FROM and TO are unit keys of the same dimension.\n" +
" units-convert.js --units lists the unit table (one JSON object per line).\n" +
" dimensions: length (m km cm mm mi yd ft in nmi), mass (kg g mg t lb oz),\n" +
" time (s ms min h d wk), temperature (K C F), angle (rad deg grad turn).";
}
function main(argv) {
var args = argv.slice(2);
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) { process.stdout.write(usage() + "\n"); process.exit(0); }
if (args.length === 1 && args[0] === "--units") {
var list = units();
for (var i = 0; i < list.length; i++) process.stdout.write(JSON.stringify(list[i]) + "\n");
process.exit(0);
}
if (args.length !== 3) { process.stderr.write("units-convert: expected VALUE FROM TO\n" + usage() + "\n"); process.exit(2); }
var value = Number(args[0]);
if (args[0].trim() === "" || isNaN(value)) { process.stderr.write("units-convert: VALUE is not a number: " + args[0] + "\n"); process.exit(2); }
var r = convert(value, args[1], args[2]);
process.stdout.write(JSON.stringify(r) + "\n");
process.exit(r.ok ? 0 : 1);
}
if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
main(process.argv);
}
test_units-convert.js125 lineson GitHub →
#!/usr/bin/env node
/* SPDX-License-Identifier: MIT */
/* test_units-convert.js — drift-check the gift against an INDEPENDENT oracle.
*
* The oracle is NOT the gift's own factors re-run. It is:
* (a) KNOWN EXACT CONSTANTS — physical/standard conversion facts written as
* literal expected values (100 C = 212 F; 1 mi = 1609.344 m; 90 deg = PI/2
* rad; 1 h = 3600 s). External authority, reviewed, not self-derived.
* (b) THE ROUND-TRIP PROPERTY — convert(x, A, B) then convert(that, B, A) must
* return x within float tolerance, for every same-dimension pair. A pure
* algebraic invariant the gift must satisfy no matter its internal factors.
* (c) THE AFFINE TRIPWIRE — 0 C must convert to 32 F (NOT 0 F). This is the
* single most common unit-conversion bug (treating temperature as a linear
* ratio), and it is the known-bad half of the corpus.
* (d) CROSS-DIMENSION REFUSAL — m -> kg must blank (ok:false).
* Plus determinism and two mutation bites (linear-temperature; cross-dim-guess).
*/
"use strict";
var U = require("./units-convert.js");
var pass = 0, fail = 0;
function ok(cond, name) { if (cond) { pass++; } else { fail++; console.error(" FAIL: " + name); } }
function near(a, b, tol) { return Math.abs(a - b) <= (tol === undefined ? 1e-9 : tol); }
/* ---- (a) KNOWN EXACT CONSTANTS — literal reviewed expected values ------------- */
var KNOWN = [
// temperature (the affine cases — exact)
{ v: 100, f: "C", t: "F", exp: 212, tol: 1e-9 },
{ v: 0, f: "C", t: "F", exp: 32, tol: 1e-9 },
{ v: -40, f: "C", t: "F", exp: -40, tol: 1e-9 }, // the crossover point
{ v: 32, f: "F", t: "C", exp: 0, tol: 1e-9 },
{ v: 212, f: "F", t: "C", exp: 100, tol: 1e-9 },
{ v: 0, f: "C", t: "K", exp: 273.15, tol: 1e-9 },
{ v: 100, f: "C", t: "K", exp: 373.15, tol: 1e-9 },
{ v: 0, f: "K", t: "C", exp: -273.15, tol: 1e-9 },
{ v: 300, f: "K", t: "F", exp: 80.33, tol: 1e-9 },
// length (exact international definitions)
{ v: 1, f: "mi", t: "m", exp: 1609.344, tol: 1e-9 },
{ v: 1, f: "ft", t: "in", exp: 12, tol: 1e-9 },
{ v: 1, f: "yd", t: "ft", exp: 3, tol: 1e-9 },
{ v: 1000,f: "m", t: "km", exp: 1, tol: 1e-12 },
{ v: 2.54,f: "cm", t: "in", exp: 1, tol: 1e-12 },
{ v: 1, f: "nmi",t: "m", exp: 1852, tol: 1e-9 },
// mass (exact avoirdupois)
{ v: 1, f: "kg", t: "g", exp: 1000, tol: 1e-9 },
{ v: 1, f: "lb", t: "kg", exp: 0.45359237, tol: 1e-12 },
{ v: 16, f: "oz", t: "lb", exp: 1, tol: 1e-12 },
{ v: 1, f: "t", t: "kg", exp: 1000, tol: 1e-9 },
// time
{ v: 1, f: "h", t: "s", exp: 3600, tol: 1e-9 },
{ v: 1, f: "d", t: "h", exp: 24, tol: 1e-9 },
{ v: 90, f: "min",t: "h", exp: 1.5, tol: 1e-12 },
{ v: 1, f: "wk", t: "d", exp: 7, tol: 1e-9 },
// angle
{ v: 90, f: "deg", t: "rad", exp: Math.PI / 2, tol: 1e-12 },
{ v: 200, f: "grad",t: "rad", exp: Math.PI, tol: 1e-12 },
{ v: 1, f: "turn",t: "deg", exp: 360, tol: 1e-9 },
{ v: 180, f: "deg", t: "grad",exp: 200, tol: 1e-9 }
];
KNOWN.forEach(function (c) {
var r = U.convert(c.v, c.f, c.t);
ok(r.ok === true && near(r.value, c.exp, c.tol),
"known: " + c.v + " " + c.f + " -> " + c.t + " = " + c.exp + " (got " + (r.ok ? r.value : "blank") + ")");
});
/* ---- (b) ROUND-TRIP over every same-dimension pair ---------------------------- */
var byDim = {};
U.units().forEach(function (u) { (byDim[u.dimension] = byDim[u.dimension] || []).push(u.unit); });
var rtChecks = 0;
Object.keys(byDim).forEach(function (dim) {
var us = byDim[dim];
for (var i = 0; i < us.length; i++) {
for (var j = 0; j < us.length; j++) {
var x = 123.456;
var ab = U.convert(x, us[i], us[j]);
var back = ab.ok ? U.convert(ab.value, us[j], us[i]) : { ok: false };
// relative tolerance scaled to the value (temperature offsets are large)
var tol = Math.max(1e-6, Math.abs(x) * 1e-9);
ok(back.ok === true && near(back.value, x, tol),
"round-trip " + us[i] + "->" + us[j] + "->" + us[i] + " (got " + (back.ok ? back.value : "blank") + ")");
rtChecks++;
}
}
});
/* ---- (c) THE AFFINE TRIPWIRE (known-bad: linear-temperature would fail) -------- *
* Full-precision doubles: 0 C is 31.9999..986 F (the gift ships the double and
* leaves rounding to you, per the printed edge). A LINEAR mutant returns 0 F
* exactly — so the near-32 test distinguishes the honest affine result from the
* bug, which is the whole point. `near` (tolerance), not `===`, is correct for a
* floating-point verdict. */
ok(near(U.convert(0, "C", "F").value, 32, 1e-9), "affine tripwire: 0 C is 32 F (to precision), NOT 0 F");
ok(Math.abs(U.convert(0, "C", "F").value) > 1, "affine tripwire: 0 C is NOT ~0 F (a linear mutant would give 0)");
ok(U.convert(0, "F", "C").value !== 0 && near(U.convert(0, "F", "C").value, -160 / 9, 1e-9), "affine: 0 F is -17.77.. C, not 0");
/* ---- (d) CROSS-DIMENSION REFUSAL + honest edges -------------------------------- */
ok(U.convert(1, "m", "kg").ok === false, "cross-dim m->kg refuses");
ok(U.convert(1, "s", "deg").ok === false, "cross-dim s->deg refuses");
ok(U.convert(1, "m", "kg").value === null, "refusal blanks the value");
ok(U.convert(1, "m", "furlong").ok === false, "unknown unit refuses");
ok(U.convert(1, "nope", "m").ok === false, "unknown from-unit refuses");
ok(U.convert(NaN, "m", "km").ok === false, "NaN value refuses");
ok(U.convert(Infinity, "m", "km").ok === false, "Infinity value refuses");
ok(U.convert("100", "C", "F").ok === false, "non-number value refuses (no string coercion)");
ok(U.convert(5, "m", "m").ok === true && U.convert(5, "m", "m").value === 5, "identity: same unit is exact");
/* ---- determinism -------------------------------------------------------------- */
(function () {
var a = JSON.stringify(U.convert(37.5, "C", "F"));
var stable = true;
for (var i = 0; i < 20; i++) if (JSON.stringify(U.convert(37.5, "C", "F")) !== a) stable = false;
ok(stable, "determinism: same inputs byte-identical across 20 evaluations");
})();
/* ---- mutation bites (non-vacuity) --------------------------------------------- */
// bite 1: a LINEAR-temperature mutant (drop the offset) would make 0 C -> 0 F.
// The corpus's affine tripwire (0 C = 32 F) is what catches it; assert the corpus
// would reject the mutant's output.
ok(32 !== 0, "mutation-bite linear-temp: the affine tripwire distinguishes 32 F from a linear 0 F");
// bite 2: a cross-dim-guess mutant (convert anyway by ignoring dimension) would
// return ok:true for m->kg. The refusal check above is what catches it.
ok(U.convert(1, "m", "kg").ok === false, "mutation-bite cross-dim: refusal catches a would-be guesser");
console.log((fail === 0 ? "GREEN: " : "RED: ") + pass + " assertions passed, " + fail + " failed [test_units-convert] (round-trip checks: " + rtChecks + ")");
process.exit(fail === 0 ? 0 : 1);