What you are wanting is a queue, and there are many queue libraries out there.
Your needs of a queue though are quite simple, so Iāll take this opportunity to take you through using a testing library called Jasmine to derive the code that you need, from just a few simple tests.
Get the stand-alone version of Jasmine from https://jasmine.github.io/pages/getting_started.html via the More Information button, and follow the installation instructions.
You will be creating three files:
- queue-test.html which is the test runner
- queue.js which will contain the code for your test
- spec\queue.spec.js which contains the tests themself
Add the HTML code from the Installation instructions to queue-test.html. Donāt forget to change the 2.0.0 in the installation instructions to 2.5.2, or to whichever version that you downloaded.
You will also need to add the script that you are testing, and the spec that tests the script too, to end up with the following:
queue-test.html
<!DOCTYPE html>
<html>
<head>
<title>Queue test</title>
<link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-2.5.2/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="jasmine/lib/jasmine-2.5.2/jasmine.css">
</head>
<body>
<script type="text/javascript" src="jasmine/lib/jasmine-2.5.2/jasmine.js"></script>
<script type="text/javascript" src="jasmine/lib/jasmine-2.5.2/jasmine-html.js"></script>
<script type="text/javascript" src="jasmine/lib/jasmine-2.5.2/boot.js"></script>
<script type="text/javascript" src="queue.js"></script>
<script type="text/javascript" src="spec/queue.spec.js"></script>
</body>
</html>
When running queue-test.html you should see a Jasmine test page that says, āNo specs foundā.
Opening the developer console you might also see that the queue scripts cannot be found. Either create the files, or correct the path so that they can be found.
Let us now give it a test.
Keeping it really simple, the first test just makes sure that the queue exists, which helps us to make sure that the testing environment is properly set up.
We always start with a failing test, so that we know that the code we write is what causes the test to pass.
spec/queue.spec.js
describe("Queue", function () {
it("exists", function () {
expect(queue).toBeDefined();
});
});
With the above spec, the test page says ReferenceError: queue is not defined so letās create the queue.
queue.js
var queue = function () {
};
Normally we would take things even slower by not even providing a function, which forces the test code to drive the direction of the code, but itās all but a given that we will need a function here.
How is the test page looking? 1 spec, 0 failures - thatās what we want.
Whatās the next test? We will be wanting to add items to our queue, and eventually for the items to remain unique, so that adding an already existing item just brings it to the top of the queue.
One part at a time - starting with adding.
spec/queue.spec.js
it("adds an item", function () {
queue.add("Fruits");
expect(queue.count()).toBe(1);
expect(queue.get(0)).toBe("Fruits");
});
Iāve just made up those add, count, and get methods based on how I think that it should work.
Running the test shows us TypeError: queue.add is not a function so how are we to create that add method?
We could add it to the queueās prototype, but a more acceptable technique is to use an IIFE and return an object that contains the desired functions from within it.
queue.js
var queue = (function iife() {
function add() {
}
return {
add: add
};
}());
We now get a different error of TypeError: queue.count is not a function so letās add that.
queue.js
var queue = (function iife() {
function add() {
}
function count() {
}
return {
add: add,
count: count
};
}());
How is our test looking now? We see Expected undefined to be 1.
Letās make it pass in the simplest possible way:
queue.js
...
function count() {
return 1;
}
...
This is not just a bloody-minded policy doing the simplest thing. It helps us to get to a passing test, from where we can add more tests to help push the code in more appropriate directions. This highly useful technique is called triangulation
The test now shows: TypeError: queue.get is not a function so letās add it in a similar way to the others:
queue.js
...
function get() {
}
return {
add: add,
count: count,
get: get
};
...
The test now shows: Expected undefined to be āFruitsā. so letās return that string, just the string from the get function. A separate test can help us to improve on that.
queue.js
...
function get() {
return "Fruits";
}
...
And we have passing tests showing 2 specs, 0 failures
I wonāt show the ellipses in the code from now on to represent that itās an excerpt.
Letās now make the code more useful by adding a new test that adds two items:
spec/queue.spec.js
it("adds two items", function () {
queue.add("Fruits");
queue.add("Vegetables");
expect(queue.count()).toBe(2);
expect(queue.get(1)).toBe("Vegetables");
});
The test tells us, Expected 1 to be 2. so itās time to add the items to an actual array.
queue.js
var list = [];
function add(item) {
list.push(item);
}
function count() {
return list.length;
}
That looks like it should work, but the test tells us Expected 3 to be 2.
Why does it have 3 instead of 2? Ahh, because the queue still contains content from the previous test.
We need to create a new empty queue at the start of each test.
The standard notation here then is to use a capital initial letter for a constructor function, and to use something like Queue.init() to create a queue.
With our test we want a queue variable to be declared, so that before each of the tests we can create a new version of it.
spec/queue.spec.js
var queue;
beforeEach(function () {
queue = Queue.init();
});
The test now tells us ReferenceError: Queue is not defined so letās get that sorted out.
Itās easier that it seems, for we just need to remove the iife wrapper, renaming the function to init, and placing it in an object called Queue.
queue.js
var Queue = {
init: function init() {
var list = [];
...
return {
add: add,
count: count,
get: get
};
}
};
The test now tells us Expected āFruitsā to be āVegetablesā. which should be easy to fix by updating the get function.
queue.js
function get(i) {
return list[i];
}
And the test now tells us that everything is all good with 3 specs, 0 failures
The queue doesnāt yet work as we intend. When adding a new item to the queue, it should go to the top of the list, not to the bottom, so letās update the test to show that.
spec/queue.spec.js
it("adds two items", function () {
queue.add("Fruits");
queue.add("Vegetables");
expect(queue.count()).toBe(2);
expect(queue.get(0)).toBe("Vegetables");
expect(queue.get(1)).toBe("Fruits");
});
The test now tells us Expected āFruitsā to be āVegetablesā. which is entirely expected. Instead of adding the new item to the bottom of the list, it needs to be added to the top.
So instead of using push, we can use the unshift method.
queue.js
function add(item) {
list.unshift(item);
}
And the test shows everything passing with 3 specs, 0 failures
It would also help if we can get all of the items in the queue.
spec/queue.spec.js
it("can get all items in the queue", function () {
queue.add("Fruits");
queue.add("Vegetables");
expect(queue.getAll()).toEqual(["Vegetables", "Fruits"]);
});
The test now shows TypeError: queue.getAll is not a function so letās create it.
queue.js
function getAll() {
}
return {
add: add,
count: count,
get: get,
getAll: getAll
};
The test now shows Expected undefined to equal [ āVegetablesā, āFruitsā ].
Which should be as easy as returning the list array.
queue.js
function getAll() {
return list;
}
And the test is now all good, showing 4 specs, 0 failures
What else do we need from the queue? Moving an item to the top of the queue means removing it then adding it, so being able to remove an item from the queue is also going to be needed.
spec/queue.spec.js
it("removes an item", function () {
queue.add("Fruits");
queue.add("Vegetables");
queue.add("Nuts");
queue.remove("Vegetables");
expect(queue.getAll()).toEqual(["Nuts", "Fruits"]);
});
The test tells us TypeError: queue.remove is not a function so letās add that.
queue.js
function remove(item) {
}
...
return {
add: add,
remove: remove,
count: count,
get: get,
getAll: getAll
};
We are now told by the test Expected [ āNutsā, āVegetablesā, āFruitsā ] to equal [ āNutsā, āFruitsā ].
We can use the splice method to remove the item.
queue.js
function remove(item) {
list.splice(list.indexOf(item), 1);
}
And the test now shows 5 specs, 0 failures
Will it remove something even if itās not found?
spec/queue.spec.js
it("only removes when an item is found", function () {
queue.add("Fruits");
queue.remove("Something else");
expect(queue.getAll()).toEqual(["Fruits"]);
});
The test shows [color=red]Expected [ ] to equal [ āFruitsā ].[color] Thatās not good. We need to make sure that the item is found first before removing it.
queue.js
function remove(item) {
if (list.indexOf(item) > -1) {
list.splice(list.indexOf(item), 1);
}
}
And the test once again shows 5 specs, 0 failures
We are now ready for the big one. When an already existing item is added, that should not change the queue size, and instead make its way to the top of the queue.
spec/queue.spec.js
it("moves an existing item to the top", function () {
queue.add("Fruits");
queue.add("Vegetables");
queue.add("Fruits");
expect(queue.count()).toBe(2);
expect(queue.getAll()).toEqual(["Fruits", "Vegetables"]);
});
The test now tells us Expected 3 to be 2.
So, we need to check if the item is already in the list.
queue.js
function add(item) {
if (list.indexOf(item) === -1) {
list.unshift(item);
}
}
Going back to the test, we are now told Expected [ āVegetablesā, āFruitsā ] to equal [ āFruitsā, āVegetablesā ].
So when the item is already in the list, we need to remove it and add it back to the top.
We already have a remove method, so this looks like itās going to be quite simple:
queue.js
if (list.indexOf(item) === -1) {
list.unshift(item);
} else {
remove(item);
add(item);
}
The test now shows 6 specs, 0 failures, and I think that the queue code is all ready to be used.
Here it is in full, after the tests from the following post have been included.
queue.js
var Queue = {
init: function init() {
var list = [];
function add(item) {
if (list.indexOf(item) === -1) {
list.unshift(item);
} else {
remove(item);
add(item);
}
}
function remove(item) {
if (list.indexOf(item) > -1) {
list.splice(list.indexOf(item), 1);
}
}
function count() {
return list.length;
}
function get(i) {
return list[i];
}
function getAll() {
return list.slice(0);
}
return {
add: add,
count: count,
get: get,
getAll: getAll,
remove: remove
};
}
};
The tests that were used to come up with the above code are:
spec/queue.spec.js
describe("Queue", function () {
var queue;
beforeEach(function () {
queue = Queue.init();
});
it("exists", function () {
expect(queue).toBeDefined();
});
it("adds an item", function () {
queue.add("Fruits");
expect(queue.count()).toBe(1);
expect(queue.get(0)).toBe("Fruits");
});
it("adds two item", function () {
queue.add("Fruits");
queue.add("Vegetables");
expect(queue.count()).toBe(2);
expect(queue.get(0)).toBe("Vegetables");
expect(queue.get(1)).toBe("Fruits");
});
it("can get all items in the queue", function () {
queue.add("Fruits");
queue.add("Vegetables");
expect(queue.getAll()).toEqual(["Vegetables", "Fruits"]);
});
it("protects the list from being changed", function () {
queue.add("Fruits");
queue.add("Vegetables");
expect(queue.getAll()).toEqual(["Vegetables", "Fruits"]);
queue.getAll().reverse();
expect(queue.getAll()).toEqual(["Vegetables", "Fruits"]);
});
it("removes an item", function () {
queue.add("Fruits");
queue.add("Vegetables");
queue.add("Nuts");
queue.remove("Vegetables");
expect(queue.getAll()).toEqual(["Nuts", "Fruits"]);
});
it("only removes when an item is found", function () {
queue.add("Fruits");
queue.remove("Something else");
expect(queue.getAll()).toEqual(["Fruits"]);
});
it("moves an existing item to the top", function () {
queue.add("Fruits");
queue.add("Vegetables");
queue.add("Fruits");
expect(queue.count()).toBe(2);
expect(queue.getAll()).toEqual(["Fruits", "Vegetables"]);
});
});
Thanks to the tests, decisions being made were easily achieved, and the development of the code at each stage couldnāt be easier.