carrefour per unit price sorter

Sorts items by price per unit (e.g. `per kg` or `per litre`) in Carrefour online shops in Poland.

// ==UserScript==
// @name           carrefour per unit price sorter
// @name:pl        carrefour sortowanie po cenie jednostki
// @namespace      cprn
// @license        GPLv3
// @match          https://*.carrefour.pl/*
// @grant          none
// @version        0.3
// @esversion      11
// @author         cprn
// @run-at         document-idle
// @date           2025-04-26 17:17:26
// @description    Sorts items by price per unit (e.g. `per kg` or `per litre`) in Carrefour online shops in Poland.
// @description:pl Sortuje produkty po cenie za jednostkę (np. za `kg` albo `l`) w sklepie internetowym polskiego Carrefoura.
// ==/UserScript==

// vim copy to clipboard:
// map ,r :wa<cr>gg"+yGzz

(function() {
    'use strict';

    const DEBUG = 1;

    function discoverClasses() {
        let priceTag = null;
        ['zł/1 l', 'zł/1 kg'].every(s => {
            let xpath = `//p[contains(., '${s}')]`;
            priceTag = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            return priceTag === null;
        });
        if (!priceTag) {
            console.error("Could not find priceTag");
            return null;
        }
        return {
            item: priceTag.parentNode.parentNode.parentNode.parentNode.classList[0],
            perUnitStrip: priceTag.parentNode.classList[0],
            mass: priceTag.parentNode.children[0].classList[0],
            perMass: priceTag.parentNode.previousSibling.children[0].children[0].classList[0],
        };
    }

    function perUnitPrice(e, classes) {
        let perUnit = e.getElementsByClassName(classes.perUnitStrip)[0]?.lastChild;
        let perMass = e.getElementsByClassName(classes.perMass)[0];
        let mass = e.getElementsByClassName(classes.mass)[0]?.lastChild;

        if (!perUnit || !mass) {
            console.debug('unknown price', e);
            return Infinity; // Treat unknowns as very expensive
        } else if (mass.innerText == '1 l' || mass.innerText == '1 kg') {
            var price = ''.concat(perMass.children[0].innerText, '.', perMass.children[1].innerText);
        } else {
            var price = perUnit.innerText.slice(0, -7).replace(',', '.');
        }

        return parseFloat(price);
    }

    function sortItems() {
        const classes = discoverClasses();
        if (!classes) {
            console.error("cprn: no classes found");
            return;
        }

        const items = Array.from(document.getElementsByClassName(classes.item));
        const parent = items[0]?.parentNode;
        if (!parent) {
            console.error("cprn: no parent found");
            return;
        }

        DEBUG && console.debug('cprn: first item', items[0])
        DEBUG && console.debug('cprn: items container', items[0].parentNode)
        DEBUG && console.debug('cprn: classes\n--------------------\n' +
            'item: ' + classes.item, document.getElementsByClassName(classes.item)[0], '\n' +
            'perUnitStrip: ' + classes.perUnitStrip, document.getElementsByClassName(classes.perUnitStrip)[0], '\n' +
            'mass: ' + classes.mass, document.getElementsByClassName(classes.mass)[0], '\n' +
            'perMass: ' + classes.perMass, document.getElementsByClassName(classes.perMass)[0], '\n' +
            'perUnit: ', perUnitPrice(items[0], classes )
        );

        console.log('cprn: solving world hunger...')
        items.sort((a, b) => perUnitPrice(a, classes) - perUnitPrice(b, classes));
        items.forEach(item => parent.appendChild(item));
        console.log('cprn: done')
    }

    function waitForPage() {
        const check = setInterval(() => {
            if (document.readyState === 'complete') {
                clearInterval(check);
                setTimeout(sortItems, 1000);
            }
        }, 300);
    }

    waitForPage();
})();