I recently took a coding test for a promotion at work. This was one of the tasks I really struggled with and was wondering what is the best way to do this. I used a load of
It's not elegant or pretty, but it seems to do the trick!
const NOT_POSSIBLE = 'NOT POSSIBLE';
function generate(A, B, C, D) {
var args = [A, B, C, D];
var idx = -1;
var out = NOT_POSSIBLE;
var firstN, secondN;
MAIN: {
args.sort(NUMERIC_ASCENDING);
// number has to start with 0, 1 or 2
if (args[0] > 2) break MAIN;
while (args[++idx] < 3) {}
// take the higest 2, 1, or 0
firstN = args[--idx];
args = pop(args, idx);
if (firstN === 2) {
// make sure that the first number doesn't exceed 23 and
// the second number 59
if (args[0] > 3 || args[0] > 1 && args[1] > 5)
break MAIN;
// advance to the first number < 3 or the length
idx = 0;
while (args[++idx] < 3){}
} else {
// much simpler if we have a 0 or 1, take the biggest n remaining
idx = args.length;
}
secondN = args[--idx];
args = pop(args, idx);
// if minutes number is too large, swap
if (args[0] > 5) {
out = '' + secondN + args[1] + ':' + firstN + args[0];
} else {
// if bottom number is low enough, swap for more minutes
out = '' + firstN + secondN + (args[1] < 6 ? ':' + args[1] + args[0] : ':' + args[0] + args[1]);
}
}
return out;
}
// numeric comparator for sort
function NUMERIC_ASCENDING(x, y) {
return x > y ? 1 : y > x ? -1 : 0;
}
// specialized "array pop" I wrote out longhand that's very optimized; might be cheating =D
function pop(arr, target) {
switch (arr.length) {
case 3:
switch (target) {
case 0: return [arr[1], arr[2]];
case 1: return [arr[0], arr[2]];
default: return [arr[0], arr[1]];
}
case 4:
switch (target) {
case 0: return [arr[1], arr[2], arr[3]];
case 1: return [arr[0], arr[2], arr[3]];
case 2: return [arr[0], arr[1], arr[3]];
default: return [arr[0], arr[1], arr[2]];
}
}
}
/* --------------- Start Speed Test --------------------- */
let startTime = Math.floor(Date.now());
let times = 10000;
let timesHolder = times;
while (times--) {
let A = randNum();
let B = randNum();
let C = randNum();
let D = randNum();
generate(A, B, C, D);
if (times == 0) {
let totalTime = Math.floor(Date.now()) - startTime;
let msg = timesHolder + ' Call Finished Within -> ' + totalTime + ' ms <-';
console.log(msg);
}
}
function randNum() {
return Math.floor(Math.random() * (9 - 0 + 1)) + 0;
}
/* --------------- END Speed Test --------------------- */
Hmmm..... I guess it's really simple if you break it into simpler problems: eg find all valid hours (00-23), for each of these valid hours use the remaining numbers to find valid minutes (00-59), combine and sort. In pseudo code something like the following
valid_times = []
function get_max(digits[]) {
for each d1 in digits[]
for each d2 in (digits[] except d1)
res = is_valid_hour(d1, d2)
if(res > 0) {
if(res == 2)
swap(d1, d2)
d3 = one of the rest in (digits except d1 and d2)
d4 = digit left in digits[]
res = is_valid_minute(d3, d4)
if(res > 0)
if(res == 2)
swap(d3, d4)
add (d1, d2, d3, d4) to valid_times;
}
sort(valid_times)
print valid_times[0]
}
function is_valid_hour(a, b) {
if (a*10+b<24)
return 1
if (b*10+a<24)
return 2
return 0;
}
function is_valid_minute(a, b) {
if (a*10+b<60)
return 1
if (b*10+a<60)
return 2
return 0;
}
My approach is to have array of available numbers (stack
) and another with return value (ret
). At first I put in ret invalid values "-1". Then I sort stack descending and loop trough to try to assign biggest possible number to return stack.
function swap(a, b, p1, p2) {
var temp = a[p1];
a[p1] = b[p2];
b[p2] = temp;
}
function t(a, b, c, d) {
var stack = [a, b, c, d];
var ret = [-1, -1, -1, -1];
stack.sort().reverse();
var change = true;
var i = 0;
// this while is assigning HOURS
while(change === true || i < 4) {
change = false;
// Assigning at first position (Hh:mm), so number must be lower or equal to 2
if(stack[i] <= 2 && ret[0] < stack[i]) {
swap(ret, stack, 0, i);
change = true;
i = 0;
}
// Assigning at second position (hH:mm), so number must be <= 4 if number
// at first position is 2, otherwise just make sure valid number
// (0 to 1) is assigned at first position
else if(((ret[0] === 2 && stack[i] <= 4) || ret[0] < 2 && ret[0] >= 0) && ret[1] < stack[i]) {
swap(ret, stack, 1, i);
change = true;
i = 0;
}
else i++;
}
stack.sort().reverse();
change = true;
i = 0;
// This while is assigning minutes
while(change === true || i < 4) {
change = false;
if(stack[i] <= 5 && ret[2] < stack[i]) {
swap(ret, stack, 2, i);
change = true;
i = 0;
}
else if(stack[i] <= 9 && ret[3] < stack[i]) {
swap(ret, stack, 3, i);
change = true;
i = 0;
}
else i++;
}
// If return stack contains -1, invalid combination was entered
return Math.min.apply(Math, ret) > -1
? ret[0] + "" + ret[1] + ":" + ret[2] + "" + ret[3]
: "NOT POSSIBLE";
}
console.log(t(6, 5, 2, 0)); // 20:56
console.log(t(3, 9, 5, 0)); // 09:53
console.log(t(2, 5, 6, 8)); // NOT POSSIBLE
With a small input and output space, using a look-up table is always an option; however, I found that in JavaScript the size of the table has a surprisingly large impact on the speed.
If we start by sorting the input to get a canonical version, so that 4,3,2,1
and 3,1,4,2
are both transformed into 1,2,3,4
, there are less than 400 possibilities that lead to a valid result. But as soon as I added more than 200 entries to the look-up table, the speed dropped considerably (which is probably browser-dependent).
However, there are only five types of digits:
0,1 <- can be first digit of hours followed by any digit
2 <- can be first digit of hours followed by 0-3
3 <- can be second digit of hours after a 2 to form 23 hours
4,5 <- can be first digits of minutes
6-9 <- can only be second digit of hours or minutes
Within these types, the digits are interchangeable; the optimal permutation will be the same:
2,4,0,6 -> 20:46 (ACBD)
2,5,1,9 -> 21:59 (ACBD)
If you represent the digits by digit types "0" (0-1), "2", "3", "4" (4-5), and "6" (6-9), there are only 48 combinations that lead to a valid solution, each using one of 16 different permutations. Code with these smaller look-up tables turns out to be much faster:
function generate(A, B, C, D) {
var swap; // sorting network
if (A > B) { swap = A; A = B; B = swap; }
if (C > D) { swap = C; C = D; D = swap; }
if (A > C) { swap = A; A = C; C = swap; }
if (B > D) { swap = B; B = D; D = swap; }
if (B > C) { swap = B; B = C; C = swap; }
var table = {"0000":15, "0002":15, "0003":14, "0004":14, "0006":14, "0022":15,
"0023":14, "0024":13, "0026":12, "0033":11, "0034":11, "0036":11,
"0044":11, "0046":11, "0066":10, "0222":15, "0223":14, "0224":13,
"0226":12, "0233":11, "0234": 9, "0236": 8, "0244": 7, "0246": 6,
"0266": 4, "0333": 5, "0334": 5, "0336": 5, "0344": 5, "0346": 5,
"0366": 4, "0444": 5, "0446": 5, "0466": 4, "2222":15, "2223":14,
"2224":13, "2226":12, "2233":11, "2234": 9, "2236": 8, "2244": 7,
"2246": 6, "2333": 5, "2334": 3, "2336": 2, "2344": 1, "2346": 0};
var type = ['0','0','2','3','4','4','6','6','6','6'];
var key = type[A] + type[B] + type[C] + type[D];
var permutation = table[key];
if (permutation == undefined) return "NOT POSSIBLE";
var digits = [[2,3,C,D], [2,3,D,C], [2,3,3,D], [2,3,D,3],
[A,D,B,C], [A,D,C,B], [2,A,C,D], [2,A,D,C],
[2,3,A,D], [2,3,D,A], [B,D,A,C], [B,D,C,A],
[2,B,A,D], [2,B,D,A], [C,D,B,A], [D,C,B,A]];
var time = digits[permutation];
return "" + time[0] + time[1] + ':' + time[2] + time[3];
}
function rndDigit() { return Math.floor(Math.random() * 10); }
for (var tests = 0; tests < 11; tests++) {
var d = [rndDigit(), rndDigit(), rndDigit(), rndDigit()];
document.write(d + " → " + generate(d[0],d[1],d[2],d[3]) + "<BR>");
}
An approach using a precomputed string, containing all possible permutations.
function generate(A,B,C,D){
var isValidTime = /^(?:[01]\d|2[0-3]):(?:[0-5]\d)$/;
var pattern = "0123012 0132013 0213021 0231023 0312031 0321032".replace(/\d/g, i => arguments[+i]);
var max = "";
for(var i=pattern.length-4; i--; ){
var time = pattern.substr(i,2) + ":" + pattern.substr(i+2,2);
if(time > max && isValidTime.test(time))
max = time;
}
return max || "NOT POSSIBLE";
}
[
[6,5,0,2],
[3,9,5,0],
[7,6,3,8]
].forEach(arr => console.log(arr + ' -> ' + generate(...arr)));
.as-console-wrapper{top:0;max-height:100%!important}
but we can improve on that, by using the regex to find only valid times:
function generate(A,B,C,D){
var pattern = "0123012 0132013 0213021 0231023 0312031 0321032".replace(/\d/g, i => arguments[+i]);
console.log(pattern);
var matchValidTime = /([01]\d|2[0-3])([0-5]\d)/g, m, max = "";
while(m = matchValidTime.exec(pattern)){
var time = m[1] + ":" + m[2];
if(time > max) max = time;
console.log("index: %o time: %o max: %o", m.index, time, max);
matchValidTime.lastIndex = m.index+1; //to find intersecting matches
}
return max || "NOT POSSIBLE";
}
[
[1,2,3,4],
//[6,5,0,2],
//[3,9,5,0],
//[7,6,3,8]
].forEach(arr => console.log(arr + ' -> ' + generate(...arr)));
.as-console-wrapper{top:0;max-height:100%!important}