Here's my answer for the next Friday puzzle that was posted on the CFML Slack channel. I skipped last week's one as... well... I couldn't be arsed, but this was an easy one and I could sort it out whilst cursing my jet lag (I've just done London -> Auckland this week).
The details of the puzzle are in this gist, but basically it's to take a number of bytes and "round" it to the appropriate closest unit of bytes (eg: kB, MB, GB etc), up to PB. There's more detail than that, but that's the bit I'm paying attention do. Doing CFML is a waste of time, so this week I've decided to do it in JavaScript instead, and re-polish my piss-poor ES2015 / Node.js / Mocha skills. Although not too much.
First I tried a solution which treated the number as a string and just reduced it down to the most significant figures and slapped a unit onto the end of it. Whilst this did what I set out to do, it only dealt with decimal groupings, not 1024-based ones. Here's the code anyhow. It's not polished up cos I abandoned it half-way through:
f = function(x) {
var units = ["B", "kB", "MB", "GB", "TB", "PB"];
var ordersOfMagnitude = 3;
var numberAsString = x.toString();
var lengthOfNumber = numberAsString.length;
var numberOfDigits = lengthOfNumber % ordersOfMagnitude;
var unitIndex = Math.floor(lengthOfNumber / ordersOfMagnitude);
if (numberOfDigits == 0) {
numberOfDigits = 3;
unitIndex--;
}
if (unitIndex+1 > units.length){
unitIndex = units.length - 1;
numberOfDigits = lengthOfNumber - (unitIndex * ordersOfMagnitude);
}
var digits = numberAsString.substring(0, numberOfDigits);
var unitToUse = units[unitIndex];
var result = digits + unitToUse;
return result;
}
This was more just a spike to get me thinking.
Then I tried another version where I did a reduction on the units array, but that was crap as it was quite side-effect-y, and I don't really think it was a good use of a reduce operation. I did not keep the code for that one, so I can't show you.
Next I decided to stop messing around and come up with an answer that actually worked and wasn't daft, so I knocked this one together:
"use strict";
let numberToMemoryUnits = function(bytes) {
let units = ["kB", "MB", "GB", "TB", "PB"];
let binaryDivisor = 1024;
let numberOfBytesAsUnit = bytes;
let unit = "B";
while (numberOfBytesAsUnit >= binaryDivisor && units.length){
numberOfBytesAsUnit /= binaryDivisor;
unit = units.shift();
}
let roundedValue = Math.floor(numberOfBytesAsUnit);
return `${roundedValue}${unit}`;
}
module.exports = numberToMemoryUnits;
That's OK-ish I guess. The only thing I don't like is how I have that first assignment of the units separate to the loop. It seems like dodgy code doubling-up to me, and I should get rid of that somehow, but can't be bothered thinking it through (I'm slightly hungover, which doesn't help).
Update:
I've tweaked this slightly since I first posted it:- Improved the argument name from a rather lazy-arsed
x
to the more descriptivebytes
. - Likewise improved the name of the variable which was
digits
to benumberOfBytesAsUnit
. - Used the intermediary variable
roundedValue
, - and used an interpolated string instead of string concatenation with the return value.
- improved the way I initialised the
units
object in the tests, from being individual assignment statements to being a reduction of the units array.
digits
(now numberOfBytesAsUnit
) instead of just using the argument x
(bytes
). This was because whilst the value passed to the function is indeed a number of bytes, once I start using it - specifically in that loop - it's no longer a value in bytes, so I use a new - more descriptively accurate - variable name instead. We also discussed my separate handling of bytes
and the other units coming from the array, and I'm still not 100% happy with it, but in using the better variable names around it I mind it less than I did before.Any other code review input is welcomed, btw.
I was a bit naughty as I tested it by hand whilst developing it, but then felt guilty and formalised a bunch of tests after the fact. I guess I still did TDD whilst throwing this together, but not as deliberately as perhaps I shoulda. Anyhow, here are the tests too:
"use strict";
let assert = require("chai").assert;
let numberToMemoryUnits = require("../src/numberToMemoryUnits.js");
let binaryFactor = 1024;
let units = ["kB", "MB", "GB", "TB", "PB"].reduce(function(units, unit, index){
units[unit] = Math.pow(binaryFactor, index + 1);
return units;
}, {});
describe("Tests for each unit", function(){
it("should work for bytes", function(){
let result = numberToMemoryUnits(123);
let expectation = "123B";
assert.equal(expectation, result);
});
it("should work for kB", function(){
let result = numberToMemoryUnits(2345);
let expectation = "2kB";
assert.equal(expectation, result);
});
it("should work for MB", function(){
let result = numberToMemoryUnits(3456789);
let expectation = "3MB";
assert.equal(expectation, result);
});
it("should work for GB", function(){
let result = numberToMemoryUnits(4567890123);
let expectation = "4GB";
assert.equal(expectation, result);
});
it("should work for TB", function(){
let result = numberToMemoryUnits(5678901234567);
let expectation = "5TB";
assert.equal(expectation, result);
});
it("should work for PB", function(){
let result = numberToMemoryUnits(6789012345678901);
let expectation = "6PB";
assert.equal(expectation, result);
});
});
describe("Test exact units", function(){
it("should work for 1kB", function(){
let result = numberToMemoryUnits(units.kB);
let expectation = "1kB";
assert.equal(expectation, result);
});
it("should work for 1MB", function(){
let result = numberToMemoryUnits(units.MB);
let expectation = "1MB";
assert.equal(expectation, result);
});
it("should work for 1GB", function(){
let result = numberToMemoryUnits(units.GB);
let expectation = "1GB";
assert.equal(expectation, result);
});
it("should work for 1TB", function(){
let result = numberToMemoryUnits(units.TB);
let expectation = "1TB";
assert.equal(expectation, result);
});
it("should work for 1PB", function(){
let result = numberToMemoryUnits(units.PB);
let expectation = "1PB";
assert.equal(expectation, result);
});
});
describe("Test off by one", function(){
it("should work for <1kB", function(){
let result = numberToMemoryUnits(units.kB-1);
let expectation = "1023B";
assert.equal(expectation, result);
});
it("should work for >1kB", function(){
let result = numberToMemoryUnits(units.kB+1);
let expectation = "1kB";
assert.equal(expectation, result);
});
it("should work for <1MB", function(){
let result = numberToMemoryUnits(units.MB-1);
let expectation = "1023kB";
assert.equal(expectation, result);
});
it("should work for >1MB", function(){
let result = numberToMemoryUnits(units.MB+1);
let expectation = "1MB";
assert.equal(expectation, result);
});
it("should work for <1GB", function(){
let result = numberToMemoryUnits(units.GB-1);
let expectation = "1023MB";
assert.equal(expectation, result);
});
it("should work for >1GB", function(){
let result = numberToMemoryUnits(units.GB+1);
let expectation = "1GB";
assert.equal(expectation, result);
});
it("should work for <1TB", function(){
let result = numberToMemoryUnits(units.TB-1);
let expectation = "1023GB";
assert.equal(expectation, result);
});
it("should work for >1TB", function(){
let result = numberToMemoryUnits(units.TB+1);
let expectation = "1TB";
assert.equal(expectation, result);
});
it("should work for <1PB", function(){
let result = numberToMemoryUnits(units.PB-1);
let expectation = "1023TB";
assert.equal(expectation, result);
});
it("should work for >1PB", function(){
let result = numberToMemoryUnits(units.PB+1);
let expectation = "1PB";
assert.equal(expectation, result);
});
});
describe("Test boundaries", function(){
it("should work for 0bytes", function(){
let result = numberToMemoryUnits(0);
let expectation = "0B";
assert.equal(expectation, result);
});
it("should work for 1024PB", function(){
let result = numberToMemoryUnits(units.PB*units.kB);
let expectation = "1024PB";
assert.equal(expectation, result);
});
it("should work for 1048576PB", function(){
let result = numberToMemoryUnits(units.PB*units.MB);
let expectation = "1048576PB";
assert.equal(expectation, result);
});
});
They're a bit long-winded in total, but the individual tests are simple enough. The key here is I test either side of each each unit, eg: 1023 is presented in bytes, 1024 is 1kB and so is 1025 etc. Also a test of zero for good measure, as well that it handles numbers beyond PB, and just uses PB thereafter (eg: 1025PB displays as such).
And they all pass:
C:\src\js\puzzle\20160903>mocha test\numberToMemoryUnitsTest.js
Tests for each unit
√ should work for bytes
√ should work for kB
√ should work for MB
√ should work for GB
√ should work for TB
√ should work for PB
Test exact units
√ should work for 1kB
√ should work for 1MB
√ should work for 1GB
√ should work for 1TB
√ should work for 1PB
Test off by one
√ should work for <1kB
√ should work for >1kB
√ should work for <1MB
√ should work for >1MB
√ should work for <1GB
√ should work for >1GB
√ should work for <1TB
√ should work for >1TB
√ should work for <1PB
√ should work for >1PB
Test boundaries
√ should work for 0bytes
√ should work for 1024PB
√ should work for 1048576PB
24 passing (31ms)
C:\src\js\puzzle\20160903>
Tests for each unit
√ should work for bytes
√ should work for kB
√ should work for MB
√ should work for GB
√ should work for TB
√ should work for PB
Test exact units
√ should work for 1kB
√ should work for 1MB
√ should work for 1GB
√ should work for 1TB
√ should work for 1PB
Test off by one
√ should work for <1kB
√ should work for >1kB
√ should work for <1MB
√ should work for >1MB
√ should work for <1GB
√ should work for >1GB
√ should work for <1TB
√ should work for >1TB
√ should work for <1PB
√ should work for >1PB
Test boundaries
√ should work for 0bytes
√ should work for 1024PB
√ should work for 1048576PB
24 passing (31ms)
C:\src\js\puzzle\20160903>
That's it. Nothing special today, but still required some thought, and also reminding myself how Mocha works.
Give it a go. Do it in some other language than CFML!
Righto.
--
Adam