When your high school gym teacher asked you to "drop and give me 20", did you ever have the guts to ask, "20 what?" Being the geeks that we are, such a question is perfectly valid, although in asking you might just earn yourself another 20 units of unwanted exercise! Although assumptions can be made in the real world about the unit implied by a certain context, the same can't be said for our code. Compilers and interpreters usually can't make assumptions like the one above based on context.
Every programming language supports numbers natively, although they are almost always implemented as primitive types – a place in memory to store a number, nothing less, nothing more. But as the opening paragraph suggests, numbers by themselves present very little information, unless they are paired with a unit. We write our own code to deal with units, but most often the code is not consistent, both in checking the unit of a number, and converting to and from units.
If you are developing in a dynamic language, such as JavaScript, you are offered an opportunity to enhance the way numbers are stored natively. The ability to "attach" a unit to a number can be gifted to the Number data type. In the case of JavaScript, this is possible because a Number is an object, and most objects can be modified through JavaScripts prototype language feature.
Enhancing the Number object with the prototype property:
Number.prototype.setUnit = function(unit) {
this._unit = unit;
}
Number.prototype.getUnit = function() {
return this.unit;
}
The downside is that to leverage these new functions, we need to "formally" create a number object:
myNum = new Number(56);
myNum.setUnit('inches');
console.log( myNum.getUnit() );
On the flip-side, we still get to use our number as we're used to, it's functionally the same:
myNum + 50
is still a valid arithmetic operation. The internal value of myNum is still 56.
Adjusting the constructor for more meaningful code
Sure, the technique above works great, but who really wants to initialize a Number object that way every time they want need to use an integer or float? What if we adjusted the Number object's constructor, to allow us to provide the unit as well? That would certainly make our additional efforts of creating a Number object more worthwhile, and perhaps make the code a little easier on the eyes:
var oldProto = Number.prototype;
Number = function(num, unit) {
this.value = num;
this.unit = unit;
};
oldProto.valueOf = function() { return this.value }
Number.prototype = oldProto;
Number.prototype.setUnit = function(unit) {
this.unit = unit;
}
Number.prototype.getUnit = function() {
return this.unit;
}
The real magic of the code above is the recreation of the valueOf() function on the Number object. If you've read an opinions on creating numbers with the new keyword, you might have found them all to be against such a practice. But, much of their reasoning is that you loose the ability to perform operations on them. By redefining the valueOf function that JavaScript uses internally, we preserve that capability.
However, it should be noted that the equality operation will no longer work! This is important to remember.
x = new Number(30, 'inches');
y = new Number(30, 'inches');
x == y
will return false. This is because the equality operator does an object comparison and does not use the valueOf() function. I'd have to say that this is probably the only downside to this method, we're forced to compare against the valueOf() method:
x.valueOf() == y.valueOf();
Or, if we're feeling ambitious, adding a equals function to the Number object would serve our purposes equally well:
x.equals(y);
If you think this approach lends to code that's easier to read and write, using the code examples above, creating such a function should be relatively easy.
Avoiding inconsistent unit values
As in the examples above, we certainly could allow string literals to be passed into the constructor. But, what happens when we use inches in one case, Inches in another case, and inch in yet another case? There are too many possible conditions to account for when testing against the units value. A simple solution to this is to provide a basic "units map":
Number.units = { inches: 'in', millimeters: 'mi', feet: 'ft' }
So, our new method of creating a Number object would look something like:
x = new Number(30, Number.units.inches);
But, what would prevent supplying string literals to the constructor? You could also perform some validation in the constructor:
Number = function(num, unit) {
this.value = num;
var unitValidates = false;
for each(value in Number.units) {
if(value === unit) {
unitValidates = true;
break;
}
}
// an assertion would make more sense here
// unless we're accepting unit input from the user
if(!unitValidates) {
throw new Error(unit + ' unit is not allowed');
return false;
}
this.unit = unit;
};
Dropping in the conversion functions
Next comes the need to convert between units. There are two ways we can do this:
Number.unitMap = [
{Number.units.inches :
{Number.unit.millimeters: 25.4,
Number.unit.feet: 1 / 12 },
{Number.units.millimeters: {Number.unit.inches: 1 / 25.4,
Number.unit.feet: 1 / (25.4 * 12) },{Number.units.feet:
{Number.unit.inches: 12,
Number.unit.millimeters: 12 * 25.4 }
]
- Modify the Number object itself
Number.prototype.changeUnit = function(unit) {this.value = this.value * Number.unitMap[this.getUnit][unit];
}
- Use a function to return the converted value
Number.prototype.convertUnit = function(unit) {
return this.value * Number.unitMap[this.getUnit][unit];
}
Arguments against the arguments against adjusting built-in prototypes
That double-negative is not a typo. As stated before, there are unlimited arguments against modifying built-in object prototypes. Some are valid, but most are nothing more than opinions about programming style and convention.
(26).doSomething();
The above statement, agreeably is confusing and probably not a good practice. But, the method I discuss above doesn't allow for such "literal processing", as you are required to formally create a Number object with the new keyword.
var x = new Number(30, Number.units.inches);
This statement can hardly be considered bad style or practice. It uses standard JavaScript object creation syntax, spells out clearly that we are using a modified version of the Number object, and encapsulates the use of units.
At worst, we're trading a questionable JavaScript practice for many rock-solid software construction practices including abstraction and encapsulation.