Universal metric translator

Automatically converts imperial units to metric units

目前为 2017-07-05 提交的版本。查看 最新版本

// ==UserScript==
// @name        Universal metric translator
// @namespace   https://bennyjacobs.nl/userscripts/Universal-metric-translator
// @description Automatically converts imperial units to metric units
// @include     about:addons
// @include     http*
// @include     https*
// @version     2.1.1
// @grant       none
// ==/UserScript==

// Flags: global, insensitive
var createTransformationRegEx = function(unit) {
    return new RegExp(
        '\\s?'

      + '((?:\\d+(?:,\\d+)*)(?:\\.\\d+)?' // eg 9.23 or 23 or 0.34 or 2,204.6
      + '|\\d*(?:\\.\\d+)'  // eg .34 but not 0.34
      + '|(?:\\d+(?:,\\d+)*\\s)?\\d+(?:,\\d+)*/\\d+(?:,\\d+)*)' // 1 1/2 or 1/4, common with imperial

      + '(?:\\s*' + unit
      + '\\b(?!(\\s\\[|\\]))' // prevent infinite replacement
      // + '|\(?=\\s*(?:to|and|-)[\\d\\./\\s]+' + unit + '\\b)'
      + ')'
     ,"gi");
};

// Sources:
// https://en.wikipedia.org/wiki/Imperial_units
// https://en.wikipedia.org/wiki/Metre
// https://en.wikipedia.org/wiki/Square_metre
// https://en.wikipedia.org/wiki/Litre
var tranformationTable = [

    // Temperature
    {
        from: '(?:F|fahrenheit|fahrenheits|degrees F|degrees fahrenheit)',
        to: '℃',
        convert: function(fahrenheits){
            return ((fahrenheits - 32) / 1.8).toFixed(2);
        }
    },

    // Distance
    {
        from: 'thou',
        to: 'm',
        convert: 25.4 * 1e-6,
    }, {
        from: '(?:inch(?:es|e)?)',
        to: 'm',
        convert: 25.4 * 1e-3,
    }, {
        from: '(?:(?:feets?|foot))',
        to: 'm',
        convert: 0.3048,
    }, {
        from: '(?:yards?|yd)',
        to: 'm',
        convert: 0.9144,
    }, {
        from: 'chains?',
        to: 'm',
        convert: 20.1168,
    }, {
        from: '(?:furlongs?|fur)',
        to: 'm',
        convert: 201.168,
    }, {
        from: 'miles?',
        to: 'm',
        convert: 1.609344 * 1e3,
    }, {
        from: 'leagues?',
        to: 'm',
        convert: 4.828032 * 1e3,
    },

    // Maritime distances
    {
        from: '(?:fathoms?|ftm)',
        to: 'm',
        convert: 1.853184,
    }, {
        from: 'cables?',
        to: 'm',
        convert: 185.3184,
    }, {
        from: 'nautical\\smiles?', // Note: two backslashes as we are escaping a javascript string
        to: 'm',
        convert: 1.853184 * 1e3,
    },

    // Gunter's survey units (17th century onwards)
    {
        from: 'link',
        to: 'm',
        convert: 0.201168,
    }, {
        from: 'rod',
        to: 'm',
        convert: 5.0292,
    }, {
        from: 'chain',
        to: 'm',
        convert: 20.1168,
    },

    // Area
    {
        from: 'acres?',
        to: 'km²',
        convert: 4.0468564224,
    },

    // Volume
    {
        from: '(?:fluid ounces?|fl oz)',
        to: 'L',
        convert: 28.4130625 * 1e-3,
    }, {
        from: 'gill?',
        to: 'L',
        convert: 142.0653125 * 1e-3,
    }, {
        from: '(?:pints?|pt)',
        to: 'L',
        convert: 0.56826125,
    }, {
        from: 'quarts?',
        to: 'L',
        convert: 1.1365225,
    }, {
        from: 'gal(?:lons?)?',
        to: 'L',
        convert: 4.54609,
    },

    //Weight
    {
        from: 'grains?',
        to: 'g',
        convert: 64.79891 * 1e-3,
    }, {
        from: 'drachm',
        to: 'g',
        convert: 1.7718451953125,
    }, {
        from: '(?:ounces?|oz)',
        to: 'g',
        convert: 28.349523125,
   }, {
       // from: 'lbs?|pounds?', // Pound is ambiguous. It can be a currency. Therefore we don't touch it.
       // Actually, since it would be displayed as
       //   "It costs 1 pound [453.59 g]."
       //   the metric translation can just be ignored by the reader.
       //   I'm leaving it out anyways. lbs is usually used in written text anyways so it covers most cases.
       from: 'lbs?',
       to: 'g',
       convert: 453.59,
   }, {
        from: 'stones?',
        to: 'g',
        convert: 6.35029318  * 1e3,
    }, {
        from: 'quarters?',
        to: 'g',
        convert: 12.70058636 * 1e3,
    }, {
        from: 'hundredweights?',
        to: 'g',
        convert: 50.80234544 * 1e3,
    },
    //  A 'ton' might belong here, but there exist a metric ton and a imperial ton.
    
    // Qon commment: A metric ton is sometimes spelled metric tonne or just tonne though. 
    // https://en.wikipedia.org/wiki/Ton
];

tranformationTable.forEach(function (transformationRule) {
    transformationRule.regex = createTransformationRegEx(transformationRule.from);
});

var replaceSubstring = function(originalText, index, length, replacement) {
    var before_substring = originalText.substring(0, index);
    var after_substring = originalText.substring(index+length);
    return before_substring + replacement + after_substring;
};

function round_number(num, dec) {
    return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
}

// The transformText function is idempotent.
// Repeated calls on the output will do nothing. Only the first invocation has any effect.
// The input will be returned on repeated calls.
var transformText = function(text) {
    tranformationTable.forEach(function (transformationRule) {
       transformationRule.regex.lastIndex = 0;
        for(var match; match = transformationRule.regex.exec(text);) {

            // console.log(match, parseFloat(match[1], 10))
            var old_value, new_value;

            // if the number is written like 1 1/4 instead of 1.25 then:
            if(/\//.test(match[1])) {
                old_value = match[1].split(' ')
                if(old_value.length == 2)
                {
                    var a = old_value[1].split('/')
                    old_value[1] = parseFloat(a[0].replace(/,/g, ''), 10) / parseFloat(a[1].replace(/,/g, ''), 10)
                    old_value = parseFloat(old_value[0].replace(/,/g, ''), 10) + old_value[1]
                }
                else
                {
                    var a = old_value[0].split('/')
                    old_value = parseFloat(a[0].replace(/,/g, ''), 10) / parseFloat(a[1].replace(/,/g, ''), 10)
                }
            } else {
                old_value = parseFloat(match[1].replace(/,/g, ''), 10)
            }
            
            if(typeof transformationRule.convert == 'function') {
                new_value = transformationRule.convert(old_value);
            } else {
                new_value = old_value * transformationRule.convert;
            }
            
            var new_unit = transformationRule.to;
            if(new_unit === 'g' || new_unit === 'L' || new_unit === 'm')
            {
                if(new_value > 1e12) {
                    new_unit = 'T' + new_unit
                    new_value /= 1e12
                } else if (new_value > 1e9) {
                    new_unit = 'G' + new_unit
                    new_value /= 1e9
                } else if (new_value > 1e6) {
                    // if(new_unit === 'g') new_unit = 'tonne' else
                    new_unit = 'M' + new_unit
                    new_value /= 1e6
                } else if (new_value > 1e3) {
                    new_unit = 'k' + new_unit
                    new_value /= 1e3
                } else if (new_value < 1e-9) {
                    new_unit = 'p' + new_unit
                    new_value /= 1e-12
                } else if (new_value < 1e-6) {
                    new_unit = 'n' + new_unit
                    new_value /= 1e-9
                } else if (new_value < 1e-3) {
                    new_unit = 'µ' + new_unit
                    new_value /= 1e-6
                } else if (new_value < 1e-2) {
                    new_unit = 'm' + new_unit
                    new_value /= 1e-3
                } else if (new_value < 1 && (new_unit !== 'g')) {
                    new_unit = 'c' + new_unit
                    new_value /= 1e-2
                }
            }
            // function significantDigits(old, new) {
            //     old.replace(/^[^1-9]*/, '').replace(/\D/g, '').length
            // }
            new_value = round_number(new_value, 2)
            if(true) {
                var new_substring =
                      match[0]
                    + ' ['
                    + new_value
                    + " "
                    + new_unit
                    + ']'
            } else {
                var new_substring =
                      new_value
                    + " "
                    + new_unit
                    + ' ['
                    + match[0]
                    + ']'
            }

            text = replaceSubstring(text, match.index, match[0].length, new_substring );
            // Move the matching index past whatever we have replaced.
            // Note: The replacement can be shorter or longer.
            transformationRule.regex.lastIndex = transformationRule.regex.lastIndex + (new_substring.length - match[0].length);
        }
    });
    return text;
};

// conversation => convert. Because this has nothing to do with discussions.
// Rounding moved so it happens only immediatly before being inserted into
//  the document.
//  If it's done as the first step we lose a lot(!) of precision.
//  As an example 0.004 inches was rounded to 0, then converted to metric
//  (still 0) and then inserted. Extremely wrong.
//  Also 0.01497 miles gets rounded to 0.01 (33% less!) and then converted to
//  metric. The result is 0.01 * 1.609344 = 0.01609344, way more digits than
//  before we started! This looks extremely precise with 8 significant digits,
//  but it's actually only 1 since began by completely destroying our initial
//  number. Correct convertion should have the same number of significant
//  digits as the initial number (4). But some numbers, like 1 inch, might 
//  actually be exactly 1 inch, or 2.54 cm. Rounding 1 inch to 3 cm seems a 
//  bit wrong. It's unusual that 1 inch is written like "1.00 inches" even
//  if that precision is intended. So a flat rounding to 2 decimals after(!) 
//  choosing a good prefix (and scaling our number by the prefix) should work
//  for most cases.

var handleTextNode = function(textNode) {
    var transformedText = transformText(textNode.nodeValue);
    if(textNode.nodeValue != transformedText)
       textNode.nodeValue = transformedText;
};

// Travel the node(s) in a recursive fashion.
var walk = function(node) {
  var child, next;

  switch (node.nodeType) {
    case 1:  // Element
    case 9:  // Document
    case 11: // Document fragment
      child = node.firstChild;
      while (child) {
        next = child.nextSibling;
        walk(child);
        child = next;
      }
      break;
    case 3: // Text node
      handleTextNode(node);
      break;
    default:
      break;
  }
};

var MutationObserver = (window.MutationObserver || window.WebKitMutationObserver);
var observer = new MutationObserver(function (mutations) {
    mutations.forEach(function (mutation) {
        if(mutation.type == 'childList') {
            for (var i = 0; i < mutation.addedNodes.length; ++i) {
               walk(mutation.addedNodes[i]);
            }
        } else if (mutation.type == 'characterData') {
            handleTextNode(mutation.target);
        }
    });
});

observer.observe(document, {
    childList: true,
    characterData: true,
    subtree: true,
});

walk(document.body);