Task description
You live in the city of Cartesia where all roads are laid out in a perfect grid. You arrived ten minutes too early to an appointment, so you decided to take the opportunity to go for a short walk. The city provides its citizens with a Walk Generating App on their phones -- everytime you press the button it sends you an array of one-letter strings representing directions to walk (eg. ['n', 's', 'w', 'e']). You always walk only a single block in a direction and you know it takes you one minute to traverse one city block, so create a function that will return true if the walk the app gives you will take you exactly ten minutes (you don't want to be early or late!) and will, of course, return you to your starting point. Return false otherwise.
Note: you will always receive a valid array containing a random assortment of direction letters ('n', 's', 'e', or 'w' only). It will never give you an empty array (that's not a walk, that's standing still!).
Task solution
Tests
describe("walk validator", () => {
it("Throws when invalid input is provided", () => {
expect(() => isValidWalk("w")).toThrow(/InvalidArgumentException/);
expect(() => isValidWalk(["w", 2])).toThrow(/InvalidArgumentException/);
expect(() => isValidWalk(["w", "test"])).toThrow(/InvalidArgumentException/);
expect(() => isValidWalk(["w"], ["2", 2])).toThrow(/InvalidArgumentException/);
expect(() => isValidWalk(["w"], 1)).toThrow(/InvalidArgumentException/);
expect(() => isValidWalk(["w"], [1, 1, 1])).toThrow(/InvalidArgumentException/);
expect(() => isValidWalk(["w"], [0, 0], "ten")).toThrow(/InvalidArgumentException/);
});
it("Should correctly identify walkable directions", () => {
expect(isValidWalk(["n", "s", "n", "s", "n", "s", "n", "s", "n", "s"])).toBe(true);
expect(isValidWalk(["w", "e", "w"])).toBe(false);
expect(isValidWalk(["w"])).toBe(false);
expect(isValidWalk(["w", "e"], [1, 1], 2)).toBe(true);
});
});
Using Jest for our tests, we begin by defining our failing input cases as usual. In our case these are:
- Are the directions not an array?
- Are the instructions all strings?
- Are the instructions expected strings ("n", "s", "e" or "w")?
- Are the starting points (if defined) integers?
- Are the starting points matching the expected
[x1, y1]
shape? - Are we able to use this function for any amount of time depending on user case?
Then we test the happy paths to be sure that our function can correctly identify valid pathways which bring us back to our starting point after the final direction is executed.
Implementation
function isValidWalk(walk, startingPosition = [0, 0], timeAvailableMinutes = 10) {
if (!Array.isArray(walk)) {
throw new Error(`InvalidArgumentException: Parameter 1 must be an array. Received: ${typeof walk}`);
} else if (!walk.every(item => typeof item === "string")) {
throw new Error("InvalidArgumentException: Parameter 1 must be an array of strings, atleast one element within the array provided is not a string");
} else if(!walk.every(item => ["n", "s", "e", "w"].includes(item))) {
throw new Error("InvalidArgumentException: Parameter 1 must be an array of strings. Each string must correspond to a compass direction, valid directions are: 'n', 's', 'e' and 'w'");
} else if (!Array.isArray(startingPosition)) {
throw new Error(`InvalidArgumentException: Parameter 2 must be an array. Received: ${typeof startingPosition}`);
} else if(startingPosition.length !== 2) {
throw new Error(`InvalidArgumentException: Parameter 2 must have 2 items representing the starting position of the user. Received: ${startingPosition} with a length of ${startingPosition.length}`);
} else if(!startingPosition.every(item => Number.isInteger(item))) {
throw new Error(`InvalidArgumentException: Parameter 2 must be an array of numbers and have a length of 2 items. This is to match the schema requirement of [x1: number, y1: number]. Received: ${startingPosition}`);
} else if(!Number.isInteger(timeAvailableMinutes)) {
throw new Error(`InvalidArgumentException: Parameter 3 must be an integer. Received: ${typeof timeAvailableMinutes}`);
}
const [x1, y1] = startingPosition;
const [x2, y2] = walk.reduce(([x, y], direction) => {
switch (direction) {
case 'n': return [x, y + 1];
case 's': return [x, y - 1];
case 'e': return [x + 1, y];
case 'w': return [x - 1, y];
}
}, [x1, y1]);
return walk.length === timeAvailableMinutes && x1 === x2 && y1 === y2;
}
We run our input checks and then begin to reason our coordinates. Firstly we strip the starting x
and y
positions of the user and name these x1
and y1
.
Next, we take the walk
array of directions and reduce it to an array of x2
and y2
positions. To achieve this, the initial "accumulator" of the reducer is set to x1
and y1
and on each iteration of the reducer, based on the current direction, we either increment or decrement x
or y
. Upon the reducers final iteration, these values will now be our x2
and y2
coordinates.
Finally we check if the walk
had the same amount of items as the minutes it takes per direction (as outlined in the task description) and from there we check if the start and end x
and y
values match. If all of these criteria match, we know the walk is valid since the walk time matches the available time and the end positions match the starting ones.
Conclusions
This challenge was a good use case for reducers and the only change I would probably make to the implementation is to return early if the walk and time available don't match, like so:
// code removed for reading ease
if(walk.length !== timeAvailableMinutes) return false;
const [x1, y1] = startingPosition;
const [x2, y2] = walk.reduce(([x, y], direction) => {
switch (direction) {
case 'n': return [x, y + 1];
case 's': return [x, y - 1];
case 'e': return [x + 1, y];
case 'w': return [x - 1, y];
}
}, [x1, y1]);
return x1 === x2 && y1 === y2;
// code removed for reading ease
Top comments (0)