Monday, 14 September 2015

JavaScript: a eurekaFFS moment

G'day:
This was going to be another "how would you solve this?" code quiz things, but in the end the problem proved to be really a lot easier to solve than I expected it to be, so I'll just use it as a demonstrating of me being daft instead. Oh, and some Jasmine unit tests.

This puzzled stemmed from this article: "Expectation management: mapping a changing array". I was wanted to take an array:

["a","b","c","d","e"]


and pass it through a function and end up with this:

[
    ["a", "b", "c", "d", "e"],
    ["b", "c", "d", "e"],
    ["c", "d", "e"],
    ["d", "e"],
    ["e"]
]

Where each progressive element has the rest of the array that was in the previous element.

My initial - not working - attempt was this:

// closure.js

var letters = ["a","b","c","d","e"];
var remappedLetters = letters.map(function(number,index){
    var localCopyOfTheseLetters = letters.slice();
    letters.shift();
    return localCopyOfTheseLetters;
});

But this demonstrates the issue that I'm altering the array as I remap it, which is pretty poor form. And doesn't work. The "doesn't work" part is the killer there.

So I decided "OK, let's have a quiz how to do this best".

First things first I knocked together some unit tests to make sure the thing works as I develop it:



// arrayRestSpec.js

require('jasmine-expect');

var arrayRest = require("./arrayRest.js");

describe("arrayRest() tests", function(){
    describe("TDD tests", function(){
        it("returns an array", function(){
            var result = arrayRest([]);
            expect(result).toBeArray();
        });
        describe("array length tests", function(){
            it("returns the correct-lengthed array when passed a zero-element array", function(){
                var input = [];
                var result = arrayRest([]);

                expect(result.length).toBe(input.length);
            });
            it("returns the correct-lengthed array when passed a one-element array", function(){
                var input = ["a"];
                var result = arrayRest(input);

                expect(result).toBeArrayOfSize(input.length);
            });
            it("returns the correct-lengthed array when passed a multiple-element array", function(){
                var input = ["a", "b", "c"];
                var result = arrayRest(input);

                expect(result).toBeArrayOfSize(input.length);
            });
        });
        describe("element tests", function(){
            it("returns an array with the expected elements", function(){
                var input = ["a", "b", "c", "d"];
                var result = arrayRest(input);

                result.forEach(function(element, index){
                    expect(element).toBeArray();
                    expect(element).toBeArrayOfSize(input.length - index);
                });
            });
            it("doesn't mess with the elements via some poor referencing", function(){
                var input = [["a","A"], ["b","B"], ["c","C"], ["d","D"]];
                var expected = [
                    [["a","A"], ["b","B"], ["c","C"], ["d","D"]],
                    [["b","B"], ["c","C"], ["d","D"]],
                    [["c","C"], ["d","D"]],
                    [["d","D"]]
                ];
                var result = arrayRest(input);

                expect(result).toEqual(expected);
            });
        });
    });
    describe("requirement tests", function(){
        it("returns the correct array", function(){
            var input = ["a", "b", "c", "d", "e"];
            var expected = [
                ["a", "b", "c", "d", "e"],
                ["b", "c", "d", "e"],
                ["c", "d", "e"],
                ["d", "e"],
                ["e"]
            ];
            var result = arrayRest(input);
            expect(result).toEqual(expected);
        });
    });
});

I claim some of those tests were "TDD" ones, but they really weren't in this case. I wrote all the tests before I started writing the code. TDD ought to be iterative as I write the function itself.

Also please note I have installed the jasmine-expect module to give me some more array-friendly expectations for those tests.

Anyway, I messed around and came up with this solution (which, I hasten to add, is pants):

// arrayRest.js
var arrayRest = function(array){
    return array.reduce(function(result, element, index, array){
        var referenceToCurrentLastElement, nextElement;
        if (result.length){
            referenceToCurrentLastElement = result.slice(-1)[0];
            nextElement = referenceToCurrentLastElement.slice();
            nextElement.shift();
        }else{
            nextElement = array.slice();
        }
        result.push(nextElement);
        return result;
    }, []);
};

module.exports = arrayRest;

That works, but I really didn't like the if() in there. If a function has a condition which amounts to "given some situation run this code, otherwise run this completely different code", it's probably wrong. Still: I decided to observe that caveat before offering it over to you lot to improve it (and in any language you like, as per usual).

As I was drafting this article in my head on the train this morning, I had a watershed moment, surprised my fellow passengers for declaring "oh for f***'s sake!" rather more loudly (and seemingly unprovokedly) than anyone expected (myself included), and hastily changed the draft of the article. Because the solution to the problem is simply this:

var arrayRest = function(array){
    return array.map(function(element,index,array){
        return array.slice(index);
    });
}

So there's no point having a quiz to solve that sort of thing, because it's just too simple.

The take away I have had from this is that if I find myself thinking "that function is wrong", then perhaps mulling it over is a good idea. And also a nod to iterative development, TDD and having test coverage: I would have been fine "going live" with the original function, and later replacing it with the improved one... my test coverage "proves" the modifications don't have any unexpected side effects, so the risk in changing code after the fact is reduced.

But, anyway, talk about making a mountain out of a mole hill.

Update:

Somewhat pleasingly, the same code works OK in CFML too, as demonstarted here:

arrayRest = function(array){
    return array.map(function(element,index,array){
        return array.slice(index);
    });
};
input = ["a", "b", "c", "d"];

result = arrayRest(input);

writeDump(result);



Righto.

--
Adam

PS: you should feel free to gist me implementations of this in other languages if you so choose: I'll share any I get in a day or so. It's always good to see other languages at work, even for mundane exercises like this.