diff --git a/README.md b/README.md index 1742ff6..055ed39 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ x.getSI(); // { magnitude: 5, units: "kg/F" } ## Syntactic Sugar If you prefer, there is a much more compact way to initialize `Quantity` instances: using the `Q` template helper. This -is slightly less efficient and less capable, but far more readable and convenient in many cases. +is slightly less efficient, but far more readable and convenient in many cases. ```ts import { Q } from "@bradenmacdonald/quantity-math-js"; @@ -91,6 +91,12 @@ force.getSI(); // { magnitude: 34.2, units: "N" } force.multiply(Q`2 s^2`).toString(); // "68.4 kg⋅m" ``` +You can also call it as a function, which acts like "parse quantity string": + +```ts +const force = Q("34.2 kg m/s^2"); // new Quantity(34.2, {units: "kg m/s^2"}) +``` + ## Error/uncertainty/tolerance You can specify a "plus/minus" value (in the same units). Operations like addition and multiplication will preserve the @@ -99,7 +105,7 @@ relative uncertainty, etc.). ```ts const x = new Quantity(4.52, { units: "cm", plusMinus: 0.02 }); // 4.52±0.02 cm -const y = new Quantity(2.0, { units: "cm", plusMinus: 0.2 }); // 2±0.2 cm" +const y = Q`2±0.2 cm`; // Or use the Q string syntax const z = x.multiply(y); // z = xy = 9.04 ± 0.944 cm² z.get(); // { magnitude: 9.04, units: "cm^2", plusMinus: 0.944 } z.toString(); // "9.0±0.9 cm^2" (toString() will automatically round the output) diff --git a/q.ts b/q.ts index d108593..2d82889 100644 --- a/q.ts +++ b/q.ts @@ -11,9 +11,11 @@ export function Q(strings: string | ReadonlyArray, ...keys: unknown[]): fullString += strings[i + 1]; } } - const match = /([-+]?\d*\.?\d*)\s*(.*)/.exec(fullString); + const match = /([-+]?\d*\.?\d*)(\s*±\s*([\d\.]+))?\s*(.*)/.exec(fullString); if (match === null) throw new QuantityError(`Unable to parse Q template string: ${fullString}`); const magnitude = parseFloat(match[1]); - const units = match[2]; - return new Quantity(magnitude, { units }); + const plusMinusStr = match[3]; + const plusMinus = plusMinusStr ? parseFloat(plusMinusStr) : undefined; + const units = match[4]; + return new Quantity(magnitude, { units, plusMinus }); } diff --git a/quantity.ts b/quantity.ts index 4a5249b..9493734 100644 --- a/quantity.ts +++ b/quantity.ts @@ -171,6 +171,36 @@ export class Quantity { ); } + /** + * Is this Quantity approximately equal to another? + * + * Uses the plusMinus tolerance of this quantity value, or if no tolerance + * is set, it defaults to a relative tolerance of a hundredth of a percent. + * + * ```ts + * Q`60±5 W`.equalsApprox(Q`63 W`) // true + * Q`60±0.05 W`.equalsApprox(Q`60.1 W`) // false + * // Default tolerance of 0.01% when no ± plusMinus tolerance is specified: + * Q`100 W`.equalsApprox(Q`100.009 W`) // true + * Q`100 W`.equalsApprox(Q`100.02 W`) // false + * ``` + */ + public equalsApprox(other: Quantity): boolean { + if (!this.sameDimensionsAs(other)) return false; + + const absDifference = Math.abs(this.magnitude - other.magnitude); + + if (this.plusMinus !== undefined) { + return absDifference <= this.plusMinus; + } else { + if (this.magnitude === 0) { + return this.magnitude === other.magnitude; + } + const relativeTolerance = 0.0001; + return (absDifference / this.magnitude) <= relativeTolerance; + } + } + /** * Compare two Quantity values (that have the same dimensions) * diff --git a/tests/q.test.ts b/tests/q.test.ts index 69b75aa..8828bc6 100644 --- a/tests/q.test.ts +++ b/tests/q.test.ts @@ -65,4 +65,9 @@ Deno.test("Constructing Quantity instances with Q`...` template", async (t) => { ) { await check(short, new Quantity(-.05123, { units: "kg⋅m/s^2" })); } + + await check(`15.5 ± 0.2 kg`, new Quantity(15.5, { units: "kg", plusMinus: 0.2 })); + await check(`0.2±.01 g`, new Quantity(0.2, { units: "g", plusMinus: 0.01 })); + await check(`60±5 W`, new Quantity(60, { units: "W", plusMinus: 5 })); + await check(`+60±5.0000 W`, new Quantity(60, { units: "W", plusMinus: 5 })); }); diff --git a/tests/quantity.test.ts b/tests/quantity.test.ts index 0a7f7d8..9e0f8d4 100644 --- a/tests/quantity.test.ts +++ b/tests/quantity.test.ts @@ -13,7 +13,7 @@ Deno.test("Quantity instance equality", async (t) => { /** * Equality test helper. * Given two functions that create Quantities, make sure that the Quantities - * generated by the first function are equal to themsevles, but not equal to + * generated by the first function are equal to themselves, but not equal to * the Quantity generated by the second. */ const check = ( @@ -163,6 +163,67 @@ Deno.test("Sorting/comparing quantities", () => { ]); }); +Deno.test("equalsApprox", async (t) => { + const compareApprox = (q1: Quantity, q2: Quantity, result: boolean) => { + return t.step(`${q1.toString()} approx equals ${q2.toString()}: should be ${result}`, () => { + assertEquals(q1.equalsApprox(q2), result); + }); + }; + + // Same magnitude but different units are never equal: + const mag = 10; + await compareApprox(new Quantity(mag, { units: "W" }), new Quantity(mag, { units: "m" }), false); + await compareApprox(new Quantity(mag, { units: "kg" }), new Quantity(mag, { units: "lb" }), false); + + // Comparison with plusMinus values: + await compareApprox( + new Quantity(10, { units: "W", plusMinus: 0.1 }), + new Quantity(10.09, { units: "W" }), + true, // 10.09 falls within [10 - 0.1, 10 + 0.1] + ); + await compareApprox( + new Quantity(10, { units: "W", plusMinus: 0.1 }), + new Quantity(10.11, { units: "W" }), + false, // 10.11 does not fall within [10 - 0.1, 10 + 0.1] + ); + await compareApprox( + new Quantity(10, { units: "W", plusMinus: 1 }), + new Quantity(11, { units: "W" }), + true, // 11 falls within [10 - 1, 10 + 1] + ); + await compareApprox( + new Quantity(10, { units: "W", plusMinus: 1 }), + new Quantity(9, { units: "W" }), + true, // 9 falls within [10 - 1, 10 + 1] + ); + await compareApprox( + new Quantity(10, { units: "W", plusMinus: 1 }), + new Quantity(8.5, { units: "W" }), + false, // 8.5 does not fall within [10 - 1, 10 + 1] + ); + await compareApprox( + new Quantity(0.003, { units: "mg", plusMinus: 0.0005 }), + new Quantity(0.0034, { units: "mg" }), + true, + ); + + // Comparison with the default relative tolerance of 0.01%: + const u = { units: "m" }; + // Comparing 10m with some other similar values: + await compareApprox(new Quantity(10, u), new Quantity(10, u), true); + await compareApprox(new Quantity(10, u), new Quantity(10.0000001, u), true); + await compareApprox(new Quantity(10, u), new Quantity(10.001, u), true); + await compareApprox(new Quantity(10, u), new Quantity(10.005, u), false); + await compareApprox(new Quantity(10, u), new Quantity(9.995, u), false); + await compareApprox(new Quantity(10, u), new Quantity(8, u), false); + // Comparing 0.010m with some other similar values: + await compareApprox(new Quantity(0.010, u), new Quantity(0.010, u), true); + await compareApprox(new Quantity(0.010, u), new Quantity(0.010001, u), true); + await compareApprox(new Quantity(0.010, u), new Quantity(0.0099995, u), true); + await compareApprox(new Quantity(0.010, u), new Quantity(0.010002, u), false); + await compareApprox(new Quantity(0.010, u), new Quantity(0.009998, u), false); +}); + Deno.test("Adding quantities", async (t) => { await t.step(`cannot add units of different dimensions`, () => { const x = new Quantity(5, { units: "m" });