Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

equalsApprox() method to compare using tolerance #2

Merged
merged 2 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions q.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ export function Q(strings: string | ReadonlyArray<string>, ...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 });
}
30 changes: 30 additions & 0 deletions quantity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand Down
5 changes: 5 additions & 0 deletions tests/q.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
});
63 changes: 62 additions & 1 deletion tests/quantity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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" });
Expand Down