3
\$\begingroup\$

I created this out of curiosity and it has some code duplication issues and the fact that sticking .yield() on the end of a call to a unit converter is pretty strange.

How would you improve it?

/**
 * A very special velocity converter
 * Designed mainly for a very natural syntax
 * It allows full chainable conversion calls and maintains its on internal state.
 * 
 * example:
 * remotewind.convert(10).knots().to.mph().to.kmh().yield();
 *
 * Each unit has a yield "method" which returns the last value (the internal state)  
 *
 * @param val
 * @return Converter
 */
remotewind.convert = function(val) {

    var val = val;

    function Unit(){
        this.yield = function(){
            return val;
        }
    }

    function Ms(ms){
        val = ms;
        this.to = {
            kmh : function(){ return new Kmh(val * 3.6) },
            mph : function(){ return new Mph(val * 2.2369362920544) },
            knots: function(){ return new Knots(val * 1.943844492) }
        }
    }
    Ms.prototype = new Unit();

    function Kmh(kmh){
        val = kmh;
        this.to = {
            ms : function(){ return new Ms(val * 0.277777778) },
            mph : function(){ return new Mph(val * 0.621371192) },
            knots: function(){ return new Knots(val * 0.539956803) }
        }
    }
    Kmh.prototype = new Unit();

    function Mph(mph){
        val = mph;
        this.to = {
            ms : function(){ return new Ms(val * 0.44704) },
            kmh : function(){ return new Kmh(val * 0.621371192) },
            knots: function(){ return new Knots(val * 0.868976242) }
        }
    }
    Mph.prototype = new Unit();

    function Knots(kn){
        val = kn;
        this.to = {
            ms : function(){ return new Ms(val * 0.514444444) },
            mph : function(){ return new Mph(val * 1.150779448) },
            knots: function(){ return new Knots(val * 1.852) }
        }
    }
    Knots.prototype = new Unit();

    function Converter(val){
        return {
            ms: function(){  return new Ms(val) },
            kmh: function(){  return new Kmh(val) },
            mph: function(){  return new Mph(val) },
            knots: function(){  return new Knots(val) }
        }
    }

    return new Converter(val);
}
\$\endgroup\$
  • \$\begingroup\$ Just curious why there's differing spellings of yield scattered about.. \$\endgroup\$ – Phix Nov 23 '13 at 2:41
  • \$\begingroup\$ @Phix, because I can never remember how to spell it. I took the liberty of fixing it now though. \$\endgroup\$ – papirtiger Nov 23 '13 at 2:43
  • \$\begingroup\$ Gotcha. Just making sure it isn't something affecting your code. \$\endgroup\$ – Phix Nov 23 '13 at 4:55
4
\$\begingroup\$

Here's a fairly cryptic rewrite, focussing mostly on extensibility and minimal repetition. The trick I'm using is to pick one unit as our "base" unit from which all others are derived. The base-unit value (m/s in this case) is carried through the chain.

var convert = (function () {
  var conversions = {
    ms:    1, // use m/s as our base unit
    kmh:   3.6,
    mph:   2.23693629,
    knots: 1.94384449
    // feel free to add more 
  };

  function Unit(unit, ms) {
    this.value = ms * conversions[unit];
    this.to = {};
    for(var otherUnit in conversions) {
      (function (target) {
        this.to[target] = function () {
          return new Unit(target, ms);
        }
      }).call(this, otherUnit);
    }
  }

  Unit.prototype = {
    yield: function () {
      return this.valueOf();
    },

    toString: function () {
      return String(this.value);
    },

    valueOf: function () {
      return this.value;
    }
  };

  return function (value) {
    var units = {};
    for(var unit in conversions) {
      (function (unit) {
        units[unit] = function () {
          return new Unit(unit, value / conversions[unit]);
        };
      }(unit));
    }
    return units;
  } 
}());

Edit: Just for fun, here's an ultra-convoluted version that handles different measurement types. Couldn't get temperature conversions in there, as simple multiplication/division isn't enough to deal with C/K/F conversions. Boo.

Really though, the code's pretty dense, and I can't recommend coding like this. But again, this was just for fun.

Anyway, the usage would be something like

convert.speed(60).mph().to.kmh();  // ~97km/h
convert.distance(100).m().to.ft(); // ~330ft
convert.mass(1000).g().to.lb();    // ~2.2lb

And here's the hairy code:

var convert = (function () {
  var conversions = {
    speed: {
      ms:    1, // use m/s as our base unit
      kmh:   3.6,
      mph:   2.23693629,
      knots: 1.94384449
    },

    distance: {
      m:      1, // use meters as our base
      inches: 39.3700787402, // can't use "in" as that's a keyword. Darn.
      ft:     3.280839895,
      mi:     0.000621371192,
      nm:     0.000539956803 // nautical miles, not nanometers
    },

    mass: {
      g:  1, // use grams as our base
      lb: 0.002204622622,
      oz: 0.0352739619
    }
  };

  function Unit(type, unit, base) {
    this.value = base * conversions[type][unit];
    this.to = {};
    for(var otherUnit in conversions[type]) {
      (function (target) {
        this.to[target] = function () {
          return new Unit(type, target, base);
        }
      }).call(this, otherUnit);
    }
  }

  Unit.prototype = {
    yield: function () {
      return this.valueOf();
    },

    toString: function () {
      return String(this.value);
    },

    valueOf: function () {
      return this.value;
    }
  };

  // my god, it's full of scopes!
  var types = {};
  for(var type in conversions) {
    (function (type) {
      types[type] = function (value) {
        var units = {};
        for(var unit in conversions[type]) {
          (function (unit) {
            units[unit] = function () {
              return new Unit(type, unit, value / conversions[type][unit]);
            }
          }(unit));
        }
        return units;
      };
    }(type));
  }

  return types;
}());
\$\endgroup\$
  • \$\begingroup\$ Brilliant, it took me a little while to decipher. Especially the line where you return a new Unit, from inside the Unit constructor. A+ \$\endgroup\$ – papirtiger Nov 23 '13 at 2:53
  • \$\begingroup\$ @papirtiger Thanks. And, yeah, it's pretty convoluted. Should have commented it, but where'd the fun be in that? :) On the plus side, it's easy to add more conversions. For fun, I just added an even more tricky version that handles different types of measurements :) \$\endgroup\$ – Flambino Nov 23 '13 at 14:05
0
\$\begingroup\$

You can override valueOf to perform the same function as yield in your code. Then, you could override toString to get the quantitative part and append the unit name. For the purposes of outputting your measures to text, and for debugging with e.g. Firebug, that would be a plus.

As for factoring, it would be more flexible if you kept units of length separate from units of time.

\$\endgroup\$
  • \$\begingroup\$ Can you give an example of separating units of length from units of time without greatly complicating it? \$\endgroup\$ – papirtiger Nov 23 '13 at 3:05
  • \$\begingroup\$ @papirtiger Maybe. It's really not that different from the original. km.per.hour().to.m.per.second() is what I would want to see from a client code perspective. Each distance unit gets a per object, the properties of which are units time. Units time get the to object, the properties of which are identical to your original to object. I'll see if I can cook up a more detailed example. \$\endgroup\$ – sqykly Nov 23 '13 at 18:04
  • \$\begingroup\$ Yeah except who wants to do km.per.hour().to.nm.per.hour() to get for example knots? \$\endgroup\$ – papirtiger Nov 23 '13 at 18:15
  • \$\begingroup\$ @papirtiger Clearly not you, I guess. It would definitely be less concise. But if you're using a library for unit conversions, you probably want to be able to express any unit in your domain. Otherwise, you would write a function or two for the units you need and let that be that. \$\endgroup\$ – sqykly Nov 23 '13 at 18:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.