DEV Community

Emir Mrkaljević
Emir Mrkaljević

Posted on

Why canned food unit prices lie about net and drained weight

A can of tuna on a German grocery page is labelled "Thunfisch in Olivenöl 185 g". It costs €1.99. If you divide the price by 185 g, you get €10.76 per kg. The same can also lists "Abtropfgewicht 140 g", the drained weight of the fish after the oil is poured off. Using that figure, the price is €14.21 per kg. The cheaper number is the can plus the oil. The dearer number is the food.

I ran into this while building ShrinkWatch EU, a free open-source extension that shows the price per kg or litre on EU grocery shops and warns when a pack gets smaller for the same price. It is listed on Firefox Add-ons.

Why the net weight is the wrong denominator

EU food labelling (Regulation 1169/2011) asks shops to print a drained net weight when a solid food sits in a liquid that you are not meant to eat as part of the product: oil, brine, or water. German shops call it Abtropfgewicht. French shops use poids égoutté, often in a short form such as "dont 140 g égoutté". Italian, Spanish, Portuguese and Dutch pages say peso sgocciolato, peso escurrido, peso escorrido, or uitlekgewicht.

The title of the page almost always uses the net weight of the full can, 185 g. A parser that only looks for "number plus unit" locks onto that first size and never reads the drained field. The unit price then makes one brand look cheaper than another because it put more oil in the can, not more fish.

A naive extra rule such as "any size next to the word égoutté" is worse. A recipe line like "Pour 200 g de pâtes égouttées, 500 g de tomates" is not a drained weight of pasta in a can. The 200 g is a cooking instruction.

What I match instead

I look for a known drained-weight phrase next to a size, on either side of it, and I also read a labelled field when the page has one (a definition list entry "Abtropfgewicht: 140 g", or a line "Poids égoutté : 140 g").

const DRAINED_WORD = '(?:abtropf-?\\s*gewicht|' +
  'poids(?:\\s+net)?\\s+[eé]goutt[eé]s?|[eé]goutt[eé]s?|' +
  'peso(?:\\s+netto)?\\s+sgocciolato|' +
  'peso\\s+escurrido|peso\\s+escorrido|' +
  'uitlekgewicht|drained(?:\\s+net)?\\s+weight)';

const DRAINED_SIZE = '(?:' + NUM + '\\s*(?:' + CONTAINER + '\\s*)?[x×*]\\s*)?' +
  NUM + '\\s*' + UNIT;

const DRAINED_THEN = new RegExp(
  DRAINED_WORD + '\\s*[:=]?\\s*' + DRAINED_SIZE + '(?![\\p{L}])',
  'iu'
);
const THEN_DRAINED = new RegExp(
  DRAINED_SIZE + '\\s*' + DRAINED_WORD,
  'iu'
);
Enter fullscreen mode Exit fullscreen mode

The full word list in the repo is longer (Nordic, Polish, Czech, Hungarian, Romanian and Greek forms as well). DRAINED_SIZE can itself be a multipack, so "Abtropfgewicht 3 x 85 g" is 255 g, not 85 g.

When a drained size is found, it replaces the net size only if the units match and the drained amount is not larger than the net. A typo that claims 400 g of drained tuna in a 185 g can is ignored.

function finish(size) {
  if (drained && (!size || (size.unit === drained.unit && drained.amount <= size.amount))) {
    return drained;
  }
  return size;
}
Enter fullscreen mode Exit fullscreen mode

On a product page, a labelled drained field wins over a net weight in the title:

const drained = labelledSize(doc, DRAINED_LABEL) ||
  parseDrainedSize(name) ||
  parseDrainedSize(desc) ||
  parseDrainedSize(og) ||
  parseDrainedSize(doc.title);

if (drained && drained.unit !== 'pc' &&
    (!size || (size.unit === drained.unit && drained.amount <= size.amount))) {
  size = drained;
}
Enter fullscreen mode Exit fullscreen mode
parseSize('Thunfisch in Öl 185 g (Abtropfgewicht 140 g)')
// { amount: 140, unit: 'g', count: 1, each: 140 }

parseSize('Thon 185 g dont 140 g égoutté').amount  // 140
parseSize('Tonno 185 g, peso sgocciolato 140 g').amount  // 140
parseSize('Mais 3 x 140 g (Abtropfgewicht 3 x 85 g)')
// { amount: 255, unit: 'g', count: 3, each: 85 }

unitPrice(1.99, parseSize('Thunfisch in Öl 185 g (Abtropfgewicht 140 g)')).value.toFixed(2)
// '14.21'
Enter fullscreen mode Exit fullscreen mode

And the cases that must stay as a net weight still do:

parseSize('Thunfisch in Öl 185 g').amount  // 185
parseSize('Pour 200 g de pâtes égouttées, 500 g de tomates').amount  // 200
Enter fullscreen mode Exit fullscreen mode

This shipped in ShrinkWatch EU 0.3.14. If you shop on Lidl, Carrefour or similar sites and want the unit price to use the food, not the liquid in the can, you can install it from Firefox Add-ons or get the source at github.com/emirmrkaljevic/shrinkwatch-eu.

I used an AI assistant to help draft this article.

Top comments (0)