Skip to content

Instantly share code, notes, and snippets.

@th507
Created March 14, 2013 04:51
Show Gist options
  • Save th507/5158907 to your computer and use it in GitHub Desktop.
Save th507/5158907 to your computer and use it in GitHub Desktop.
recursively flatten array in JavaScript
/*
* @name arrayEqual
* @description check if two arrays are equal
* @param {arr} Array to check
* @param {arr} Array to check
* @return {Boolean} Boolean, returns true if arrays are the same
*/
function arrayEqual(a, b) {
var i = Math.max(a.length, b.length, 1);
while(i-- >= 0 && a[i] === b[i]);
return (i === -2);
}
/*
* @name flattenArray
* @description flattens multi-dimension array into one-dimension array
* useful for manipulating function arguments like flattenArray(arguments)
* @usage flattenArray(arr)
* eg.
* [1, 2] => [1, 2]
* [1, [[[[[[2]]]]]]] => [1, 2]
* [1,[2,3],[[[[4]]],5]] => [1, 2, 3, 4, 5]
*
* @param {arr} Array to flatten
* @return {Array} Array, one-dimension array
*/
function flattenArray(arr) {
var r = [];
while (!arrayEqual(r, arr)) {
r = arr;
arr = [].concat.apply([], arr);
}
return arr;
}
@ptdecker
Copy link

This works great, but I do not understand the tricks you are applying here at all. Would you be so kind as to explain your cleverness?

For example, why the obfuscated approach to comparing the two arrays in arrayEqual? I think you're trying to walk down the array elements from last to first comparing each one, but I don't understand the approach nor how you would ever reach a value of i === -2. JSLint (not that I 100% agree with its suggestions) throws up all over this line. And, I thought I understood .apply but I cannot grasp how you're applying it here. Anyway, thank you in advance for taking the time to respond.

@silverweed
Copy link

The arrayEqual() approach is certainly unusual, but it's 100% legitimate (it actually fails in two cases, see below).

The i variable is declared as Math.max(a.length, b.length, 1), therefore it will be 1 when the function is given two empty arrays, else it'll be the length of the longest array among a and b.

This means that the while loop will always:

  1. check for its condition at least twice
  2. run its loop at least once.

Since the cycle breaks as soon as the two arrays are different, the case where the two arrays are of different length is accounted, except when either a or b contains the undefined value, in which case the condition check may fail:

arrayEqual([1, 2, undefined], [1, 2]) === true // even though a.length == 3 and b.length == 2

Another case where this check fails is when comparing the NaN value:

arrayEqual([NaN], [NaN]) === false

but this is due to the sicked nature of NaN of not being equal to itself rather than this specific algorithm.

Discarded these two special cases, which are probably not an issue for the use of the function in flatten, we may consider the limit case, i.e. when both arrays are of length 1. In fact, if i == 1, the while loop will become something like this:

condition A:
   is i less than 0 ?                      # the first time this is false, since i is 1
   either way, decrement i by 1.  # now i is 0
   if condition A was true, then:   # and it was at least once
condition B:
   is a[0] === b[0] ?
   if a[0] is different than b[0], the loop will break here, and i will be 0, thus the function returns false;
   else:
   -- here there would be the while loop, but it's empty --
   back to the condition check:
condition A:
   is i less than 0 ?               # nope, it is 0
   either way, decrement i    # now i is -1
condition B:
   is a[-1] === b[-1] ?            # yes, they're both undefined!
   -- here there would be the while loop, but it's empty --
  back to
condition A:
   is i less than 0?                # yes, it is now -1
   either way, decrement i    # now i is -2!
   BREAK

control reaches here if and only if all the array elements were equal, and here i is -2

The function arrayEqual may be improved to take the 1st special case into account by extending it a bit:

function arrayEqual(a, b) {
    if (a.length !== b.length) return false;
    // ...
}

as for the second case, I don't see an elegant way to do it; maybe using isNaN.

As regards the [].concat.apply([], arr), it's a pretty standard way to "1-level flatten" an array; [].concat is the same as Array.prototype.concat, while concat.apply just applies concat using as this argument to itself the first argument passed to it (in this case, [], which is used as the base array for the concatenation).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment