Give the food trend's figures, not only its rate

"Up about 40 g a week" is a rate with nothing to anchor it: it says the line
slopes without saying where it sits. The sentence now names both ends of the
fit — "roughly 280 g a day then, 400 g a day now" — which is the reading anyone
actually wants from a growth chart.

When the fit is not trustworthy it quotes the average instead, and says the
day-to-day variation is larger than any trend. That difference is the point.
The average is a measurement and survives the noise; the ends of the line are
the line's own output, and quoting them on a fit nobody should read would dress
a guess up as a reading. Everything rounds to 10 g for the same reason — "287 g
a day" would be false precision from four noisy points.

The wording moves into a pure foodTrendSentence() so those rules can be
checked, which is worth doing precisely because they are judgement rather than
arithmetic: the checks now pin that a clear climb gives both figures, a flat run
gives the average and no endpoints, a see-saw gives neither, and nothing is ever
quoted to the gram.
This commit is contained in:
Alexander Heldt
2026-09-21 20:43:50 +00:00
parent 668f1f039e
commit 7451650b6f
3 changed files with 67 additions and 22 deletions
+32
View File
@@ -5,6 +5,7 @@ import { load } from "./extract.mjs";
import { suite, eq, ok, report } from "./assert.mjs";
const app = load({ names: ["foodTrend"] });
const words = load({ names: ["foodTrendSentence"] });
// The 7/14/30 picker reaches the trend by deciding how many days weeklyData
// builds — there is no second mechanism, so this is the thing to hold still.
@@ -86,4 +87,35 @@ suite("marked days are skipped without shifting the line");
ok(t.clear, "…and it is still clear, not drowned by a false zero");
}
suite("what the sentence is allowed to say");
{
const say = (grams, opts) => words.foodTrendSentence(app.foodTrend(days(grams, opts)), 14);
const rising = say([200, 250, 300, 350, 400, 450, 0]);
ok(/up about 350 g a week/.test(rising), "a clear climb gives the rate");
ok(/200 g a day then/.test(rising) && /450 g a day now/.test(rising),
"…and the figures at each end of the line, so it is not only a rate");
ok(/the last 14 days/.test(rising), "…named against the window it was fitted over");
const steady = say([300, 302, 298, 301, 299, 300, 0]);
ok(/roughly steady/.test(steady), "a flat run is called steady");
ok(/averaging about 300 g a day/.test(steady),
"…and quotes the average, which is a measurement rather than model output");
ok(!/then/.test(steady) && !/ now\b/.test(steady),
"…but not fitted endpoints, which would dress up a line nobody should read");
const noisy = say([200, 500, 210, 480, 190, 520, 0]);
ok(/roughly steady/.test(noisy) && /variation is larger/.test(noisy),
"a see-saw says the variation beat the trend, rather than quoting a slope");
const none = say([300, 320, 310]);
ok(/Not enough complete days/.test(none) && /needs four/.test(none),
"too few days explains itself instead of leaving the chart bare");
// False precision would make a fit look like a reading.
ok(/\b\d*[05] g a day/.test(rising), "figures are rounded to 10 g, not quoted to the gram");
const falling = say([450, 400, 350, 300, 250, 200, 0]);
ok(/down about/.test(falling), "a clear fall says down");
}
export default report("food-trend");