Buy Me A Coffee

Imperial to Metric units conversion directly in Safari

Metrify takes Imperial units and converts them to Metric directly on the page using an action in the iOS share sheet. It works on iPhones and iPads.

Supported units: miles, mph, feet, inches, yards, ounces, pounds, °F, gallons, fl oz

Before

After

A live result from https://www.wired.com/2016/01/urban-aeronautics-airmule/

How it works

Metrify uses Run JavaScript on Webpage action from iOS Shortcuts. It looks for values in the Imperial measurement system and then replaces them with their Metric equivalents. It requires the Allow Untrusted Shortcuts setting to be on for you to use it.

Step 1

Allow Untrusted Shortcuts

This is necessary because I simply do not work for Apple :D

Step 2

Add Metrify to Shortcuts

After tapping on the Add button, you will see the code the shortcut executes and can add it to your library all the way at the bottom.

Step 3

Go to a page with imperial units and open the Share Sheet

Metrify lives in your Safari share sheet now, use it on any webpage

Step 4

Allow access to the page

This is the last security check to make sure you understand Metrify sees the whole content of the page. If you are unsure about this, the source code is below for your viewing. When you confirm (you have to do this only once), Metrify runs.

Security

Since Metrify has access to the contents of the page, it is important to know, no information is leaving your device. I could try and convince you, but my weekend is running up.
Therefore, here is the whole source code:

                
// 2011 Metrify bookmarklet by https://github.com/ikhramts where I got the RegExp and conversions from https://github.com/ikhramts/metrify/blob/master/metrify.js
// RegExp query selector https://stackoverflow.com/questions/37098405/javascript-queryselector-find-div-by-innertext/37098508

function getDOMMatches(selector,expression){
    //the reason to not use innerHTML on the whole body is to preserve event listeners
    return Array.from(document.querySelectorAll(selector)).filter(
        el => RegExp(expression).test(el.textContent) //all elements that match the expression
        && el.childNodes.length != el.children.length // childNodes include text nodes, children do not = if the length is different there is an unwrapped text in that element
        && Array.from(el.childNodes).filter(
            node => node.nodeType == Node.TEXT_NODE && node.textContent.trim() == ""
        ).length == 0 //for some reason there were sometimes empty text nodes that squeezed through the previous condition
    );
}

function metrify(element){
    if(typeof element === 'undefined') return false;

    var position;
    var i = 0; // just for safety

    while ((position = quantityPattern.exec(element.innerHTML)) !== null) {
        //["1,100-pound", "1,100", "1,100", ",100", ",", undefined, undefined, "-", "pound", index: 298...
        //["1,538.46 feet", "1,538.46", "1,538", ",538", ",", ".46", ".", " ", "feet", index: 70
        var wholeString = position[0];
        var numberString = position[1];
        var separator = position[7];
        var units = position[8];
        var hasMinus = false;

        var toDecimalPlaces = (typeof position[5] !== 'undefined') ? position[5].length-1 : 0;
        var separator = (separator == ' ') ? " " : ' ';

        var number = numberString.replaceAll(",","") * 1; // take out thousand separators and convert to number


        // Figure out which units we're working with.
        var intercept = 0;
        var slope = 1;
        var toUnits = "";
        
        if (/^(miles?|mi)$/i.test(units)) {
            slope = 1.609344;
            toUnits = "km";
            
        } else if (/^(foot|feet|ft)$/i.test(units)) {
            slope = 0.3048;
            toUnits = "m";
            
        } else if (/^(inch|inches|in|"|")$/i.test(units)) {
            slope = 2.54;
            toUnits = "cm";
            toDecimalPlaces = 2;
            
        } else if (/^(yards?|yd)$/i.test(units)) {
            slope = 0.9144;
            toUnits = "m";
            
        } else if (/^(ounces?|oz)$/i.test(units)) {
            slope = 28.3495231;
            toUnits = "g";
            
        } else if (/^(pounds?|lb)$/i.test(units)) {
            slope = 0.45359237;
            toUnits = "kg";
            
        } else if (/(°F|ºF|°F|fahrenheit|degrees? fahrenheit)$/i.test(units)) {
            if(element.innerHTML.substring(position.index-1, position.index) == "-"){
                number = number * -1;
                hasMinus = true;
            }

            intercept = -32 * 5 / 9;
            slope = 5 / 9;
            toUnits = "°C";

        } else if (/^(mph?|mi\/h)$/i.test(units)) {
            slope = 1.609344;
            toUnits = "km/h";

        } else if (/^(gallons?|gal)$/i.test(units)) {
            slope = 3.78541;
            toDecimalPlaces = 1;
            toUnits = "l";
        }
        else if (/^(fl? oz)$/i.test(units)) {
            slope = 29.5735;
            toDecimalPlaces = 0;
            toUnits = "ml";
        }

        var convertedNumber = parseFloat(number) * slope + intercept;
        var wholeString = (hasMinus) ? "-" + wholeString : wholeString; //the regex does not include the - in matches
        
        var convertedString = new Intl.NumberFormat('en-US').format(convertedNumber.toFixed(toDecimalPlaces)) + (separator ? separator : "") + toUnits;
        var formattedString = "" + convertedString + "";


        //insert back into element
        element.innerHTML = element.innerHTML.replace(wholeString,formattedString);
        changes += wholeString + "->" + formattedString + "\n";

        i++;
        if(i>3000) return false;
    }

    return true;
}


var quantityPattern =  /\b([\+\-]?(\d+|\d{1,3}(([\s,])\d{3})+)(([.])\d+)?)(\s*|-)?(miles?|mph|mi\/h|mi|foot|feet|ft|inch|inches|in|"|"|yards?|yd|ounces?|oz|pounds?|lb|°F|ºF|°F|fahrenheit|degrees? fahrenheit|gallons?|gal|fl? oz)\b/i;
var nodesWithMatches = getDOMMatches("*:not(head):not(html):not(script):not(body):not(title)",quantityPattern);
var changes = [];

if(nodesWithMatches.length){
    nodesWithMatches.forEach(element => {
        metrify(element);
    });
}
                
                             
            

Expectations

This is a weekend project. I am aware there could be improvements and abstractions. My goal was to learn how to work with iOS shortcuts and the limitations of website manipulations in iOS Safari. You are using Metrify at your own risk. That being said, if Metrify is helpful, consider buying me a coffee

Buy Me A Coffee
© 2020 Jan Beránek