On a German grocery page, a yogurt is labelled "4 Becher à 125 g". The pack costs €1.99. If you treat that as 125 g, you get €15.92 per kg. The real pack is 500 g, so the price is €3.98 per kg. That is a four-times error, and it shows up in French and Italian shops too: "4 pots de 125 g", "6 bottiglie da 1,5 l".
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 a simple "number plus unit" parse fails
The easy case is already handled: 4 x 125 g and 6x0,5l are a count, a times sign, and a size. German, French and Italian shops often write the container instead of the times sign:
Joghurt 4 Becher à 125 g
Yaourt nature 4 pots de 125 g
Acqua 6 bottiglie da 1,5 l
If the parser only looks for a size, it locks onto 125 g or 1,5 l and ignores the count. The unit price then uses one cup or one bottle, not the whole pack.
A naive fix such as "any number, then de or à, then a size" is worse. It would read "Gouda 48 de 400 g" as 48 packs of 400 g, or "Vin 2020 bouteilles de 75 cl" as 2020 bottles.
What I match instead
I only accept this form when a known container word sits between the count and the size. The joiners are the usual ones on those shops: à, de, da, of, po, x, and a few more.
const CONTAINER_OF = '(?:becher|beutel|dosen|flaschen|' +
'pots|bouteilles|canettes|sachets|' +
'vasetti|bottiglie|lattine|bustine|' +
'unidades|botellas|latas|' +
'zakjes|flessen|' +
'bottles|cans|bags|' +
'kubeczki|butelki|' +
'flaskor|burkar|flasker)';
const MULTI_OF = new RegExp(
'(?<![\\d.,])(\\d{1,3})\\s*' + CONTAINER_OF +
'(?![\\p{L}])\\.?\\s*(?:(?:à|á|a|je|zu je|zu|de|da|di|van|of|po)(?![\\p{L}])|[x×*])\\s*' +
NUM + '\\s*' + UNIT + '(?![\\p{L}])',
'iu'
);
The list of container words is longer in the repo. The negative lookarounds stop a word like "portionen" being eaten as "portion" plus a leftover letter, and they stop a year or a brand name from becoming a count.
When that pattern matches, the pack size is count * each:
const o = t.match(MULTI_OF);
if (o) {
const count = parseInt(o[1], 10);
const [base, f] = UNITS[o[3].toLowerCase()];
const each = sizeNum(o[2], o[3]) * f;
if (count >= 2 && count <= 100 && each > 0) {
return { amount: round(count * each), unit: base, count, each: round(each) };
}
}
parseSize('Joghurt 4 Becher à 125 g')
// { amount: 500, unit: 'g', count: 4, each: 125 }
parseSize('Acqua 6 bottiglie da 1,5 l')
// { amount: 9000, unit: 'ml', count: 6, each: 1500 }
unitPrice(1.99, parseSize('Joghurt 4 Becher à 125 g')).value.toFixed(2)
// '3.98'
And the cases that must stay as a single size still do:
parseSize('Gouda 48 de 400 g').amount // 400, not 19200
parseSize('Vin 2020 bouteilles de 75 cl').amount // 750
This shipped in ShrinkWatch EU 0.3.13. If you shop on Lidl, Carrefour or similar sites and want the unit price to use the whole pack, 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)