├── .gitignore ├── .babelrc ├── .travis.yml ├── example ├── map.js ├── fizzbuzz.js ├── math_tree.js └── user_age.js ├── package.json ├── test └── test.js ├── README.md └── src └── redp.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 5.0 4 | - 4.2 5 | -------------------------------------------------------------------------------- /example/map.js: -------------------------------------------------------------------------------- 1 | import {match, when, Pattern} from '..' 2 | 3 | const {_, rest} = Pattern 4 | 5 | function map(array, func) { 6 | return match(array, { 7 | [when([])] : [], 8 | [when([_, rest])]: array => [func(array[0])].concat(map(array.slice(1), func)), 9 | }) 10 | } 11 | 12 | console.log(map([1, 2, 3, 4, 5], val => val * 2)) // => [2, 4, 6, 8, 10] 13 | -------------------------------------------------------------------------------- /example/fizzbuzz.js: -------------------------------------------------------------------------------- 1 | import {match, when, Pattern} from '..' 2 | 3 | const {_} = Pattern 4 | 5 | const isFizz = n => n % 3 === 0 6 | const isBuzz = n => n % 5 === 0 7 | 8 | const fizzbuzz = match.fn({ 9 | [when.and(isFizz, isBuzz)]: 'FizzBuzz', 10 | [when(isFizz)] : 'Fizz', 11 | [when(isBuzz)] : 'Buzz', 12 | [when(_)] : n => n, 13 | }) 14 | 15 | const array = [] 16 | for (let i = 1; i <= 15; i++) { 17 | array.push(fizzbuzz(i)) 18 | } 19 | 20 | console.log(array) //=> [1, 2, 'Fizz', 4, 'Buzz', ...] 21 | -------------------------------------------------------------------------------- /example/math_tree.js: -------------------------------------------------------------------------------- 1 | import {match, when} from '..' 2 | 3 | function binary(op, left, right) { 4 | return { 5 | type: 'binary', 6 | operator: op, 7 | left, right 8 | } 9 | } 10 | 11 | function unary(op, value) { 12 | return { 13 | type: 'unary', 14 | operator: op, 15 | value 16 | } 17 | } 18 | 19 | function number(value) { 20 | return { 21 | type: 'number', 22 | value 23 | } 24 | } 25 | 26 | // -100 * 200 + 300 27 | const tree = binary('+', binary('*', unary('-', number(100)), number(200)), number(300)) 28 | 29 | const calc = match.fn({ 30 | [when({type: 'binary', operator: '+'})]: ({left, right}) => calc(left) + calc(right), 31 | [when({type: 'binary', operator: '*'})]: ({left, right}) => calc(left) * calc(right), 32 | [when({type: 'unary' , operator: '-'})]: ({value}) => -calc(value), 33 | [when({type: 'number'})] : ({value}) => value, 34 | }) 35 | 36 | console.log(calc(tree)) //=> -19700 37 | -------------------------------------------------------------------------------- /example/user_age.js: -------------------------------------------------------------------------------- 1 | import {match, when, Pattern} from '..' 2 | 3 | import * as faker from 'faker' 4 | 5 | const {_} = Pattern 6 | 7 | function lt(n) { 8 | return m => m < n 9 | } 10 | 11 | function collectAgeGroup(users) { 12 | const ageGroup = new Map() 13 | const pattern = {} 14 | 15 | for (let i = 0; i < 10; i++) { 16 | const age = i * 10 17 | const key = ` ${age ? age : ' 0'}+` 18 | 19 | ageGroup.set(key, []) 20 | pattern[when({old: lt(age + 10)})] = ({name}) => ageGroup.get(key).push(name) 21 | } 22 | ageGroup.set('100+', []) 23 | pattern[when(_)] = ({name}) => ageGroup.get('100+').push(name) 24 | 25 | users.forEach(match.fn(pattern)) 26 | 27 | return ageGroup 28 | } 29 | 30 | const users = [] 31 | for (let i = 0; i < 30; i++) { 32 | users.push({old: ~~(Math.random() * 110), name: faker.name.findName()}) 33 | } 34 | 35 | for (let [age, names] of collectAgeGroup(users)) { 36 | console.log(`${age}: ${names.join(', ')}`) 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redp", 3 | "description": "Red Phosphorus - Pattern Match Library for ES2015 Era", 4 | "version": "1.0.1", 5 | "author": "TSUYUSATO Kitsune ", 6 | "bugs": { 7 | "url": "https://github.com/makenowjust/redp/issues" 8 | }, 9 | "devDependencies": { 10 | "ava": "^0.9.1", 11 | "babel-cli": "^6.3.17", 12 | "babel-preset-es2015": "^6.3.13", 13 | "faker": "^3.0.1", 14 | "npm-run-all": "^1.4.0" 15 | }, 16 | "files": [ 17 | "lib/", 18 | "src/" 19 | ], 20 | "homepage": "https://github.com/MakeNowJust/redp", 21 | "keywords": [ 22 | "es2015", 23 | "es6", 24 | "match", 25 | "pattern", 26 | "when" 27 | ], 28 | "license": "MIT", 29 | "main": "lib/redp.js", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/makenowjust/redp.git" 33 | }, 34 | "scripts": { 35 | "build": "babel -d lib src", 36 | "clean": "rm -rf lib", 37 | "prepublish": "npm-run-all clean build", 38 | "test": "npm-run-all test:*", 39 | "test:ava": "ava", 40 | "test:example": "cd ./example/ && ls | xargs -tL1 babel-node" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import {match, when, Pattern} from '..' 2 | 3 | import test from 'ava' 4 | 5 | const {_, rest} = Pattern 6 | 7 | test('value', t => { 8 | ['test', 42, false, undefined, null, Symbol('test')].forEach(val => { 9 | t.true(match(val, {[when(val)]: true})) 10 | t.is(match('not match', {[when(val)]: true}), undefined) 11 | }) 12 | }) 13 | 14 | test('regexp', t => { 15 | const val = '==test==' 16 | const regexp = /test/ 17 | 18 | t.true(match(val, {[when(regexp)]: true})) 19 | t.is(match('not match', {[when(regexp)]: true}), undefined) 20 | }) 21 | 22 | test('array', t => { 23 | const array = ['test', 42, false, undefined, null, Symbol('test'), /test/] 24 | const val = array.slice() 25 | 26 | val[val.length - 1] = '==test==' 27 | t.true(match(val, {[when(array)]: true})) 28 | 29 | val[val.length - 1] = 'not match' 30 | t.is(match(val, {[when(array)]: true}), undefined) 31 | 32 | array[val.length - 1] = rest 33 | t.true(match(val, {[when(array)]: true})) 34 | }) 35 | 36 | test('object', t => { 37 | const object = { 38 | string: 'test', 39 | number: 42, 40 | boolean: false, 41 | undefined: undefined, 42 | null: null, 43 | symbol: Symbol('test'), 44 | regexp: /test/, 45 | } 46 | const val = Object.keys(object).reduce((val, key) => { 47 | val[key] = object[key] 48 | return val 49 | }, {}) 50 | val.regexp = '==test==' 51 | 52 | t.true(match(val, {[when(object)]: true})) 53 | 54 | val.regexp = 'not match' 55 | t.is(match(val, {[when(object)]: true}), undefined) 56 | }) 57 | 58 | test('condition', t => { 59 | t.plan(2) 60 | 61 | const pred = () => { 62 | t.pass() 63 | return true 64 | } 65 | 66 | t.true(match(null, {[when(pred)]: true})) 67 | }) 68 | 69 | test('wild', t => { 70 | t.true(match(null, {[when(_)]: true})) 71 | }) 72 | 73 | test('or', t => { 74 | const pats = ['test', 42] 75 | 76 | t.true(match('test', {[when.or(...pats)]: true})) 77 | t.true(match(42, {[when.or(...pats)]: true})) 78 | }) 79 | 80 | test('and', t => { 81 | const pats = [/test/, /^==|==$/] 82 | 83 | t.true(match('==test', {[when.and(...pats)]: true})) 84 | t.true(match('test==', {[when.and(...pats)]: true})) 85 | }) 86 | 87 | test(t => { 88 | ['test', 42, false, undefined, null, Symbol('test'), ['hello', 'world'], {hello: 'world'}].forEach(val => { 89 | const pat = Pattern.from(val) 90 | 91 | t.true(Pattern.is(pat)) 92 | t.true(match(val, {[when(pat)]: true})) 93 | t.is(match('not match', {[when(pat)]: true}), undefined) 94 | }) 95 | 96 | ;[ 97 | [/test/, 'test'], 98 | [(test => /test/.test(test)), 'test'], 99 | ].forEach(([pat, val]) => { 100 | pat = Pattern.from(pat) 101 | 102 | t.true(Pattern.is(pat)) 103 | t.true(match(val, {[when(pat)]: true})) 104 | t.is(match('not match', {[when(pat)]: true}), undefined) 105 | }) 106 | 107 | const pats = [/test/, /==/] 108 | 109 | ;['and', 'or'].forEach(op => { 110 | const pat = Pattern[op](...pats) 111 | 112 | t.true(Pattern.is(pat)) 113 | t.true(match('==test==', {[when(pat)]: true})) 114 | t.is(match('not match', {[when(pat)]: true}), undefined) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redp 2 | 3 | Red Phosphorus - Pattern Match Library for ES2015 Era 4 | 5 | [![Build Status](https://travis-ci.org/MakeNowJust/redp.svg?branch=master)](https://travis-ci.org/MakeNowJust/redp) 6 | 7 | 8 | ## example 9 | 10 | ```javascript 11 | import {match, when} from 'redp' 12 | 13 | // create function to calculate expression tree 14 | const calc = match.fn({ 15 | [when({type: 'binary', operator: '+'})]: ({left, right}) => calc(left) + calc(right), 16 | [when({type: 'binary', operator: '*'})]: ({left, right}) => calc(left) * calc(right), 17 | [when({type: 'unary' , operator: '-'})]: ({value}) => -calc(value), 18 | [when({type: 'number'})] : ({value}) => value, 19 | }) 20 | 21 | // -100 + 200 ==> 100 22 | console.log(calc({ 23 | type: 'binary', 24 | operator: '+', 25 | left: { 26 | type: 'unary', 27 | operator: '-', 28 | value: { 29 | type: 'number', 30 | value: 100, 31 | }, 32 | }, 33 | right: { 34 | type: 'number', 35 | value: 200, 36 | }, 37 | })) 38 | ``` 39 | 40 | You could be look for more examples in [example/](./example/) and [test/](./test/). 41 | 42 | 43 | ## install 44 | 45 | ```console 46 | $ npm install --save redp 47 | ``` 48 | 49 | 50 | ## API 51 | 52 | ```javascript 53 | import {match, when, Pattern} from 'redp' 54 | ``` 55 | 56 | ### `match(value, pattern)` 57 | 58 | Shortcut to `match.fn(pattern)(value)`. 59 | 60 | ### `match.fn(patterns)` 61 | 62 | Create new function to match `patterns`. `patterns` should be an `Object`. Its keys should be created by `when` function. Returned function accept one argument then try to match `patterns`. Matching some pattern, return it. If not, return `undefined`. 63 | 64 | ### `when(pattern)` 65 | 66 | Create pattern *key* from `pattern`. `pattern` is either some javascript value (`"string"` or `true` or `/regexp/` or `(arrow) => func` or ...) or `Pattern.*` function's result. If `pattern` is some javascript value, it is passed to `Pattern.from`. 67 | 68 | Pattern key is not reusable. If you want to reuse `pattern`, you could use `Pattern.from`. 69 | 70 | ### `when.and(...patterns)` 71 | 72 | Shortcut to `when(Pattern.and(...patterns))`. 73 | 74 | ### `when.or(...patterns)` 75 | 76 | Shortcut to `when(Pattern.or(...patterns))`. 77 | 78 | ### `Pattern` 79 | 80 | This is a factory for pattern object. 81 | 82 | ```javascript 83 | export const Pattern = { 84 | // Wild card pattern. It can match all values. 85 | _: ..., 86 | 87 | // Rest pattern. It can match no items or some rest items of array (cannot object!). 88 | rest: ..., 89 | 90 | // Create pattern object from `object`. 91 | from(object) { 92 | // If `object` is pattern object already, return `object`. 93 | // If `object` is a `RegExp`, create regexp pattern. 94 | // If `object` is an `Array`, create array pattern. 95 | // If `object` is a javascript object, create object pattern. 96 | // If `object` is a function, create condition pattern. 97 | // Otherwise, create value pattern. 98 | ... 99 | }, 100 | 101 | // Create regexp pattern. 102 | regexp(regexp) { ... }, 103 | 104 | // Create array pattern. 105 | array(array) { ... }, 106 | 107 | // Create object pattern. 108 | object(object) { ... }, 109 | 110 | // Create condition pattern. 111 | condition(predicate) { ... }, 112 | 113 | // Create value pattern. 114 | value(value) { ... }, 115 | 116 | // Create 'and' pattern. 117 | and(...patterns) { ... }, 118 | 119 | // Create 'or' pattern. 120 | or(...patterns) { ... }, 121 | 122 | // If `object` is pattern object, return `true`, 123 | // otherwise return `false`. 124 | is(object) { ... }, 125 | } 126 | ``` 127 | 128 | 129 | ## note 130 | 131 | This library uses ES2015 `Symbol` and `Map`. If you want to run this on not ES2015 environment, you can try to use some polyfills. (However, it is not tested. I'm waiting your pull-request) 132 | 133 | 134 | ## license 135 | 136 | MIT License: 137 | 138 | (C) 2016 TSUYUSATO Kitsune 139 | -------------------------------------------------------------------------------- /src/redp.js: -------------------------------------------------------------------------------- 1 | // Internal constants 2 | 3 | // A map to register patterns 4 | const SYM2PAT = new Map() 5 | // A symbol for rest parameter 6 | const REST = Symbol('rest') 7 | // A symbol to mark array as pattern 8 | const MARK = Symbol('mark') 9 | // Pattern type flags 10 | const FLAGS = ` 11 | wild 12 | and 13 | or 14 | value 15 | regexp 16 | array 17 | object 18 | condition` 19 | .trim() 20 | .split(/\s+/g) 21 | .filter(Boolean) 22 | .reduce((flags, flag) => { 23 | flags[flag] = flag 24 | return flags 25 | }, {}) 26 | // Return `undefined` every 27 | const ignore = () => undefined 28 | 29 | 30 | // Export functions and objects 31 | 32 | // Test to match `pattern` with `value`, 33 | // then matched action call. 34 | export function match(value, pattern) { 35 | return match.fn(pattern)(value) 36 | } 37 | 38 | // Create pattern-matcher function from given `patterns`. 39 | match.fn = function fn(patterns) { 40 | const matchers = Object.getOwnPropertySymbols(patterns) 41 | .map(key => { 42 | const matcher = createMatcher(SYM2PAT.get(key)) 43 | SYM2PAT.delete(key) 44 | return [matcher, createAction(patterns[key])] 45 | }) 46 | 47 | return input => (matchers.find(([matcher, action]) => matcher(input)) || [, ignore])[1](input) 48 | } 49 | 50 | // Create pattern key from given `pattern`. 51 | // `pattern` must be pattern object. 52 | // 53 | // NOTE: pattern key can be used only once. 54 | // If you want to reuse the pattern, you can use `pattern` APIs. 55 | export function when(pattern) { 56 | const key = Symbol() 57 | SYM2PAT.set(key, Pattern.from(pattern)) 58 | 59 | return key 60 | } 61 | 62 | // Shortcut to `when(pattern.and(...patterns))` 63 | when.and = function and(...patterns) { 64 | return when(Pattern.and(...patterns)) 65 | } 66 | 67 | // Shortcut to `when(pattern.or(...patterns))` 68 | when.or = function or(...patterns) { 69 | return when(Pattern.or(...patterns)) 70 | } 71 | 72 | // Mark array as pattern 73 | function mark(array) { 74 | array[MARK] = true 75 | 76 | return array 77 | } 78 | 79 | // Create pattern object from given arguments (except for `pattern._` and `pattern.rest`). 80 | export const Pattern = { 81 | mark: mark, 82 | _: mark([FLAGS.wild]), 83 | rest: REST, 84 | 85 | is(object) { 86 | return object && object[MARK] 87 | }, 88 | 89 | // Create pattern object from given `object`. 90 | from(object) { 91 | if (Pattern.is(object)) { 92 | return object 93 | } 94 | 95 | if (object instanceof RegExp) { 96 | return Pattern.regexp(object) 97 | } 98 | 99 | if (isArray(object)) { 100 | return Pattern.array(object) 101 | } 102 | 103 | if (isObject(object)) { 104 | return Pattern.object(object) 105 | } 106 | 107 | if (typeof object === 'function') { 108 | return Pattern.condition(object) 109 | } 110 | 111 | return Pattern.value(object) 112 | }, 113 | 114 | and(...patterns) { 115 | return mark([FLAGS.and, ...patterns.map(Pattern.from)]) 116 | }, 117 | 118 | or(...patterns) { 119 | return mark([FLAGS.or, ...patterns.map(Pattern.from)]) 120 | }, 121 | 122 | value(value) { 123 | return mark([FLAGS.value, value]) 124 | }, 125 | 126 | regexp(regexp) { 127 | return mark([FLAGS.regexp, regexp.toString()]) 128 | }, 129 | 130 | array(patterns) { 131 | let restFlag = false 132 | 133 | if (patterns[patterns.length - 1] === REST) { 134 | restFlag = true 135 | patterns = patterns.slice(0, -1) 136 | } 137 | 138 | return mark([FLAGS.array, restFlag, patterns.map(Pattern.from)]) 139 | }, 140 | 141 | object(object) { 142 | const patterns = Object.keys(object).map(key => [key, Pattern.from(object[key])]) 143 | 144 | return mark([FLAGS.object, patterns]) 145 | }, 146 | 147 | condition(predicate) { 148 | return mark([FLAGS.condition, predicate]) 149 | }, 150 | } 151 | 152 | 153 | // Internals 154 | 155 | // Create matcher from given `pattern`. 156 | function createMatcher(pattern) { 157 | return createMatcher[pattern[0]](...pattern.slice(1)) 158 | } 159 | 160 | createMatcher[FLAGS.wild] = function wild() { 161 | // ['wild'] 162 | return () => true 163 | } 164 | 165 | createMatcher[FLAGS.and] = function and(...patterns) { 166 | // ['and', pattern1, pattern2, ...] 167 | const matchers = patterns.map(createMatcher) 168 | 169 | return input => matchers.every(matcher => matcher(input)) 170 | } 171 | 172 | createMatcher[FLAGS.or] = function or(...patterns) { 173 | // ['or', pattern1, pattern2, ...] 174 | const matchers = patterns.map(createMatcher) 175 | 176 | return input => matchers.some(matcher => matcher(input)) 177 | } 178 | 179 | createMatcher[FLAGS.value] = function value(value) { 180 | // ['value', value] 181 | return input => input === value 182 | } 183 | 184 | createMatcher[FLAGS.regexp] = function regexp(regexp) { 185 | // ['regexp', '/regexp_pattern/regexp_flag'] 186 | const flagIndex = regexp.lastIndexOf('/') 187 | const pattern = regexp.slice(1, flagIndex) 188 | const flag = regexp.slice(flagIndex + 1) 189 | 190 | regexp = new RegExp(pattern, flag) 191 | 192 | return input => regexp.test(input) 193 | } 194 | 195 | createMatcher[FLAGS.array] = function array(restFlag, patterns) { 196 | // ['array', rest_flag, [pattern1, pattern2, ...]] 197 | const matchers = patterns.map(createMatcher) 198 | 199 | return input => 200 | isArray(input) && 201 | (input.length === matchers.length || restFlag && input.length > matchers.length) && 202 | matchers.every((matcher, i) => matcher(input[i])) 203 | } 204 | 205 | createMatcher[FLAGS.object] = function object(patterns) { 206 | // ['object', [[key1, pattern1], [key2, pattern2], ...]] 207 | const matchers = patterns.map(([key, pattern]) => [key, createMatcher(pattern)]) 208 | 209 | return input => { 210 | if (!isObject(input)) { 211 | return false 212 | } 213 | 214 | const keys = Object.keys(input) 215 | 216 | return matchers.every(([key, matcher]) => matcher(input[key])) 217 | } 218 | } 219 | 220 | createMatcher[FLAGS.condition] = function condition(predicate) { 221 | // ['condition', predicate] 222 | return predicate 223 | } 224 | 225 | // Create action from `action`. 226 | function createAction(action) { 227 | return typeof action === 'function' ? action : () => action 228 | } 229 | 230 | function isArray(object) { 231 | return Array.isArray(object) 232 | } 233 | 234 | function isObject(object) { 235 | return object && typeof object === 'object' 236 | } 237 | --------------------------------------------------------------------------------