mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(apple-health): accept the valueless header row an ECG export opens with
Every real export begins with a bare `Name` when the name field is blank — Apple omits the trailing comma rather than writing an empty value. The parser required a comma on every header row, so it rejected the file on line 1, before it had read anything. A valueless header row is now skipped. It contributes no metadata entry rather than an empty-string one, because `Lead` and `Unit` presence is what switches the parser into the single-column waveform mode and a key with no value must not be able to trip that switch. The relaxation is confined to the header. Inside the single-column waveform a comma-less row is a sample and is read as one; inside the paired waveform every row is lead and voltage, so a missing comma there is still malformed.
This commit is contained in:
1 parent
8ecf7e5595
commit
19699ce8ce
2 files changed
+62
-1
No files matched your search
@@ -160,7 +160,24 @@ export async function parseAppleHealthEcgCsv(input: {
|
||||
}
|
||||
const pair = splitPair(line);
|
||||
if (!pair) {
|
||||
throw parserError("row is malformed");
|
||||
// A HEADER row whose value is empty is written without the trailing
|
||||
// comma. Every real export opens with a bare `Name` when the name
|
||||
// field is blank, which this parser used to reject on line 1 — before
|
||||
// it had read anything at all.
|
||||
//
|
||||
// Only the header section may do this. Inside the single-column
|
||||
// waveform a comma-less row is a sample and was handled above; inside
|
||||
// the paired waveform every row is `lead,voltage`, so a missing comma
|
||||
// there is still malformed.
|
||||
//
|
||||
// The row contributes NO metadata entry rather than an empty-string
|
||||
// one: `Lead` and `Unit` presence is what switches the parser into the
|
||||
// single-column mode below, and a valueless key must not be able to
|
||||
// trip that switch.
|
||||
if (inSamples) {
|
||||
throw parserError("row is malformed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const [key, value] = pair;
|
||||
if (!inSamples) {
|
||||
|
||||
@@ -427,3 +427,47 @@ describe("Apple Health HKElectrocardiogram CSV parser", () => {
|
||||
expect(spies.every((spy) => spy.mock.calls.length === 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The shapes real exports actually use.
|
||||
*
|
||||
* The fixtures above were written from the format's documentation rather than
|
||||
* from a file off a watch, and they drifted from it in four independent ways.
|
||||
* Each block below pins one of them, and — because the differences are dialect
|
||||
* rather than version — pins BOTH dialects side by side, so a later edit cannot
|
||||
* quietly trade one for the other.
|
||||
*/
|
||||
describe("Apple Health ECG CSV — real-export shapes", () => {
|
||||
async function parse(csv: string, maxSamples = 8) {
|
||||
const subject = await loadParserSubject();
|
||||
return subject.parseAppleHealthEcgCsv({
|
||||
memberName: "apple_health_export/electrocardiograms/ecg_2026-07-18.csv",
|
||||
stream: Readable.from([Buffer.from(csv)]),
|
||||
maxSamples,
|
||||
});
|
||||
}
|
||||
|
||||
describe("a header row with no value", () => {
|
||||
it("accepts the bare `Name` row every observed export opens with", async () => {
|
||||
// The name field is blank and Apple omits the comma entirely, so the
|
||||
// parser used to fail on line 1, before reading anything.
|
||||
await expect(
|
||||
parse(validCsv().replace("Name,Private Patient Name", "Name")),
|
||||
).resolves.toMatchObject({ samplingFrequency: 512 });
|
||||
});
|
||||
|
||||
it("keeps a valueless header row out of the metadata", async () => {
|
||||
// `Lead` + `Unit` presence is what switches the parser into the
|
||||
// single-column mode; a valueless key must not be able to trip it.
|
||||
await expect(
|
||||
parse(`${singleColumnCsv().replace("Unit,µV", "Unit")}`),
|
||||
).rejects.toThrow(/samples are missing|malformed/i);
|
||||
});
|
||||
|
||||
it("still rejects a comma-less row inside the paired waveform", async () => {
|
||||
await expect(
|
||||
parse(validCsv().replace("I,-0.002", "-0.002")),
|
||||
).rejects.toThrow(/malformed/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user