mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(integrations): round imported provider values through one normaliser
Fitbit, WHOOP, Oura, Polar and Withings each kept a private `parseFloat(n.toFixed(2))`, which mis-rounds halves on the double below the decimal. `importValue` keeps the same two places, rounds half away from zero on the printed decimal and returns the nearest double, so a value prints cleanly. A guard keeps `toFixed` and private `round2` helpers out of the provider directories; the Google Health mapper is the one listed exception until its next change.
This commit is contained in:
1 parent
e452dd62e2
commit
2b61762962
8 files changed
+188
-60
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { importValue } from "../import-value";
|
||||
|
||||
describe("importValue", () => {
|
||||
it("drops the binary noise provider arithmetic leaves behind", () => {
|
||||
expect(importValue(72.30000000000001)).toBe(72.3);
|
||||
expect(importValue(0.1 + 0.2)).toBe(0.3);
|
||||
// Withings: value * 10^unit.
|
||||
expect(importValue(78456 * Math.pow(10, -3))).toBe(78.46);
|
||||
expect(String(importValue(1234 * 0.01))).toBe("12.34");
|
||||
});
|
||||
|
||||
it("rounds halves on the decimal the value prints as, away from zero", () => {
|
||||
// `(1.005).toFixed(2)` is "1.00".
|
||||
expect(importValue(1.005)).toBe(1.01);
|
||||
expect(importValue(-0.125)).toBe(-0.13);
|
||||
expect(importValue(2.675)).toBe(2.68);
|
||||
});
|
||||
|
||||
it("keeps whole numbers, zero and non-finite values as they are", () => {
|
||||
expect(importValue(64)).toBe(64);
|
||||
expect(Object.is(importValue(-0.001), 0)).toBe(true);
|
||||
expect(importValue(0)).toBe(0);
|
||||
expect(importValue(Number.NaN)).toBeNaN();
|
||||
expect(importValue(1e-7)).toBe(0);
|
||||
expect(importValue(1e22)).toBe(1e22);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* The one rounding a provider value gets on its way in.
|
||||
*
|
||||
* Provider payloads arrive as binary floats that carry noise from the
|
||||
* provider's own arithmetic and from ours (Withings sends `value * 10^unit`,
|
||||
* seconds become minutes, metres become kilometres): `72.30000000000001`
|
||||
* stored as is shows up in a chart tooltip, an export and a delta. Every
|
||||
* provider client used to keep its own `parseFloat(n.toFixed(2))`, which
|
||||
* also mis-rounds on halves (`(1.005).toFixed(2)` is `"1.00"`, because the
|
||||
* double below 1.005 is what gets rounded).
|
||||
*
|
||||
* Two decimals, rounded half away from zero on the decimal the value prints
|
||||
* as, so the result is the double nearest to a two-place decimal and prints
|
||||
* cleanly. Two places is the precision every provider client already stored;
|
||||
* this keeps it. `src/__tests__/provider-rounding-guard.test.ts` keeps
|
||||
* `toFixed` out of the provider directories.
|
||||
*/
|
||||
export function importValue(n: number): number {
|
||||
if (!Number.isFinite(n)) return n;
|
||||
const magnitude = Math.abs(n);
|
||||
const text = String(magnitude);
|
||||
// A value printed in exponent form is below a millionth or beyond 1e21;
|
||||
// shifting its text would not parse, and plain arithmetic is exact enough.
|
||||
const hundredths = text.includes("e")
|
||||
? Math.round(magnitude * 100)
|
||||
: Math.round(Number(`${text}e2`));
|
||||
return (Math.sign(n) * hundredths) / 100 || 0;
|
||||
}
|
||||
Reference in new issue
Block a user