買ったアイテムの合計金額を表示するGreasemonkeyスクリプト

インストールすると日付の横に「合計金額を計算」というリンクが現れる。それをクリックすると、その日の日記に含まれるアイテムの金額の合計が計算される。

// ==UserScript==
// @name           How much money do I waste?
// @namespace      http://d.hatena.ne.jp/nozom/
// @include        http://d.hatena.ne.jp/*
// ==/UserScript==

(function(){
    var item_price = {};
    var item_title = {};
    var req = {};

    function number_to_human_readable(n) {
        var num = new String(n);
        while (num != (num = num.replace(/^(-?\d+)(\d{3})/, "$1,$2"))) {};
        return num;
    }

    function getElementsByXPath(xpath, doc, context) {
        if (doc == null) doc = document;
        if (context == null) context = doc;
        var it = doc.evaluate(xpath, context, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        var nodes = new Array();

        var node;
        while ((node = it.iterateNext()) != null) {
            nodes.push(node);
        }
        return nodes;
    }

    function get_asin_from_link(link) {
        if (link.match(/\/(ASIN|asin|asininfo)\/([^\/]+)/))
            return RegExp.$2;
        if (link.match(/\/gp\/product\/([^\/]+)/))
            return RegExp.$1;
        if (link.match(/\/dp\/product\/([^\/]+)/))
            return RegExp.$1;

        return null;
    }

    function get_title(doc) {
        var title = doc.getElementsByTagName('title')[0];
        if (title) {
            return title.textContent;
        } else {
            return null;
        }
    }

    function get_amazon_price(doc) {
        var nodes = doc.getElementsByTagName('li');
        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].className == 'price' &&
                nodes[i].textContent.match(/Amazon価格:/)) {
                var node = nodes[i].getElementsByTagName('strong')[0];
                if (node) {
                    var price = node.textContent.replace(",", "");
                    return parseInt(price);
                }
            }
        }
        return null;
    }

    var handlers = [];
    function add_handler(handler) {
        handlers.push(handler);
    }

    function call_handlers() {
        var new_handlers = [];
        for (var i = 0; i < handlers.length; i++) {
            var handler = handlers[i];
            if (!handler()) {
                new_handlers.push(handler);
            }
        }
        handlers = new_handlers;
    }

    function new_request(asin) {
        req[asin] = GM_xmlhttpRequest({
            method: 'GET',
            url: "http://d.hatena.ne.jp/asin/" + asin,
            onload: function(req) {
                var d = document.createElement('div');
                d.innerHTML = req.responseText;
                item_price[asin] = get_amazon_price(d);
                item_title[asin] = get_title(d);
                req[asin] = null;
                call_handlers();
            },
        });
    }

    function update_label(a, n) {
        a.textContent = "合計金額を計算中... (残り: " + n + ")";
    }

    function create_result_node(total_price, n, errors) {
        var span = document.createElement('span');
        span.style.fontWeight = 'normal';
        span.style.fontSize = '80%';
        span.style.float = 'right';

        var text = "合計金額 (" + n + "点): " + number_to_human_readable(total_price) + "円";
        span.appendChild(document.createTextNode(text));
        if (errors.length > 0) {
            span.appendChild(document.createTextNode(" (エラー: "));
            for (var i = 0; i < errors.length; i++) {
                if (i > 0) {
                    span.appendChild(document.createTextNode(", "));
                }
                var asin = errors[i];
                var a = document.createElement('a');
                a.title = item_title[asin];
                a.href = "http://d.hatena.ne.jp/asin/" + asin;
                a.textContent = asin;
                span.appendChild(a);
            }
            span.appendChild(document.createTextNode(")"));
        }
        return span;
    }

    function on_calc_button_clicked(ev) {
        var a = ev.target;
        var divDay = a.parentNode.parentNode;
        var nodes = divDay.getElementsByTagName("a");

        var asinHash = {};
        for (var i = 0; i < nodes.length; i++) {
            var asin = get_asin_from_link(nodes[i].href);
            if (asin) {
                asinHash[asin] = 1;
            }
        }

        var n = 0;
        for (var asin in asinHash) {
            n++;
        }
        update_label(a, n);

        add_handler(function() {
            var n = 0;
            var errors = [];
            var wait = 0;
            var total_price = 0;
            for (var asin in asinHash) {
                if (typeof item_price[asin] == 'undefined') {
                    GM_log("waiting: " + asin);
                    wait++;
                } else if (item_price[asin] == null) {
                    errors.push(asin);
                } else {
                    n++;
                    total_price += item_price[asin];
                }
            }
            if (wait > 0) {
                update_label(a, wait);
                return false;
            }

            var span = create_result_node(total_price, n, errors);
            a.parentNode.appendChild(span);
            a.parentNode.removeChild(a);

            return true;
        });

        var asinList = [];
        for (var asin in asinHash) {
            if (!item_price[asin] && !req[asin]) {
                new_request(asin);
            }
        }

        ev.preventDefault();
        return;
    }

    function add_calc_button(h2) {
        var a = document.createElement("a");
        a.href = "#";
        a.appendChild(document.createTextNode("合計金額を計算"));
        a.style.fontWeight = 'normal';
        a.style.fontSize = '80%';
        a.style.float = 'right';
        a.addEventListener('click', on_calc_button_clicked, false);
        h2.appendChild(a);
    }

    var divDayHeadings = getElementsByXPath('//div[@class="hatena-body"]//div[@id="days"]/div[@class="day"]/h2');
    for (var i = 0; i < divDayHeadings.length; i++) {
        add_calc_button(divDayHeadings[i]);
    }
})()