DEV Community

Vera Yang for BeGoodTool.com

Posted on

Why every "calories burned" calculator boils down to one formula (I found this out because my own copy button was lying to me)

I built a little MET-based calorie table — type in your weight and how long you exercised, get calories burned for 178 different activities, from mountain biking to sitting on the couch. While testing it I clicked the calorie cell to copy the number to my clipboard, and the value that landed in my clipboard didn't match the number on screen. Not a rounding difference — a completely different number. Chasing that down taught me more about how these calculators actually work than building the formula did.

The whole calculator is one line of math

Every "calories burned" tool, no matter how many activities it lists, comes down to the same MET (Metabolic Equivalent of Task) formula: calories = MET × weight in kg × duration in hours. 1 MET is defined as the energy your body burns at rest — about 3.5 ml of oxygen per kg per minute — and every activity in the table just gets a multiplier of that baseline (walking is around 3, running 8 km/h is around 8.3, competitive rowing can be 12+).

In the component, weight and duration are the only two reactive inputs, and they get collapsed into one computed value that every row in the table reuses:

const weight = ref(60);
const weightUnit = ref("Kilos");
const duration = ref(10);

const MET_computed = computed(() => {
  let kg = weightUnit.value == "pounds" ? weight.value / 2.2 : weight.value;
  return (kg * duration.value) / 60;
});
Enter fullscreen mode Exit fullscreen mode

MET_computed is just "weight in kg, converted to a per-hour basis" (dividing minutes by 60 turns duration into hours). Then for each of the 178 rows, the displayed calorie figure is simply MET_computed * item.MET — weight × time × that activity's multiplier. No per-activity logic, no special cases. One formula, reused 178 times with a different constant.

The number you see isn't the number you copy

Here's the bug that sent me digging. Each row has a click-to-copy cell for the calorie value:

<div
  class="tableGroup__item__cal"
  @click="copy(item.man)"
  :class="{ canCopy: item.man }"
>
  {{
    ((MET_computed * (item.MET * 10)) / 10)
      .toFixed(1)
      .replace(/\.?0+$/, "")
  }}
</div>
Enter fullscreen mode Exit fullscreen mode

The text rendered on screen is the live calculation — it changes the instant you change your weight or duration. But the click handler copies item.man instead, which is a static field baked into the dataset for every single exercise, like this one for mountain biking:

{
  idx: "01009",
  group: "bicycle",
  man: "595",
  feman: "425",
  desc_en: "bicycling, mountain, general",
  MET: "8.5",
}
Enter fullscreen mode Exit fullscreen mode

That 595 is a precomputed reference value — 8.5 MET × 70 kg × 1 hour = 595, the classic "average man" figure. feman (425) is the same math at 50 kg, the "average woman" reference. Those numbers exist because the underlying data was originally built as a static lookup table, and the live per-user calculation got layered on top of it later. If you leave the defaults at 60 kg and 10 minutes, the screen shows a small number based on your actual inputs, but the clipboard silently gives you the "70kg person exercising for an hour" reference instead. I left it as-is for now — fixing it means deciding whether man/feman are worth keeping around at all, since nothing else in the UI uses them.

The * 10 / 10 that does nothing (and the regex that does)

That calorie expression has another detail worth pointing at: item.MET * 10 / 10. Multiplying by 10 and immediately dividing by 10 is mathematically a no-op — the only real effect is that item.MET (stored as a string like "8.5" in the dataset) gets forced into a number a step earlier than it strictly needs to be, since JavaScript's * operator already coerces strings automatically. My best guess, looking at it now, is that it's a leftover from an earlier version that rounded to the nearest tenth before this logic got simplified, and only half of the original line got deleted. It's harmless, so it stayed.

The part actually doing work is the tail end: .toFixed(1).replace(/\.?0+$/, ""). toFixed(1) always produces one decimal place, so a clean result like 85 comes out as "85.0". The regex strips trailing zeros (and the decimal point if nothing but zeros follows), turning "85.0" back into "85" while leaving a genuine decimal like "92.1" untouched. Small detail, but it's the difference between a table that looks hand-typed and one that visibly reeks of floating-point math.

Where this falls apart

MET values aren't measured on you — they're population averages from a published reference table (the Compendium of Physical Activities), tested on a mix of people under lab conditions. Two people doing the exact same 30 minutes of "moderate cycling" can burn meaningfully different amounts of energy depending on fitness level, technique, terrain, and even how their body regulates heat that day. The calculator can only ever give you an estimate built from weight and duration — it has no way to know if you're a beginner grinding through this activity for the first time or someone who does it every day. It also converts pounds to kilograms with a flat /2.2, not the more precise 2.20462, so heavier inputs in pounds drift by a small but nonzero amount. None of that makes the number useless, it just means "482 calories" should be read as "roughly this ballpark," not a lab-verified figure.

I cleaned up the version I built for this into a small free tool if you want to look up MET values for something specific without hunting through a PDF: Exercise Calorie Burn Calculator. No sign-up, works for any weight or duration you throw at it.


Available in other languages

Top comments (0)