├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── src ├── __snapshots__ │ ├── map.test.js.snap │ └── reduce.test.js.snap ├── filter.js ├── filter.test.js ├── flatten.js ├── flatten.test.js ├── map.js ├── map.test.js ├── reduce.js ├── reduce.test.js ├── zip.js └── zip.test.js └── wallaby.conf.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "standard", "prettier"], 3 | "plugins": ["jest"], 4 | "env": { 5 | "jest/globals": true 6 | }, 7 | "rules": { 8 | "jest/no-disabled-tests": "warn", 9 | "jest/no-identical-title": "error", 10 | "jest/valid-expect": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arrasync 2 | Async array utilities for ES6 3 | 4 | Six functions are included in `arrasync`, all of which are async: 5 | * [`map`](#maparray-func) 6 | * [`filter`](#filterarray-func) 7 | * [`reduce`](#reducearray-func-accumulator) 8 | * [`flatten`](#flattenarray) 9 | * [`zip`](#ziparrays) 10 | * [`unzip`](#unziparray) 11 | 12 | ## Installation 13 | ```npm install --save arrasync``` 14 | 15 | Import and usage: 16 | ```javascript 17 | import {map} from 'arrasync'; 18 | 19 | // returns the squares of the whole numbers from 1 to 5 20 | const getSquares = async () => { 21 | const array = [1, 2, 3, 4, 5]; 22 | const squares = await map(array, async value => value ** 2); 23 | return squares; 24 | }; 25 | 26 | console.log(getSquares()); // [1, 4, 9, 16, 25] 27 | ``` 28 | 29 | 30 | ## Usage 31 | 32 | ### `map(array, func)` 33 | Asynchronously apply a function to every element of an array, returning an array of the results. 34 | 35 | Arguments: 36 | * `array` - the array to be mapped. 37 | * `func(value, index, array)` - the function to be called for each value of array. Can be sync or async, and can optionally take the index of each value and the array being mapped as arguments. 38 | 39 | Example: 40 | ```javascript 41 | // Map values to their squares 42 | 43 | const array = [1, 2, 3, 4, 5]; 44 | const squares = await map(array, async value => value ** 2); 45 | console.log(squares); // [1, 4, 9, 16, 25] 46 | ``` 47 | 48 | ### `filter(array, func)` 49 | 50 | Asynchronously filter an array based on a true/false test. 51 | 52 | Arguments: 53 | * `array` - the array to be filtered 54 | * `func(value, index, array)` - a function that returns true on a value to be included, and returns false on a value that shouldn't be included. Can be sync or async, and can optionally take the index of each value and the array being mapped as arguments. 55 | 56 | Example: 57 | ```javascript 58 | // Filter numbers out of an array 59 | 60 | const array = [1, 'a', 'b', 2, 3, 'c', 4, 'd']; 61 | const strings = await filter(array, async value => typeof value !== 'number'); 62 | console.log(strings); // ['a', 'b', 'c', 'd'] 63 | ``` 64 | 65 | ### `reduce(array, func, accumulator)` 66 | 67 | Apply a function on each value in an array (from left to right) using an accumulator to reduce it to a single value. 68 | 69 | Arguments: 70 | * `array` - the array to be reduced 71 | * `func(accumulator, value, index, array)` - The function to execute on each value of the array, which returns the new value of the accumulator at each step. Can be sync or async, but executes in series either way. Optionally can take the index of each value and the array being reduced as arguments. 72 | * `accumulator` - the initial value of the accumulator 73 | 74 | Example: 75 | ```javascript 76 | // Sum values of an array 77 | 78 | const array = [1, 2, 3, 4, 5]; 79 | const sum = await reduce(array, async (accumulator, value) => accumulator + value, 0); 80 | console.log(sum); // 15 81 | ``` 82 | 83 | ### `flatten(array)` 84 | 85 | Given an arbirarily nested array, returns a flat array with order preserved. 86 | 87 | Arguments: 88 | * `array` - the nested array to be flattened 89 | 90 | Example: 91 | ```javascript 92 | // Flatten a nested array 93 | 94 | const array = ['a', [['b', 'c'], 'd', 'e'], 'f', 'g', ['h'], 'i']; 95 | const flattened = await flatten(array); 96 | console.log(flattened) // ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']; 97 | ``` 98 | 99 | ### `zip(...arrays)` 100 | 101 | Takes multiple arrays and returns an array of sub-arrays, where the first sub-array holds the first element from each of the input arrays, the second sub-array holds the second element from each of the input arrays, and so on. 102 | 103 | Arguments: 104 | * `...arrays` - any number of equal-length arrays 105 | 106 | Example: 107 | ```javascript 108 | // Zip together two arrays 109 | 110 | const numbers = [1, 2, 3, 4, 5]; 111 | const letters = ['a', 'b', 'c', 'd', 'e']; 112 | const zipped = await zip(numbers, letters); 113 | console.log(zipped); // [[1, 'a'], [2, 'b'], [3, 'c'], [4, 'd'], [5, 'e']] 114 | ``` 115 | 116 | ### `unzip(array)` 117 | 118 | Does the reverse of `zip` (equivalent to `zip(...array)`). 119 | 120 | Arguments: 121 | * `array` - array to be unzipped 122 | 123 | Example: 124 | ```javascript 125 | // Unzip an array 126 | 127 | const array = [[1, 'a'], [2, 'b'], [3, 'c'], [4, 'd'], [5, 'e']] 128 | const unzipped = await unzip(array); 129 | console.log(unzipped); // [[1, 2, 3, 4, 5], ['a', 'b', 'c', 'd', 'e']] 130 | ``` 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const {filter} = require('./src/filter'); 2 | const {flatten} = require('./src/flatten'); 3 | const {map} = require('./src/map'); 4 | const {reduce} = require('./src/reduce'); 5 | const {unzip, zip} = require('./src/zip'); 6 | 7 | module.exports = {filter, flatten, map, reduce, unzip, zip}; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mattmorgis/arrasync", 3 | "version": "0.1.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --watch" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/tamera-lanham/arrasync.git" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "eslint": "^5.14.0", 17 | "eslint-config-prettier": "^4.1.0", 18 | "eslint-config-standard": "^12.0.0", 19 | "eslint-plugin-import": "^2.16.0", 20 | "eslint-plugin-jest": "^22.0.0", 21 | "eslint-plugin-node": "^8.0.0", 22 | "eslint-plugin-promise": "^4.0.0", 23 | "eslint-plugin-standard": "^4.0.0", 24 | "jest": "^24.0.0", 25 | "prettier": "^1.16.4" 26 | }, 27 | "prettier": { 28 | "bracketSpacing": false, 29 | "jsonEnable": [ 30 | "json" 31 | ], 32 | "javascriptEnable": [ 33 | "javascript", 34 | "javascriptreact" 35 | ], 36 | "singleQuote": true, 37 | "trailingComma": "es5", 38 | "tabWidth": 2 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/__snapshots__/map.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Map test use arguments of mapper function 1`] = ` 4 | Array [ 5 | Array [ 6 | "a", 7 | 0, 8 | "a", 9 | ], 10 | Array [ 11 | "b", 12 | 1, 13 | "a", 14 | ], 15 | Array [ 16 | "c", 17 | 2, 18 | "a", 19 | ], 20 | Array [ 21 | "d", 22 | 3, 23 | "a", 24 | ], 25 | ] 26 | `; 27 | -------------------------------------------------------------------------------- /src/__snapshots__/reduce.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Reduce Tests Reduce uses optional arguments 1`] = ` 4 | Array [ 5 | Array [ 6 | "a", 7 | 0, 8 | "a", 9 | ], 10 | Array [ 11 | "b", 12 | 1, 13 | "a", 14 | ], 15 | Array [ 16 | "c", 17 | 2, 18 | "a", 19 | ], 20 | Array [ 21 | "d", 22 | 3, 23 | "a", 24 | ], 25 | ] 26 | `; 27 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | const {zip} = require('./zip'); 2 | 3 | /** 4 | * Filter an array based on a true/false test. 5 | * 6 | * @function filter 7 | * @param {Array} array - The array to be filtered 8 | * @param {function(value, i, array)} func - A function that returns true on a 9 | * value to be included in the filtered array, and returns false on a value 10 | * that shouldn't be included. Optionally can take the index of each value and 11 | * the array being filtered as arguments. 12 | */ 13 | const filter = async (array, func) => { 14 | const promises = []; 15 | for (const [i, value] of array.entries()) { 16 | promises.push(func(value, i, array)); 17 | } 18 | 19 | const results = []; 20 | for (const [value, promise] of await zip(array, promises)) { 21 | if (await promise) { 22 | results.push(value); 23 | } 24 | } 25 | 26 | return results; 27 | }; 28 | 29 | module.exports = {filter}; 30 | -------------------------------------------------------------------------------- /src/filter.test.js: -------------------------------------------------------------------------------- 1 | const {filter} = require('./filter'); 2 | 3 | describe('Filter test', () => { 4 | test('first filter test', async () => { 5 | const testArray = [1, 1, 2, 3, 5, 8]; 6 | const filtered = await filter(testArray, async num => { 7 | return num % 2 === 0; 8 | }); 9 | expect(filtered).toEqual([2, 8]); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/flatten.js: -------------------------------------------------------------------------------- 1 | const {reduce} = require('./reduce'); 2 | 3 | /** 4 | * Given an arbirarily nested array, returns a flat array (no nesting) with 5 | * order preserved. 6 | * 7 | * @param {Array} array - nested array 8 | * 9 | * @returns {Array} - flattened array 10 | */ 11 | const flatten = async array => { 12 | return reduce( 13 | array, 14 | async (acc, value) => { 15 | if (!Array.isArray(value)) { 16 | acc.push(value); 17 | } else { 18 | const flattenedArray = await flatten(value); 19 | acc.push(...flattenedArray); 20 | } 21 | return acc; 22 | }, 23 | [] 24 | ); 25 | }; 26 | 27 | module.exports = {flatten}; 28 | -------------------------------------------------------------------------------- /src/flatten.test.js: -------------------------------------------------------------------------------- 1 | const {flatten} = require('./flatten'); 2 | 3 | describe('flatten tests', () => { 4 | test('flattens a 2D list', async () => { 5 | const list = [['a', 'x'], ['b', 'y'], ['c', 'z']]; 6 | const result = await flatten(list); 7 | expect(result).toEqual(['a', 'x', 'b', 'y', 'c', 'z']); 8 | }); 9 | test('flattens a nasty list', async () => { 10 | const list = [['a', [['b', 'c'], 'd', 'e'], 'f', 'g', ['h'], 'i']]; 11 | const result = await flatten(list); 12 | expect(result).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply a function to every element of an array, returning an array of the 3 | * results. 4 | * 5 | * @function map 6 | * @param {Array} array - The array to be mapped 7 | * @param {function(value, number, Array): Promise} func - The async 8 | * function to be called for each value of array. Optionally can take the 9 | * index of each value and the array being mapped as arguments. 10 | */ 11 | const map = (array, func) => { 12 | const promises = []; 13 | for (const [i, value] of array.entries()) { 14 | promises.push(func(value, i, array)); 15 | } 16 | 17 | return Promise.all(promises); 18 | }; 19 | 20 | module.exports = {map}; 21 | -------------------------------------------------------------------------------- /src/map.test.js: -------------------------------------------------------------------------------- 1 | const {map} = require('./map'); 2 | 3 | const sleep = seconds => { 4 | const milliseconds = seconds * 1000; 5 | return new Promise((resolve, reject) => { 6 | setTimeout(() => { 7 | return resolve(`Waited ${seconds} seconds`); 8 | }, milliseconds); 9 | }); 10 | }; 11 | 12 | describe('Map test', () => { 13 | test('first map test', async () => { 14 | const testArray = [1, 1, 2, 3, 5, 8]; 15 | const mapped = await map(testArray, async num => { 16 | return num * num; 17 | }); 18 | expect(mapped).toEqual([1, 1, 4, 9, 25, 64]); 19 | }); 20 | 21 | test('use arguments of mapper function', async () => { 22 | const testArray = ['a', 'b', 'c', 'd']; 23 | const mapped = await map(testArray, (value, i, array) => { 24 | return [value, i, array[0]]; 25 | }); 26 | expect(mapped).toMatchSnapshot(); 27 | }); 28 | 29 | test('should perform tasks in parallel', async () => { 30 | // To test that all tasks are performmed in parallel 31 | // we will create an array of promises that simulate an async 32 | // operation by simply sleeping. 33 | const testArray = [2, 3, 2]; 34 | // Our map function will sleep for 2 seconds, 3 seconds, and 2 seconds. 35 | // We want the entire operation to take 3 seconds, not 7 (2 + 3 + 2) seconds. 36 | // The test will automatically timeout and fail after 5 seconds. 37 | const mapped = await map(testArray, async seconds => sleep(seconds)); 38 | expect(mapped[1]).toBe('Waited 3 seconds'); 39 | }); 40 | 41 | test('Should catch async errors', async () => { 42 | const testArray = ['a', 'b']; 43 | try { 44 | await map(testArray, async item => { 45 | try { 46 | // simulate async error happening 47 | throw new Error('bang'); 48 | } catch (e) { 49 | throw e; 50 | } 51 | }); 52 | } catch (e) { 53 | expect(e.message).toBe('bang'); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/reduce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Applies a function against an accumulator and each element in the array 3 | * (from left to right) to reduce it to a single value. 4 | * 5 | * @function reduce 6 | * @param {Array} array - The array to be reduced 7 | * @param {function(accumulator, value, i, array)} func - The function to 8 | * execute on each value of array, which returns the new value of accumulator 9 | * at each step. Optionally can take the index of each value and the array 10 | * being reduced as arguments. 11 | * @param accumulator - Accumulates the values returned by func 12 | */ 13 | const reduce = async (array, func, accumlator) => { 14 | for (const [i, value] of array.entries()) { 15 | accumlator = await func(accumlator, value, i, array); 16 | } 17 | 18 | return accumlator; 19 | }; 20 | 21 | module.exports = {reduce}; 22 | -------------------------------------------------------------------------------- /src/reduce.test.js: -------------------------------------------------------------------------------- 1 | const {reduce} = require('./reduce'); 2 | 3 | describe('Reduce Tests', () => { 4 | test('Reduces into single value', async () => { 5 | const list = [1, 2, 3, 4, 5]; 6 | const result = await reduce( 7 | list, 8 | (item, accumlator) => { 9 | return item + accumlator; 10 | }, 11 | 0 12 | ); 13 | expect(result).toBe(15); 14 | }); 15 | 16 | test('Reduce uses optional arguments', async () => { 17 | const testArray = ['a', 'b', 'c', 'd']; 18 | const result = await reduce( 19 | testArray, 20 | (accumulator, value, i, array) => { 21 | accumulator.push([value, i, array[0]]); 22 | return accumulator; 23 | }, 24 | [] 25 | ); 26 | expect(result).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/zip.js: -------------------------------------------------------------------------------- 1 | const {map} = require('./map'); 2 | 3 | /** 4 | * Takes multiple arrays and returns an array of sub-arrays, where the first 5 | * sub-array holds the first element from each of the input arrays, the second 6 | * sub-array holds the second element from each of the input arrays, and so on. 7 | * Input arrays must have equal length. 8 | * 9 | * @param {...Array} arrays - arrays to be zipped 10 | * 11 | * @returns {Array} - zipped arrays 12 | */ 13 | const zip = async (...arrays) => { 14 | for (const array of arrays) { 15 | if (array.length !== arrays[0].length) { 16 | throw new Error('Cannot zip arrays of unequal length'); 17 | } 18 | } 19 | 20 | return map(arrays[0], async (value, i) => { 21 | return map(arrays, arr => arr[i]); 22 | }); 23 | }; 24 | 25 | /** 26 | * Does the reverse of zip, equivalent to zip(...array). 27 | * 28 | * @param {Array} array - array to be unzipped 29 | * 30 | * @returns {Array} - unzipped arrays 31 | */ 32 | const unzip = async array => { 33 | return zip(...array); 34 | }; 35 | 36 | module.exports = {zip, unzip}; 37 | -------------------------------------------------------------------------------- /src/zip.test.js: -------------------------------------------------------------------------------- 1 | const {zip, unzip} = require('./zip'); 2 | 3 | describe('Zip Tests', () => { 4 | test('check length match', async () => { 5 | expect.assertions(1); 6 | const firstArray = ['a', 'b', 'c']; 7 | const secondArray = ['x', 'y']; 8 | try { 9 | await zip(firstArray, secondArray); 10 | } catch (error) { 11 | expect(error).toBeTruthy(); 12 | } 13 | }); 14 | 15 | test('zip should combine first elements', async () => { 16 | const firstArray = ['a', 'b', 'c']; 17 | const secondArray = ['x', 'y', 'z']; 18 | const expectedResult = [['a', 'x'], ['b', 'y'], ['c', 'z']]; 19 | 20 | expect(await zip(firstArray, secondArray)).toEqual(expectedResult); 21 | }); 22 | 23 | test('zip should combine three arrays', async () => { 24 | const firstArray = ['a', 'b', 'c']; 25 | const secondArray = ['x', 'y', 'z']; 26 | const thirdArray = [1, 2, 3]; 27 | const expectedResult = [['a', 'x', 1], ['b', 'y', 2], ['c', 'z', 3]]; 28 | 29 | expect(await zip(firstArray, secondArray, thirdArray)).toEqual( 30 | expectedResult 31 | ); 32 | }); 33 | 34 | test('zip can unzip', async () => { 35 | const firstArray = ['a', 'b', 'c']; 36 | const secondArray = ['x', 'y', 'z']; 37 | const zipped = await zip(firstArray, secondArray); 38 | const unzipped = await unzip(zipped); 39 | expect(unzipped).toEqual([firstArray, secondArray]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | files: ['src/**/*.js', '!src/**/*.test.js'], 4 | 5 | tests: ['src/**/*.test.js'], 6 | 7 | env: { 8 | type: 'node', 9 | runner: 'node', 10 | }, 11 | 12 | testFramework: 'jest', 13 | }; 14 | }; 15 | --------------------------------------------------------------------------------