views:

346

answers:

3

What is the best/most elegant way to abstract out the conversion of units of measures in the client, based on a user-preferred unit of measure setting?

For example, let's say user A's preferred unit of measure is "metric", while user's B's preference is "imperial".

Now lets say I've calculated the area of something in meters squared. When I go to display the value I need to use different conversion factors for each user (eg, "1 meter = 1.09361 yards"). Or say I've calculated the fluid volume in mL. User B's view will get calculated using the conversion "236.588237 mL = 1 US cup".

Is there an existing javascript library that anyone here knows about that handles these trivial UOM conversions?

A: 

How many different units are being converted? It sounds as though you are writing an specific application which I'm taking as you only need a few different types of conversion (area, volume, etc)

If this is the case, it might be faster just to look up the conversion for those that you need and code them into a class for yourself. Loading a complete javascript library just to perform a few specific multiplications could be overkill.

Paulo
There are many apps that make up the system, each calculating and showing different values (temperature, volume, lengths, areas, etc). I want to not write specific conversion routines in each and just have one library (as bloated as that seems) that I can rely on from within any app.
JPot
+3  A: 

Here's a little script I threw together just for the heck of it. It handles all the SI conversions for grams, bytes, meters and liters, and also I've added ounces and pounds as an example of non-SI units. To add more, you'll need to:

  1. Add the base type to the "units" list for items that follow SI or
  2. Add the conversion ratios for items that don't follow SI

Usage:

$u(1, 'g').as('kg').val(); // converts one gram to kg

You can get the value out with .val(), a string representation using .toString() or the full details via .debug()

(function () {
    var table = {};

    window.unitConverter = function (value, unit) {
        this.value = value;
        if (unit) {
            this.currentUnit = unit;
        }
    };
    unitConverter.prototype.as = function (targetUnit) {
        this.targetUnit = targetUnit;
        return this;
    };
    unitConverter.prototype.is = function (currentUnit) {
        this.currentUnit = currentUnit;
        return this;
    };

    unitConverter.prototype.val = function () {
        // first, convert from the current value to the base unit
        var target = table[this.targetUnit];
        var current = table[this.currentUnit];
        if (target.base != current.base) {
            throw new Error('Incompatible units; cannot convert from "' + this.currentUnit + '" to "' + this.targetUnit + '"');
        }

        return this.value * (current.multiplier / target.multiplier);
    };
    unitConverter.prototype.toString = function () {
        return this.val() + ' ' + this.targetUnit;
    };
    unitConverter.prototype.debug = function () {
        return this.value + ' ' + this.currentUnit + ' is ' + this.val() + ' ' + this.targetUnit;
    };
    unitConverter.addUnit = function (baseUnit, actualUnit, multiplier) {
        table[actualUnit] = { base: baseUnit, actual: actualUnit, multiplier: multiplier };
    };

    var prefixes = ['Y', 'Z', 'E', 'P', 'T', 'G', 'M', 'k', 'h', 'da', '', 'd', 'c', 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y'];
    var factors = [24, 21, 18, 15, 12, 9, 6, 3, 2, 1, 0, -1, -2, -3, -6, -9, -12, -15, -18, -21, -24];
    // SI units only, that follow the mg/kg/dg/cg type of format
    var units = ['g', 'b', 'l', 'm'];

    for (var j = 0; j < units.length; j++) {
        var base = units[j];
        for (var i = 0; i < prefixes.length; i++) {
            unitConverter.addUnit(base, prefixes[i] + base, Math.pow(10, factors[i]));
        }
    }

    // we use the SI gram unit as the base; this allows
    // us to convert between SI and English units
    unitConverter.addUnit('g', 'ounce', 28.3495231);
    unitConverter.addUnit('g', 'oz', 28.3495231);
    unitConverter.addUnit('g', 'pound', 453.59237);
    unitConverter.addUnit('g', 'lb', 453.59237);


    window.$u = function (value, unit) {
        var u = new window.unitConverter(value, unit);
        return u;
    };
})();

console.log($u(1, 'g').as('kg').debug());  
console.log($u(1, 'kg').as('g').debug());
console.log($u(1, 'g').as('mg').debug());
console.log($u(1, 'mg').as('g').debug());
console.log($u(1, 'mg').as('kg').debug());

console.log($u(1, 'g').as('oz').debug());
console.log($u(1, 'g').as('lb').debug());

console.log($u(1, 'oz').as('lb').debug());

console.log($u(1, 'lb').as('g').debug());

// this last one throws an exception since you can't convert liters to mg
console.log($u(1, 'l').as('mg').debug());
jvenema
Nice! Very readable too. Thank you.
JPot
A: 

Here's something extensible and terse that builds a lookup table so performance should be good.

core = {};
core.mixin = function (dst, src) { if (src) for (var i in src) dst[i] = src[i]; return dst; }

function UnitConverter(meter) {
    var feetPerMeter = 3.2808399, feetPerMile = 5280;
    this.meter = core.mixin({ km: 1000, mi: feetPerMile / feetPerMeter, ft: 1 / feetPerMeter, m: 1 }, meter);
    var t = [], i = 0;
    for (t[i++] in this.meter);
    this.table = [];
    for (i = 0; i < t.length; i++)
        for (j = 0; j < t.length; j++) {
            var key1 = t[i], key2 = t[(i + j + 1) % t.length];
            this.table[key1 + "/" + key2] = this.meter[key2] / this.meter[key1];
        }
};

Usage

Number.converter = new UnitConverter({ nm: 1852 }); // extent to include nautical miles
Number.units = Number.converter.table;
var km = 1;
var mi = km * Number.units["mi/km"];
var m = Number.converter.convert(mi, "mi", "m");
var ft = Number.converter.convert(m, "ft/m");
m = Number.converter.convert(ft, "ft", "m");
ft = m / Number.converter.convert("m/ft");
km = ft * Number.units["km/ft"];
nm = Number.converter.convert(km, "km", "nm");
Corey Alix
Neglected to include the converter code: with (UnitConverter) { prototype = new Object(); prototype.convert = function () { switch (arguments.length) { case 1: return this.table[arguments[0]]; case 2: return arguments[0] * this.table[arguments[1]]; case 3: return arguments[0] * this.meter[arguments[1]] / this.meter[arguments[2]]; } return Number.NaN; } }
Corey Alix