diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..433845f --- /dev/null +++ b/lib/index.js @@ -0,0 +1,230 @@ +// Reference: +// https://medium.com/dailyjs/functional-js-with-es6-recursive-patterns-b7d0813ef9e3 +// Version 0.1.0 + +/** + * Return if argument supplied is defined. + * @param {*} object + */ +const def = x => typeof x !== 'undefined' + +/** + * is? + * @param {*} object + * @param {*} type + */ +const is = (x, t) => { + switch(t) { + case Array: return Array.isArray(x) + case String: return typeof(x) === 'string' + case Number: return typeof(x) === 'number' + case Function: return typeof(x) === 'function' + case Object: return typeof(x) === 'object' + default: return x instanceof t + } +} + +/** + * Returns a copy of an array + * @param {Array} array + */ +const copy = array => [...array] + +/** + * Return the first item in an array + * @param {Array} array + */ +const head = ([x]) => x + +/** + * Return all array but first element + * @param {Array} array + */ +const tail = ([, ...xs]) => xs + +/** + * Convert function that takes an array to one that takes multiple arguments. + * @param {Function} fn + */ +const spreadArg = (fn) => (...args) => fn(args) + +/** + * Partially apply a function by filling in any number of its arguments. + * @param {Function} fn + * @param {*} args + */ +const partial = (fn, ...args) => (...newArgs) => fn(...args, ...newArgs) + +/** + * Extract property value from array. Useful when combined with the map function. + * @param {*} key + * @param {*} object + */ +const pluck = (key, object) => object[key] + +/** + * Returns a new array with value inserted at given index. + * @param {Array} array + * @param {Number} index + * @param {*} object + */ +const slice = ([x, ...xs], i, y, curr = 0) => def(x) + ? curr === i + ? [y, x, ...slice(xs, i, y, curr + 1)] + : [x, ...slice(xs, i, y, curr + 1)] + : [] + +/** + * Applies a function against an accumulator and each element in the array + * (from left to right) to reduce it to a single value. + * @param {Array} array + * @param {Function} fn + * @param {*} memo + */ +const reduce = ([x, ...xs], fn, memo, i = 0) => def(x) + ? reduce(xs, fn, fn(memo, x, i), i + 1) : memo + +/** + * Creates a new array with the results of calling + * a provided function on every element in this array. + * @param {Array} xs + * @param {Function} fn + */ +const map = (xs, fn) => reduce(xs, (memo, x, i) => [...memo, fn(x, i)], []) + +/** + * Return the length of an array. + * @param {Array} xs + */ +const length = xs => reduce(xs, (memo, x) => memo + 1, 0) + +/** + * Returns a new array that contains the first n items of the given array. + * @param {Array} xs + * @param {Number} n + */ +const first = (xs, n) => reduce(xs, (memo, x, i) => i < n +? [...memo, x] : [...memo], []) + +/** + * Returns a new array that contains the last n items of the given array. + * @param {Array} xs + * @param {Number} n + */ +const last = (xs, n) => reduce(xs, (memo, x, i) => i >= (length(xs) - n) + ? [...memo, x] : [...memo], []) + +/** + * Return a reversed array. + * @param {Array} xs + */ +const reverse = xs => reduce(xs, (memo, x) => [x, ...memo], []) + +/** + * Reverse function argument order. + * @param {Function} fn + */ +const reverseArgs = (fn) => (...args) => fn(...reverse(args)) + +/** + * Similar to reduce, but applies the function from right-to-left. + * @param {Array} array + * @param {Function} fn + * @param {*} memo + */ +const reduceRight = (xs, fn, memo) => reduce(reverse(xs), fn, memo) + +/** + * Creates a new array with all elements that pass the test + * implemented by the provided function. + * @param {Array} xs + * @param {Function} fn + */ +const filter = (xs, fn) => reduce(xs, (memo, x) => fn(x) + ? [...memo, x] : [...memo], []) + +/** + * The opposite of filter, returns an array that does not pass the filter function. + * @param {Array} xs + * @param {Function} fn + */ +const reject = (xs, fn) => reduce(xs, (memo, x) => fn(x) + ? [...memo] : [...memo, x], []) + +/** + * Splits an array into two arrays. + * One whose items pass a filter function and one whose items fail. + * + * @param {Array} xs + * @param {Function} fn + */ +const partition = (xs, fn) => [filter(xs, fn), reject(xs, fn)] + +/** + * Return a new array with 2 items swapped based on their index. + * @param {Array} array + * @param {Number} i + * @param {Number} j + */ +const swap = (a, i, j) => ( + map(a, (x,y) => { + if(y === i) return a[j] + if(y === j) return a[i] + return x + }) +) + +/** + * Merges two lists into one + * @param {*} xs... + */ +const merge = spreadArg(xs => reduce(xs, (memo, x) => [...memo, ...x], [])) + +/** + * Combines nested arrays into a single array. + * @param {*} xs... + */ +const flatten = xs => reduce(xs, (memo, x) => x + ? is(x, Array) ? [...memo, ...flatten(x)] : [...memo, x] : [], []) + + +/** + * Each function consumes the return value of the function that came before. + * @param {*} args + */ +const flow = (...args) => init => reduce(args, (memo, fn) => fn(memo), init) + +/** + * The same as flow, but arguments are applied in the reverse order. + * @param {*} args + */ +const compose = (...args) => flow(...reverse(args)) + + +module.exports = { + def, + is, + copy, + head, + tail, + spreadArg, + partial, + pluck, + slice, + reduce, + map, + length, + first, + last, + reverse, + reverseArgs, + reduceRight, + filter, + reject, + partition, + swap, + merge, + flatten, + flow, + compose, +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d142e1c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,196 @@ +{ + "name": "fp", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2d94de0 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "fp", + "version": "0.1.0", + "description": "Functional Programming colllection of functions", + "main": "lib/index.js", + "files": [ + "lib/**/*.js" + ], + "scripts": { + "test": "mocha tests/" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/lightningspirit/fp.git" + }, + "keywords": [ + "functional", + "programming", + "functional", + "fp", + "library", + "lib", + "node", + "js", + "javascript", + "node.js" + ], + "author": { + "name": "lightningspirit", + "email": "lightningspirit@gmail.com", + "url": "https://lightningspirit.net" + }, + "contributors": [ + { + "name": "Casey Morris" + } + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/lightningspirit/fp/issues" + }, + "homepage": "https://github.com/lightningspirit/fp#readme", + "engines": { + "node": ">=8.11.1" + }, + "devDependencies": { + "mocha": "5.2.0" + } +} diff --git a/tests/fp_test.js b/tests/fp_test.js new file mode 100644 index 0000000..e5cc57c --- /dev/null +++ b/tests/fp_test.js @@ -0,0 +1,240 @@ +const fp = require('../lib/index') +const assert = require('assert') + +describe('#def()', () => { + it('should return true if value is defined', () => { + const defined = 'this is defined' + assert.equal(fp.def(defined), true) + }) + + it('should return false if value is not defined', () => { + assert.equal(fp.def(Array.notDefined), false) + }) +}) + +describe('#is()', () => { + it('should return true if value is array', () => { + assert.equal(fp.is([], Array), true) + }) + + it('should return true if value is string', () => { + assert.equal(fp.is('foo', String), true) + }) + + it('should return true if value is number', () => { + assert.equal(fp.is(3.141592, Number), true) + }) + + it('should return true if value is object', () => { + assert.equal(fp.is({}, Object), true) + }) + + it('should return true if value is function', () => { + assert.equal(fp.is(() => {}, Function), true) + }) +}) + +describe('#copy()', () => { + it('should return a new array', () => { + const list = [1,2,3] + assert.notStrictEqual(list, fp.copy(list)) + }) +}) + +describe('#head()', () => { + it('should return first element', () => { + assert.equal(fp.head([1,2,3]), 1) + }) +}) + +describe('#tail()', () => { + it('should return all but first element', () => { + assert.deepEqual(fp.tail([1,2,3]), [2,3]) + }) +}) + +describe('#spreadArg()', () => { + it('should transform function args into spread one', () => { + const add = ([x, ...xs]) => fp.def(x) ? parseInt(x + add(xs)) : [] + assert.equal(fp.spreadArg(add)(1,2,3), add([1,2,3])) + }) +}) + +describe('#partial()', () => { + it('should transform function args into spread one', () => { + const add = (x,y) => x + y + const add5to = fp.partial(add, 5) + assert.equal(add5to(10), 15) + }) +}) + +describe('#pluck()', () => { + it('should pluck property from object of the list', () => { + const product = {price: 15} + assert.equal(fp.pluck('price', product), 15) + + const getPrices = fp.partial(fp.pluck, 'price') + const products = [ + {price: 10}, + {price: 5}, + {price: 1} + ] + + assert.deepEqual(fp.map(products, getPrices), [10,5,1]) + }) +}) + +describe('#slice()', () => { + it('should add object in specified index', () => { + const array = [1,2,4,5] + assert.deepEqual(fp.slice(array, 2, 3), [1,2,3,4,5]) + }) +}) + +describe('#reduce()', () => { + it('should apply reduce to list', () => { + const sum = (memo, x) => memo + x + assert.equal(fp.reduce([1,2,3], sum, 0), 6) + + const flatten = (memo, x) => memo.concat(x) + assert.deepEqual(fp.reduce([4,5,6], flatten, [1,2,3]), [1,2,3,4,5,6]) + }) +}) + +describe('#map()', () => { + it('should apply map', () => { + const double = x => x * 2 + assert.deepEqual(fp.map([1,2,3], double), [2,4,6]) + }) +}) + +describe('#length()', () => { + it('should return length of list', () => { + const array = [1,2,3,4,5] + assert.equal(fp.length(array), 5) + }) +}) + +describe('#first()', () => { + it('should return first three elements', () => { + const array = [1,2,3,4,5] + assert.deepEqual(fp.first(array, 3), [1,2,3]) + }) +}) + +describe('#last()', () => { + it('should return last three elements', () => { + const array = [1,2,3,4,5] + assert.deepEqual(fp.last(array, 3), [3,4,5]) + }) +}) + +describe('#reverse()', () => { + it('should return reversed list', () => { + const array = [1,2,3] + assert.deepEqual(fp.reverse(array), [3,2,1]) + }) +}) + +describe('#reverseArgs()', () => { + it('should return reversed list', () => { + const divide = (x,y) => x / y + assert.equal(divide(100,10), 10) + + const reverseDivide = fp.reverseArgs(divide) + assert.equal(reverseDivide(100,10), 0.1) + }) +}) + +describe('#reduceRight()', () => { + it('should return reversed list', () => { + const flatten = (memo, x) => memo.concat(x) + const reduced = fp.reduceRight([[0,1], [2,3], [4,5]], flatten, []) + assert.deepEqual(reduced, [4, 5, 2, 3, 0, 1]) + }) +}) + +describe('#filter()', () => { + it('should filter list', () => { + const even = x => x % 2 === 0 + const odd = x => !even(x) + const array = [1,2,3,4,5] + + assert.deepEqual(fp.filter(array, even), [2,4]) + assert.deepEqual(fp.filter(array, odd), [1,3,5]) + }) +}) + +describe('#reject()', () => { + it('should reject some', () => { + const even = x => x % 2 === 0 + const array = [1,2,3,4,5] + + assert.deepEqual(fp.reject(array, even), [1,3,5]) + }) +}) + +describe('#partition()', () => { + it('should reject some', () => { + const even = x => x % 2 === 0 + const array = [0,1,2,3,4,5] + + assert.deepEqual(fp.partition(array, even), [[0,2,4], [1,3,5]]) + }) +}) + +describe('#swap()', () => { + it('should swap elements', () => { + const array = [1,2,3,4,5] + + assert.deepEqual(fp.swap(array, 0, 4), [5,2,3,4,1]) + }) +}) + +describe('#merge()', () => { + it('should merge lists', () => { + assert.deepEqual(fp.merge([1,2,3],[4,5,6]), [1,2,3,4,5,6]) + }) +}) + +describe('#flatten()', () => { + it('should flatten lists', () => { + assert.deepEqual(fp.flatten([1,[2,3,[4,[5,[[6]]]]]]), [1,2,3,4,5,6]) + }) +}) + +describe('#flow()', () => { + it('should flow functions', () => { + const getPrice = fp.partial(fp.pluck, 'price') + const discount = x => x * 0.9 + const tax = x => x + (x * 0.075) + + const getFinalPrice = fp.flow(getPrice, discount, tax) + + const products = [ + {price: 10}, + {price: 5}, + {price: 1} + ] + + assert.deepEqual(fp.map(products, getFinalPrice), [9.675, 4.8375, 0.9675]) + }) +}) + +describe('#compose()', () => { + it('should compose functions', () => { + const getPrice = fp.partial(fp.pluck, 'price') + const discount = x => x * 0.9 + const tax = x => x + (x * 0.075) + + const getFinalPrice = fp.compose(tax, discount, getPrice) + + const products = [ + {price: 10}, + {price: 5}, + {price: 1} + ] + + assert.deepEqual(fp.map(products, getFinalPrice), [9.675, 4.8375, 0.9675]) + }) +})