├── .gitignore ├── .travis.yml ├── README.md ├── babel.config.js ├── dist ├── mu-lambda.js └── mu-lambda.min.js ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── rollup.config.min.js ├── src └── index.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | nodejs: 3 | - "6" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # μλ 2 | 3 | [![Build Status](https://travis-ci.org/Khan/mu-lambda.svg?branch=master)](https://travis-ci.org/Khan/mu-lambda) 4 | 5 | A small library of functional programming utilities. 6 | 7 | ## function list 8 | 9 | All functions are curried. 10 | 11 | - all 12 | - some 13 | - compose 14 | - curry 15 | - eq 16 | - filter 17 | - find 18 | - findIndex 19 | - first 20 | - flatMap 21 | - flatten 22 | - fromEntries 23 | - fromPairs (alias for 'fromEntries') 24 | - id 25 | - identity (alias for 'id') 26 | - join 27 | - last 28 | - length 29 | - map 30 | - neq 31 | - none 32 | - pipe 33 | - prop 34 | - propEq 35 | - reduce 36 | - reduceRight 37 | - remove 38 | - replace 39 | - split 40 | - sum 41 | - uniq 42 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /dist/mu-lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | /** 6 | * A very small library of helpful functional programming methods. 7 | * 8 | * Temporary docs: http://ramdajs.com/docs/. 9 | */ 10 | 11 | const arity = (n, fn) => { 12 | switch (n) { 13 | case 0: return function() { return fn.apply(null, arguments) }; 14 | case 1: return function(a) { return fn.apply(null, arguments) }; 15 | case 2: return function(a, b) { return fn.apply(null, arguments) }; 16 | case 3: return function(a, b, c) { return fn.apply(null, arguments) }; 17 | case 4: return function(a, b, c, d) { return fn.apply(null, arguments) }; 18 | case 5: return function(a, b, c, d, e) { return fn.apply(null, arguments) }; 19 | default: { 20 | const args = []; 21 | for (let i = 0; i < n; i++) { 22 | args.push(`arg${i}`); 23 | } 24 | args.push('return fn.apply(null, arguments)'); 25 | return new Function(...args); 26 | } 27 | } 28 | }; 29 | 30 | const _curryN = (n, received, fn) => 31 | (...args) => { 32 | const combined = received.concat(args); 33 | const left = n - combined.length; 34 | return left <= 0 35 | ? fn(...combined) 36 | : arity(left, _curryN(n, combined, fn)); 37 | }; 38 | 39 | // curryN :: Number -> (* -> a) -> (* -> a) 40 | const curryN = (n, fn) => arity(n, _curryN(n, [], fn)); 41 | 42 | // curry :: (* -> a) -> (* -> a) 43 | const curry = (fn) => curryN(fn.length, fn); 44 | 45 | // reduce :: ((a, b) -> a) -> a -> [b] -> a 46 | const reduce = curry((f, init, arr) => arr.reduce(f, init)); 47 | 48 | // reduceRight :: ((a, b) -> a) -> a -> [b] -> a 49 | const reduceRight = curry((f, init, arr) => arr.reduceRight(f, init)); 50 | 51 | // compose :: Function f => [f] -> (a -> b) 52 | const compose = (...farr) => x => farr.reduceRight((v, f) => f(v), x); 53 | 54 | // pipe :: Function f => [f] -> (a -> b) 55 | const pipe = (...farr) => x => farr.reduce((v, f) => f(v), x); 56 | 57 | // map :: (a -> b) -> [a] -> [b] 58 | const map = curry((f, arr) => arr.map(f)); 59 | 60 | // filter :: (a -> Boolean) -> [a] -> [a] 61 | const filter = curry((f, arr) => arr.filter(f)); 62 | 63 | // find :: (a -> Boolean) -> [a] -> a 64 | const find = curry((f, arr) => arr.find(f)); 65 | 66 | // findIndex :: (a -> Boolean) -> [a] -> Number 67 | const findIndex = curry((f, arr) => arr.findIndex(f)); 68 | 69 | // flatten :: [a] -> [b] 70 | const flatten = reduce( 71 | (res, v) => res.concat(Array.isArray(v) ? flatten(v) : [v]), []); 72 | 73 | // flatMap :: (a -> [b]) -> [a] -> [b] 74 | const flatMap = curry((f, arr) => compose(flatten, map(f))(arr)); 75 | 76 | // all :: (a -> Boolean) -> [a] -> Boolean 77 | const all = curry((f, arr) => reduce((res, v) => res && f(v), true, arr)); 78 | 79 | // none :: (a -> Boolean) -> [a] -> Boolean 80 | const none = curry((f, arr) => !reduce((res, v) => res || f(v), false, arr)); 81 | 82 | // some :: (a -> Boolean) -> [a] -> Boolean 83 | const some = curry((f, arr) => reduce((res, v) => res || f(v), false, arr)); 84 | 85 | // split :: (String | RegExp) -> String -> [String] 86 | const split = curry((splitOn, str) => str.split(splitOn)); 87 | 88 | // replace :: (String | RegExp) -> String -> String 89 | const replace = curry((s, r, str) => str.replace(s, r)); 90 | 91 | // remove :: String a => a -> a -> a 92 | const remove = curry((s, str) => replace(s, '')(str)); 93 | 94 | // eq :: a -> b -> Boolean 95 | // NOTE: shallow equals 96 | const eq = curry((a, b) => a === b); 97 | 98 | // neq :: a -> b -> Boolean 99 | // NOTE: shallow not equals 100 | const neq = curry((a, b) => a !== b); 101 | 102 | // prop :: s -> {s: a} -> a | Undefined 103 | const prop = curry((k, obj) => obj[k]); 104 | 105 | // propEq :: String -> a -> Object -> Boolean 106 | const propEq = curry((k, v, obj) => compose(eq(v), prop(k))(obj)); 107 | 108 | // last :: [a] -> a 109 | const last = (arr) => arr[arr.length - 1]; 110 | 111 | // first :: [a] -> a 112 | const first = (arr) => arr[0]; 113 | 114 | // id :: a -> a 115 | const id = (a) => a; 116 | 117 | // length :: [a] -> Number 118 | const length = (arr) => arr.length; 119 | 120 | // fromEntries :: [[k,v]] → {k: v} 121 | const fromEntries = 122 | (entries) => reduce((obj, entry) => (obj[entry[0]] = entry[1], obj), {}, entries); 123 | 124 | // sum :: [Number] -> Number 125 | const sum = (arr) => reduce((res, v) => res + v, 0, arr); 126 | 127 | // tap :: fn -> a -> a 128 | const tap = curry((fn, a) => (fn(a), a)); 129 | 130 | // log :: a -> void 131 | const log = (a) => console.log(a); 132 | 133 | // join :: String -> [a] -> String 134 | const join = curry((s, arr) => arr.join(s)); 135 | 136 | // uniq :: [a] -> [a] 137 | // NOTE: doesn't handle structures 138 | const uniq = (arr) => reduce( 139 | (res, v) => res.indexOf(v) !== -1 ? res : (res.push(v), res), [], arr); 140 | 141 | exports.curryN = curryN; 142 | exports.curry = curry; 143 | exports.reduce = reduce; 144 | exports.reduceRight = reduceRight; 145 | exports.compose = compose; 146 | exports.pipe = pipe; 147 | exports.map = map; 148 | exports.filter = filter; 149 | exports.find = find; 150 | exports.findIndex = findIndex; 151 | exports.flatten = flatten; 152 | exports.flatMap = flatMap; 153 | exports.all = all; 154 | exports.none = none; 155 | exports.some = some; 156 | exports.split = split; 157 | exports.replace = replace; 158 | exports.remove = remove; 159 | exports.eq = eq; 160 | exports.neq = neq; 161 | exports.prop = prop; 162 | exports.propEq = propEq; 163 | exports.last = last; 164 | exports.first = first; 165 | exports.id = id; 166 | exports.length = length; 167 | exports.fromEntries = fromEntries; 168 | exports.sum = sum; 169 | exports.tap = tap; 170 | exports.log = log; 171 | exports.join = join; 172 | exports.uniq = uniq; 173 | -------------------------------------------------------------------------------- /dist/mu-lambda.min.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const arity=(r,e)=>{switch(r){case 0:return function(){return e.apply(null,arguments)};case 1:return function(r){return e.apply(null,arguments)};case 2:return function(r,t){return e.apply(null,arguments)};case 3:return function(r,t,p){return e.apply(null,arguments)};case 4:return function(r,t,p,n){return e.apply(null,arguments)};case 5:return function(r,t,p,n,u){return e.apply(null,arguments)};default:{const e=[];for(let t=0;t(...p)=>{const n=e.concat(p),u=r-n.length;return u<=0?t(...n):arity(u,_curryN(r,n,t))},curryN=(r,e)=>arity(r,_curryN(r,[],e)),curry=r=>curryN(r.length,r),reduce=curry((r,e,t)=>t.reduce(r,e)),reduceRight=curry((r,e,t)=>t.reduceRight(r,e)),compose=(...r)=>e=>r.reduceRight((r,e)=>e(r),e),pipe=(...r)=>e=>r.reduce((r,e)=>e(r),e),map=curry((r,e)=>e.map(r)),filter=curry((r,e)=>e.filter(r)),find=curry((r,e)=>e.find(r)),findIndex=curry((r,e)=>e.findIndex(r)),flatten=reduce((r,e)=>r.concat(Array.isArray(e)?flatten(e):[e]),[]),flatMap=curry((r,e)=>compose(flatten,map(r))(e)),all=curry((r,e)=>reduce((e,t)=>e&&r(t),!0,e)),none=curry((r,e)=>!reduce((e,t)=>e||r(t),!1,e)),some=curry((r,e)=>reduce((e,t)=>e||r(t),!1,e)),split=curry((r,e)=>e.split(r)),replace=curry((r,e,t)=>t.replace(r,e)),remove=curry((r,e)=>replace(r,"")(e)),eq=curry((r,e)=>r===e),neq=curry((r,e)=>r!==e),prop=curry((r,e)=>e[r]),propEq=curry((r,e,t)=>compose(eq(e),prop(r))(t)),last=r=>r[r.length-1],first=r=>r[0],id=r=>r,length=r=>r.length,fromEntries=r=>reduce((r,e)=>(r[e[0]]=e[1],r),{},r),sum=r=>reduce((r,e)=>r+e,0,r),tap=curry((r,e)=>(r(e),e)),log=r=>console.log(r),join=curry((r,e)=>e.join(r)),uniq=r=>reduce((r,e)=>-1!==r.indexOf(e)?r:(r.push(e),r),[],r);exports.curryN=curryN,exports.curry=curry,exports.reduce=reduce,exports.reduceRight=reduceRight,exports.compose=compose,exports.pipe=pipe,exports.map=map,exports.filter=filter,exports.find=find,exports.findIndex=findIndex,exports.flatten=flatten,exports.flatMap=flatMap,exports.all=all,exports.none=none,exports.some=some,exports.split=split,exports.replace=replace,exports.remove=remove,exports.eq=eq,exports.neq=neq,exports.prop=prop,exports.propEq=propEq,exports.last=last,exports.first=first,exports.id=id,exports.length=length,exports.fromEntries=fromEntries,exports.sum=sum,exports.tap=tap,exports.log=log,exports.join=join,exports.uniq=uniq; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/0p/b3gdtwl52fs9b3b14dd8g1d00000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files usin a array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: "node", 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | // testMatch: [ 142 | // "**/__tests__/**/*.[jt]s?(x)", 143 | // "**/?(*.)+(spec|test).[tj]s?(x)" 144 | // ], 145 | 146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 147 | // testPathIgnorePatterns: [ 148 | // "/node_modules/" 149 | // ], 150 | 151 | // The regexp pattern or array of patterns that Jest uses to detect test files 152 | // testRegex: [], 153 | 154 | // This option allows the use of a custom results processor 155 | // testResultsProcessor: null, 156 | 157 | // This option allows use of a custom test runner 158 | // testRunner: "jasmine2", 159 | 160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 161 | // testURL: "http://localhost", 162 | 163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 164 | // timers: "real", 165 | 166 | // A map from regular expressions to paths to transformers 167 | // transform: null, 168 | 169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 170 | // transformIgnorePatterns: [ 171 | // "/node_modules/" 172 | // ], 173 | 174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 175 | // unmockedModulePathPatterns: undefined, 176 | 177 | // Indicates whether each individual test should be reported during the run 178 | // verbose: null, 179 | 180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 181 | // watchPathIgnorePatterns: [], 182 | 183 | // Whether to use watchman for file crawling 184 | // watchman: true, 185 | }; 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mu-lambda", 3 | "version": "0.0.3", 4 | "description": "", 5 | "main": "dist/mu-lambda.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "jest", 11 | "build": "rollup -c rollup.config.js", 12 | "build-min": "rollup -c rollup.config.min.js" 13 | }, 14 | "author": "Kevin Barabash ", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/core": "^7.2.2", 18 | "@babel/preset-env": "^7.3.1", 19 | "babel-jest": "^24.1.0", 20 | "jest": "^24.1.0", 21 | "rollup": "^1.1.2", 22 | "rollup-plugin-terser": "^4.0.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | input: 'src/index.js', 3 | output: { 4 | file: 'dist/mu-lambda.js', 5 | format: 'cjs' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /rollup.config.min.js: -------------------------------------------------------------------------------- 1 | import {terser} from "rollup-plugin-terser"; 2 | 3 | module.exports = { 4 | input: 'src/index.js', 5 | output: { 6 | file: 'dist/mu-lambda.min.js', 7 | format: 'cjs' 8 | }, 9 | plugins: [terser()] 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A very small library of helpful functional programming methods. 3 | * 4 | * Temporary docs: http://ramdajs.com/docs/. 5 | */ 6 | 7 | const arity = (n, fn) => { 8 | switch (n) { 9 | case 0: return function() { return fn.apply(null, arguments) }; 10 | case 1: return function(a) { return fn.apply(null, arguments) }; 11 | case 2: return function(a, b) { return fn.apply(null, arguments) }; 12 | case 3: return function(a, b, c) { return fn.apply(null, arguments) }; 13 | case 4: return function(a, b, c, d) { return fn.apply(null, arguments) }; 14 | case 5: return function(a, b, c, d, e) { return fn.apply(null, arguments) }; 15 | default: { 16 | const args = []; 17 | for (let i = 0; i < n; i++) { 18 | args.push(`arg${i}`); 19 | } 20 | args.push('return fn.apply(null, arguments)'); 21 | return new Function(...args); 22 | } 23 | } 24 | } 25 | 26 | const _curryN = (n, received, fn) => 27 | (...args) => { 28 | const combined = received.concat(args); 29 | const left = n - combined.length; 30 | return left <= 0 31 | ? fn(...combined) 32 | : arity(left, _curryN(n, combined, fn)); 33 | }; 34 | 35 | // curryN :: Number -> (* -> a) -> (* -> a) 36 | export const curryN = (n, fn) => arity(n, _curryN(n, [], fn)); 37 | 38 | // curry :: (* -> a) -> (* -> a) 39 | export const curry = (fn) => curryN(fn.length, fn); 40 | 41 | // reduce :: ((a, b) -> a) -> a -> [b] -> a 42 | export const reduce = curry((f, init, arr) => arr.reduce(f, init)); 43 | 44 | // reduceRight :: ((a, b) -> a) -> a -> [b] -> a 45 | export const reduceRight = curry((f, init, arr) => arr.reduceRight(f, init)); 46 | 47 | // compose :: Function f => [f] -> (a -> b) 48 | export const compose = (...farr) => x => farr.reduceRight((v, f) => f(v), x); 49 | 50 | // pipe :: Function f => [f] -> (a -> b) 51 | export const pipe = (...farr) => x => farr.reduce((v, f) => f(v), x); 52 | 53 | // map :: (a -> b) -> [a] -> [b] 54 | export const map = curry((f, arr) => arr.map(f)); 55 | 56 | // filter :: (a -> Boolean) -> [a] -> [a] 57 | export const filter = curry((f, arr) => arr.filter(f)); 58 | 59 | // find :: (a -> Boolean) -> [a] -> a 60 | export const find = curry((f, arr) => arr.find(f)); 61 | 62 | // findIndex :: (a -> Boolean) -> [a] -> Number 63 | export const findIndex = curry((f, arr) => arr.findIndex(f)); 64 | 65 | // flatten :: [a] -> [b] 66 | export const flatten = reduce( 67 | (res, v) => res.concat(Array.isArray(v) ? flatten(v) : [v]), []); 68 | 69 | // flatMap :: (a -> [b]) -> [a] -> [b] 70 | export const flatMap = curry((f, arr) => compose(flatten, map(f))(arr)); 71 | 72 | // all :: (a -> Boolean) -> [a] -> Boolean 73 | export const all = curry((f, arr) => reduce((res, v) => res && f(v), true, arr)); 74 | 75 | // none :: (a -> Boolean) -> [a] -> Boolean 76 | export const none = curry((f, arr) => !reduce((res, v) => res || f(v), false, arr)); 77 | 78 | // some :: (a -> Boolean) -> [a] -> Boolean 79 | export const some = curry((f, arr) => reduce((res, v) => res || f(v), false, arr)); 80 | 81 | // split :: (String | RegExp) -> String -> [String] 82 | export const split = curry((splitOn, str) => str.split(splitOn)); 83 | 84 | // replace :: (String | RegExp) -> String -> String 85 | export const replace = curry((s, r, str) => str.replace(s, r)); 86 | 87 | // remove :: String a => a -> a -> a 88 | export const remove = curry((s, str) => replace(s, '')(str)); 89 | 90 | // eq :: a -> b -> Boolean 91 | // NOTE: shallow equals 92 | export const eq = curry((a, b) => a === b); 93 | 94 | // neq :: a -> b -> Boolean 95 | // NOTE: shallow not equals 96 | export const neq = curry((a, b) => a !== b); 97 | 98 | // prop :: s -> {s: a} -> a | Undefined 99 | export const prop = curry((k, obj) => obj[k]); 100 | 101 | // propEq :: String -> a -> Object -> Boolean 102 | export const propEq = curry((k, v, obj) => compose(eq(v), prop(k))(obj)); 103 | 104 | // last :: [a] -> a 105 | export const last = (arr) => arr[arr.length - 1]; 106 | 107 | // first :: [a] -> a 108 | export const first = (arr) => arr[0]; 109 | 110 | // id :: a -> a 111 | export const id = (a) => a; 112 | 113 | // length :: [a] -> Number 114 | export const length = (arr) => arr.length; 115 | 116 | // fromEntries :: [[k,v]] → {k: v} 117 | export const fromEntries = 118 | (entries) => reduce((obj, entry) => (obj[entry[0]] = entry[1], obj), {}, entries); 119 | 120 | // sum :: [Number] -> Number 121 | export const sum = (arr) => reduce((res, v) => res + v, 0, arr); 122 | 123 | // tap :: fn -> a -> a 124 | export const tap = curry((fn, a) => (fn(a), a)); 125 | 126 | // log :: a -> void 127 | export const log = (a) => console.log(a); 128 | 129 | // join :: String -> [a] -> String 130 | export const join = curry((s, arr) => arr.join(s)); 131 | 132 | // uniq :: [a] -> [a] 133 | // NOTE: doesn't handle structures 134 | export const uniq = (arr) => reduce( 135 | (res, v) => res.indexOf(v) !== -1 ? res : (res.push(v), res), [], arr); 136 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for fp-utils.js. 3 | */ 4 | const assert = require('assert'); 5 | 6 | const fp = require('../src/index.js'); 7 | 8 | describe('fp-utils', () => { 9 | describe('curry', () => { 10 | it('should return a curried function', () => { 11 | const add = fp.curry((a, b, c) => a + b + c); 12 | 13 | assert.equal(add(1, 2, 3), 6); 14 | assert.equal(add(1)(2, 3), 6); 15 | assert.equal(add(1, 2)(3), 6); 16 | assert.equal(add(1)(2)(3), 6); 17 | }); 18 | 19 | it('should maintain the function length', () => { 20 | const add = fp.curry((a, b, c) => a + b + c); 21 | 22 | assert.equal(add.length, 3); 23 | }); 24 | }); 25 | 26 | describe('curryN', () => { 27 | it('should return a function with the specified length', () => { 28 | const max3 = fp.curryN(3, (...args) => Math.max(...args)); 29 | 30 | assert.equal(max3.length, 3); 31 | assert.equal(max3(3, 7, 5), 7); 32 | }); 33 | }); 34 | 35 | describe('reduce', () => { 36 | it('should concat strings from left to right', () => { 37 | const actual = fp.reduce((res, v) => res + v, '')(['a', 'b', 'c']); 38 | assert.equal(actual, 'abc'); 39 | }); 40 | }); 41 | 42 | describe('reduceRight', () => { 43 | it('should concat strings from right to left', () => { 44 | const actual = 45 | fp.reduceRight((res, v) => res + v, '')(['a', 'b', 'c']); 46 | assert.equal(actual, 'cba'); 47 | }); 48 | }); 49 | 50 | describe('compose', () => { 51 | it('should apply functions from right to left', () => { 52 | const foo = fp.compose((v) => 2 * v, (v) => v + 5); 53 | const actual = foo(10); 54 | assert.equal(actual, 30); 55 | }); 56 | }); 57 | 58 | describe('pipe', () => { 59 | it('should apply functions from left to right', () => { 60 | const foo = fp.pipe((v) => 2 * v, (v) => v + 5); 61 | const actual = foo(10); 62 | assert.equal(actual, 25); 63 | }); 64 | }); 65 | 66 | describe('map', () => { 67 | it('should apply the function to each item', () => { 68 | const exclaim = (v) => `${v}!`; 69 | const actual = fp.map(exclaim)(['a', 'b', 'c']); 70 | assert.deepEqual(actual, ['a!', 'b!', 'c!']); 71 | }); 72 | }); 73 | 74 | describe('filter', () => { 75 | it('should exclude items that do satisfy predicate', () => { 76 | const isEven = (v) => v % 2 === 0; 77 | const actual = fp.filter(isEven)([1, 2, 3]); 78 | assert.deepEqual(actual, [2]); 79 | }); 80 | }); 81 | 82 | describe('find', () => { 83 | it('should return the first item that satisfies the predicate', () => { 84 | const hasName = (v) => v.hasOwnProperty('name'); 85 | const actual = fp.find(hasName)([ 86 | {x: 5, y: 10}, 87 | {name: 'foo'}, 88 | {name: 'bar'}, 89 | ]); 90 | assert.deepEqual(actual, {name: 'foo'}); 91 | }); 92 | }); 93 | 94 | describe('findIndex', () => { 95 | it('returns index of the first item satisfying the predicate', () => { 96 | const hasName = (v) => v.hasOwnProperty('name'); 97 | const actual = fp.findIndex(hasName)([ 98 | {x: 5, y: 10}, 99 | {name: 'foo'}, 100 | {name: 'bar'}, 101 | ]); 102 | assert.equal(actual, 1); 103 | }); 104 | }); 105 | 106 | describe('flatten', () => { 107 | it('should not change flat arrays', () => { 108 | const actual = fp.flatten(['a', 'b', 'c']); 109 | assert.deepEqual(actual, ['a', 'b', 'c']); 110 | }); 111 | 112 | it('should ignore empty arrays', () => { 113 | const actual = fp.flatten(['a', 'b', 'c', []]); 114 | assert.deepEqual(actual, ['a', 'b', 'c']); 115 | }); 116 | 117 | it('should flatten nested arrays', () => { 118 | const actual = fp.flatten([['a'], ['b', 'c']]); 119 | assert.deepEqual(actual, ['a', 'b', 'c']); 120 | }); 121 | 122 | it('should flatten multiple levels of nested arrays', () => { 123 | const actual = fp.flatten([[[], ['a']], [['b', []], 'c']]); 124 | assert.deepEqual(actual, ['a', 'b', 'c']); 125 | }); 126 | }); 127 | 128 | describe('flatMap', () => { 129 | it('should map and then flatten', () => { 130 | const exclaim = (v) => `${v}!`; 131 | const actual = fp.flatMap(fp.map(exclaim))([['a'], ['b', 'c']]); 132 | assert.deepEqual(actual, ['a!', 'b!', 'c!']); 133 | }); 134 | }); 135 | 136 | describe('split', () => { 137 | it('should split a string based on a separator string', () => { 138 | const actual = fp.split('&')('x=5&y=10'); 139 | assert.deepEqual(actual, ['x=5', 'y=10']); 140 | }); 141 | 142 | it('should split a string based on a separator regex', () => { 143 | const actual = fp.split(/[&\=]/)('x=5&y=10'); 144 | assert.deepEqual(actual, ['x', '5', 'y', '10']); 145 | }); 146 | }); 147 | 148 | describe('replace', () => { 149 | it('should replace the first instance of a string', () => { 150 | const actual = fp.replace('!', '?')('a!b!c!'); 151 | assert.deepEqual(actual, 'a?b!c!'); 152 | }); 153 | 154 | it('should replace all instances of a global regex with', () => { 155 | const actual = fp.replace(/!/g, '?')('a!b!c!'); 156 | assert.deepEqual(actual, 'a?b?c?'); 157 | }); 158 | }); 159 | 160 | describe('remove', () => { 161 | it('should remove the first instance of a string', () => { 162 | const actual = fp.remove('!')('a!b!c!'); 163 | assert.deepEqual(actual, 'ab!c!'); 164 | }); 165 | 166 | it('should remove all instances of a global regex with', () => { 167 | const actual = fp.remove(/!/g)('a!b!c!'); 168 | assert.deepEqual(actual, 'abc'); 169 | }); 170 | }); 171 | 172 | describe('prop', () => { 173 | it('should return the property of an object', () => { 174 | const actual = fp.prop('x')({x: 5, y: 10}); 175 | assert.equal(actual, 5); 176 | }); 177 | 178 | it('should return undefined if it does not exist', () => { 179 | const actual = fp.prop('z')({x: 5, y: 10}); 180 | assert.equal(actual, undefined); 181 | }); 182 | }); 183 | 184 | describe('some', () => { 185 | it('should return true if some item satisfies the predicate', () => { 186 | const someEven = fp.some((v) => v % 2 === 0); 187 | assert(someEven([1, 2, 3])); 188 | }); 189 | 190 | it('should return false if no item satisfies the predicate', () => { 191 | const someEven = fp.some((v) => v % 2 === 0); 192 | assert.equal(someEven([1, 3, 5]), false); 193 | }); 194 | }); 195 | 196 | describe('all', () => { 197 | it('should return true if all items satisfy the predicate', () => { 198 | const allEven = fp.all((v) => v % 2 === 0); 199 | assert(allEven([2, 4, 6])); 200 | }); 201 | 202 | it('should return false if at least one item fails predicate', () => { 203 | const allEven = fp.all((v) => v % 2 === 0); 204 | assert.equal(allEven([2, 4, 5]), false); 205 | }); 206 | }); 207 | 208 | describe('none', () => { 209 | it('should return true if no items satisfy the predicate', () => { 210 | const noneEven = fp.none((v) => v % 2 === 0); 211 | assert(noneEven([1, 3, 5])); 212 | }); 213 | 214 | it('returns false if at least one item satisfies predicate', () => { 215 | const noneEven = fp.none((v) => v % 2 === 0); 216 | assert.equal(noneEven([1, 3, 4]), false); 217 | }); 218 | }); 219 | 220 | describe('fromEntries', () => { 221 | it('should create an object from an array of entry pairs', () => { 222 | const obj = fp.fromEntries([['x', 5], ['y', 10]]); 223 | assert.deepEqual(obj, {x: 5, y: 10}); 224 | }); 225 | }); 226 | 227 | describe('eq', () => { 228 | it('should provide same behavior as ===', () => { 229 | assert.equal(fp.eq(1, 1), true); 230 | assert.equal(fp.eq('1', '1'), true); 231 | assert.equal(fp.eq(1, '1'), false); 232 | }); 233 | }); 234 | 235 | describe('neq', () => { 236 | it('should provide the same behavior as !==', () => { 237 | assert.equal(fp.neq(1, 1), false); 238 | assert.equal(fp.neq('1', '1'), false); 239 | assert.equal(fp.neq(1, '1'), true); 240 | }); 241 | }); 242 | 243 | describe('length', () => { 244 | it('should return the length of an array', () => { 245 | assert.equal(fp.length([1, 2, 3]), 3); 246 | assert.equal(fp.length([]), 0); 247 | }); 248 | }); 249 | 250 | describe('id', () => { 251 | it('should return whatever value is passed to it', () => { 252 | const point = {x: 5, y: 10}; 253 | assert.equal(fp.id(point), point); 254 | }); 255 | }); 256 | 257 | describe('last', () => { 258 | it('should return the last item in an array', () => { 259 | assert.equal(fp.last([1, 2, 3]), 3); 260 | assert.equal(fp.last([]), undefined); 261 | }); 262 | }); 263 | 264 | describe('propEq', () => { 265 | it('should return true if the prop exists and value matches', () => { 266 | const actual = fp.propEq('x', 5)({x: 5, y: 10}); 267 | assert(actual); 268 | }); 269 | 270 | it('should return false if the prop does not exist', () => { 271 | const actual = fp.propEq('z', 5)({x: 5, y: 10}); 272 | assert.equal(actual, false); 273 | }); 274 | 275 | it('should return false if the value does not match', () => { 276 | const actual = fp.propEq('z', 10)({x: 5, y: 10}); 277 | assert.equal(actual, false); 278 | }); 279 | }); 280 | 281 | describe('sum', () => { 282 | it('should return the sum of all the numbers', () => { 283 | const actual = fp.sum([1, 2, 3]); 284 | assert.equal(actual, 6); 285 | }); 286 | }); 287 | 288 | describe('join', () => { 289 | it('should return a string of items joined by separator', () => { 290 | const actual = fp.join('|', [1, 2, 3]); 291 | assert.equal(actual, '1|2|3'); 292 | }); 293 | }); 294 | 295 | describe('uniq', () => { 296 | it('should remove duplicate values from an array', () => { 297 | const actual = fp.uniq([1, 1, 2, 1]); 298 | assert.deepEqual(actual, [1, 2]); 299 | }); 300 | }); 301 | 302 | describe('tap', () => { 303 | it('should return the second param unaltered', () => { 304 | const actual = fp.tap(fp.log, 'hello, world'); 305 | assert.equal(actual, 'hello, world'); 306 | }); 307 | }); 308 | }); 309 | --------------------------------------------------------------------------------