Last active
April 6, 2017 04:11
-
-
Save Edward-Lombe/eb284b5d632025869b18b50bd29389cc to your computer and use it in GitHub Desktop.
Simple, compatible implementation of a recursive flatten function, now with tests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Polyfill isArray if needed | |
if (typeof Array.isArray !== 'function') { | |
Array.isArray = isArray | |
} | |
function isArray(arg) { | |
return Object.prototype.toString.call(arg) === '[object Array]' | |
} | |
if (typeof module === 'object') { | |
module.exports = flatten | |
} | |
/** | |
* Flattens an array, throws a TypeError if the first argument is not an array. | |
* As this is a recursive implementation without tail call optimization it can | |
* also throw a RangeError, which will be thrown at nesting levels roughly | |
* greater than 5000 in a browser context and 10000 within a node context | |
* | |
* @param {any[]} array | |
* @returns {any[]} | |
*/ | |
function flatten(array) { | |
if (!Array.isArray(array)) { | |
// Makes errors from null more explicit | |
var badType = array === null | |
? 'null' | |
: typeof array | |
var message = 'First argument to flatten must be an array, not ' + badType | |
throw new TypeError(message) | |
} | |
var flattened = [] | |
// Iterate input array | |
for (var i = 0; i < array.length; i++) { | |
var element = array[i] | |
// Check whether element is an array | |
if (Array.isArray(element)) { | |
// If it is flatten recursively | |
var flattened_ = flatten(element) | |
// Then append the returned array | |
for (var j = 0; j < flattened_.length; j++) { | |
flattened[flattened.length] = flattened_[j] | |
} | |
} else { | |
flattened[flattened.length] = element | |
} | |
} | |
return flattened | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const flatten = require('./flatten') | |
const { deepEqual, throws, ok } = require('assert') | |
console.log('Beginning tests...') | |
const start = Date.now() | |
// Test that array is flat | |
ok(isFlat(flatten([[1, 2, [3, [4, [[5, [[[[7]]]]]], []]]]]))) | |
ok(isFlat(flatten([[], [], [], [[], [], []]]))) | |
ok(isFlat(flatten(randomArray()))) | |
// Test basic functionality | |
deepEqual(flatten([]), []) | |
deepEqual(flatten([[], [], []]), []) | |
deepEqual(flatten([1, [2, [3, [4]]]]), [1, 2, 3, 4]) | |
deepEqual(flatten([[1, 2, [3]], 4]), [1, 2, 3, 4]) | |
deepEqual(new Array(), []) | |
deepEqual(flatten([{}]), [{}]) | |
deepEqual(flatten([{ 0: [] }]), [{ 0: [] }]) | |
// Test that array does not mutate input arguments | |
const input = [1, 2, [3, 4, [[5]]]] | |
const copy = JSON.parse(JSON.stringify(input)) | |
flatten(input) | |
deepEqual(input, copy) | |
// Test that correct error type is throw | |
throws(throwsTypeError(), TypeError) | |
throws(throwsTypeError(undefined), TypeError) | |
throws(throwsTypeError({}), TypeError) | |
throws(throwsTypeError({ 0: 0, 1: 1}), TypeError) | |
throws(throwsTypeError(0), TypeError) | |
throws(throwsTypeError(''), TypeError) | |
throws(throwsTypeError(false), TypeError) | |
throws(throwsTypeError(null), TypeError) | |
throws(throwsTypeError(() => {}), TypeError) | |
throws(throwsTypeError(Symbol()), TypeError) | |
// Test that flatten will throw RangeError given an array that is deeply nested | |
throws(throwsRangeError, RangeError) | |
// Test that correct error message is thrown | |
throws(throwsTypeError(), /undefined$/) | |
throws(throwsTypeError(undefined), /undefined$/) | |
throws(throwsTypeError({}), /object$/) | |
throws(throwsTypeError(0), /number$/) | |
throws(throwsTypeError(''), /string$/) | |
throws(throwsTypeError(false), /boolean$/) | |
throws(throwsTypeError(null), /null$/) | |
throws(throwsTypeError(() => {}), /function$/) | |
throws(throwsTypeError(Symbol()), /symbol$/) | |
console.log(`All tests passed in ${Date.now() - start}ms`) | |
/** | |
* Returns a function that calls flatten with the specified value | |
* | |
* @param {any} input | |
* @returns {Function} | |
*/ | |
function throwsTypeError(input) { | |
return () => { | |
flatten(input) | |
} | |
} | |
/** | |
* Increases the nesting of an array until a RangeError is thrown | |
* | |
*/ | |
function throwsRangeError() { | |
let i = 0 | |
let nested = null | |
while (true) { | |
// Need to make sure that our recursive nest function does not error for | |
// high values of n | |
try { | |
nested = nest(i) | |
} catch (error) { | |
// As we expect the function to throw, just catch and log possible error | |
console.error(error) | |
break | |
} | |
// Make sure we only catch RangeErrors | |
try { | |
flatten(nested) | |
} catch (error) { | |
if (!(error instanceof RangeError)) { | |
console.error(error) | |
break | |
} else { | |
throw error | |
} | |
} | |
i++ | |
} | |
} | |
/** | |
* Helper function to nest an array to a depth of n | |
* | |
* @param {integer} n | |
* @returns {any[]} | |
*/ | |
function nest(n) { | |
if (!Number.isInteger(n)) { | |
throw new TypeError('n must be an integer') | |
} | |
if (n >= 1) { | |
return [nest(n - 1)] | |
} else { | |
return [] | |
} | |
} | |
/** | |
* Determines if an array is flat (contains only non array elements) | |
* | |
* @param {any} array | |
*/ | |
function isFlat(array) { | |
if (!Array.isArray(array)) { | |
throw new TypeError('isFlat must be called with array') | |
} | |
return array.every(element => !Array.isArray(element)) | |
} | |
/** | |
* Generates a random array with elements of many types and values | |
* | |
* @returns {any[]} | |
*/ | |
function randomArray() { | |
const MAX_DEPTH = 8 | |
const MAX_LENGTH = 8 | |
const RANDOM_ELEMENTS = | |
[ 0, '', false, null, undefined, {}, [], () => {} | |
, 1, ' ', true, { k: 'v' }, { 0: 0, 1: 1}, [[], [], []] | |
, '[]', 'array' | |
] | |
return (function generateRandomArray(depth) { | |
const randomLength = Math.ceil(Math.random() * MAX_LENGTH) | |
const randomDepth = Math.ceil(Math.random() * depth) | |
const random = [] | |
if (depth > 1) { | |
for (let i = 0; i < randomLength; i++) { | |
random.push(generateRandomArray(depth - 1)) | |
} | |
} else { | |
return pluckRandom() | |
} | |
return random | |
})(MAX_DEPTH) | |
function pluckRandom() { | |
return RANDOM_ELEMENTS[Math.floor(Math.random() * RANDOM_ELEMENTS.length)] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment