Skip to content

Instantly share code, notes, and snippets.

@Edward-Lombe
Last active April 6, 2017 04:11
Show Gist options
  • Save Edward-Lombe/eb284b5d632025869b18b50bd29389cc to your computer and use it in GitHub Desktop.
Save Edward-Lombe/eb284b5d632025869b18b50bd29389cc to your computer and use it in GitHub Desktop.
Simple, compatible implementation of a recursive flatten function, now with tests
// 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
}
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