A macro calculator takes a calorie target and splits it into grams of carbs, protein and fat. Mine did this:
const carb = cals * split[0] / 100 / 4;
const prot = cals * split[1] / 100 / 4;
const fat = cals * split[2] / 100 / 9;
return `${Math.round(prot)} / ${Math.round(carb)} / ${Math.round(fat)} g`;
Ask it for 2,000 kcal on a 40/30/30 split and it says 150 / 200 / 67 g.
Multiply that back out — protein and carbs are 4 kcal a gram, fat is 9:
200 × 4 + 150 × 4 + 67 × 9 = 2,003
You asked for 2,000. It is not a big miss. It is a miss on the one thing the tool exists to do, and any user with a phone can find it, because adding up to the number you typed is the entire point of a split.
It was not one unlucky input either:
| target | plan | it said | that is actually |
|---|---|---|---|
| 2,000 | balanced | 150 / 200 / 67 | 2,003 |
| 2,000 | keto | 125 / 25 / 156 | 2,004 |
| 1,750 | low carb | 175 / 109 / 68 | 1,748 |
| 2,500 | high protein | 250 / 250 / 56 | 2,504 |
Every plan. Every time.
Why rounding each one cannot work
Because the three values are not independent. They have to satisfy
4c + 4p + 9f = calories
and Math.round does not know that. It rounds c to whatever is nearest to c, which is the right answer to a question nobody asked. The constraint is on the sum, and three separately-correct roundings do not add up to a correctly-rounded total.
This is the same family as a percentage table where the column has to total 100% and comes out at 99.9%. The usual fix there is largest-remainder: hand out the floors, then give the spare units to whoever was robbed most by rounding down.
That does not work here, and the reason is the interesting part.
The weights are 4, 4 and 9
In a percentage table every unit is worth the same, so you can move one from anywhere to anywhere. Here, moving a gram of carbs changes the total by 4, and moving a gram of fat changes it by 9. You cannot always close a gap of 3 kcal, because nothing in your inventory is worth 3.
So the order matters. Pick fat first, and pick it so that what is left over divides by 4 — because whatever is left has to be made up entirely of carbs and protein, and those only come in fours.
const want = kcal * split[2] / 100 / 9; // ideal fat, in grams
for (let f = Math.round(want) - 3; f <= Math.round(want) + 3; f++) {
const left = kcal - 9 * f;
if (left < 0 || left % 4 !== 0) continue; // no integer c,p can finish this
const rest = left / 4; // carbs + protein, in grams
const c = Math.round(rest * split[0] / (split[0] + split[1]));
const p = rest - c;
// ...
}
And the search never has to go far. 9 ≡ 1 (mod 4), so incrementing f by one shifts kcal - 9f by one modulo 4 — which means one fat value in every four consecutive integers satisfies the condition. A candidate always exists within two grams of the ideal. There is no failure case to handle, only a choice to make.
The first version was still wrong, and quietly
I originally took the nearest valid f and stopped. Every combination hit its calorie target exactly, the tests I had written passed, and I nearly shipped it.
Then I measured how far the grams had drifted from the ideal split, which is the other thing the tool is promising:
worst drift: 4.75 g on protein at 2,500 kcal keto
Keto puts 70% of the calories in fat, so fat is large, its rounding error is large in kcal, and all of that leftover lands on carbs and protein. Taking the first valid fat value meant taking whatever drift came with it.
There are usually several valid candidates in that ±3 window, and they do not cost the same. Scoring all of them by total drift and keeping the best brings the worst case down to 3.94 g — under 3% of that macro — while still landing exactly on the calorie target. Seven candidates is nothing to evaluate.
That is the general lesson and it is not about food:
Satisfying the constraint is necessary, not sufficient. The first solution that satisfies it is rarely the best one, and if you only assert on the constraint your tests will happily bless the worst member of the solution set.
Testing it
Sweeping, not sampling:
for (let cals = 800; cals <= 4000; cals += 25) {
for (const p of Object.values(PLANS)) {
const g = macroGrams(cals, p);
if (g.kcal !== cals) miss++;
}
}
516 combinations, and they all have to land exactly. Sampling round numbers would have been useless here — whether the bug shows depends on where the ideal fat value happens to fall relative to a multiple of four, and 2,000 kcal is exactly the kind of tidy input that could have gone either way.
The drift bound in the test is the measured number, not a guess:
// 3.94 g is what the algorithm actually holds across the swept range,
// on protein in a keto split. Four is that, rounded up.
check('no macro drifts more than 4 g from its ideal', worst <= 4, true);
Writing worst <= 10 because it passes is not a test, it is a formality. Writing worst <= 3 because it sounds tidy fails on an input that is perfectly fine. Measure the bound, then assert it.
And say the number
The last change was not code:
Protein 149 g
Carbohydrates 198 g
Fat 68 g
These add up to 2,000 kcal
The reader was going to multiply it out anyway. Better that the page does it first.
I build Utilorax, a set of free browser-based tools. This came out of the macro calculator, which now hands you three numbers that add up to the one you asked for.












