├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generate-radix-tree 2 | 3 | Generates a function that uses a [radix tree](https://en.wikipedia.org/wiki/Radix_tree) to match which pattern fits the input 4 | 5 | ``` 6 | npm install generate-radix-tree 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```js 12 | const gentree = require('generate-radix-tree') 13 | 14 | const match = gentree([ 15 | {match: 'hello'}, 16 | {match: 'world'}, 17 | {match: 'hello world'} 18 | ]) 19 | 20 | console.log(match('hello')) // returns {match: 'hello'} as it matches 21 | console.log(match('hello world')) // returns {match: 'hello world'} 22 | console.log(match('hey')) // returns null 23 | ``` 24 | 25 | The returned match function is code generated based in the input 26 | to make as few comparisons as possible to find the pattern that matches. 27 | 28 | You can view the generated source code by calling `toString()` on the function 29 | 30 | ```js 31 | console.log(match.toString()) 32 | ``` 33 | 34 | ## Dynamic matches 35 | 36 | If you want to match against a dynamic pattern use a function. 37 | This function *must* set `fn.pointer` to the end index in the string it matches. 38 | 39 | For example 40 | 41 | ```js 42 | const match = gentree([ 43 | {match: ['hello', any, 'world']}, 44 | {match: 'hello world'} 45 | ]) 46 | 47 | console.log(match('hello world')) // return {match: 'hello world'} 48 | console.log(match('hello_world')) // return {match: ['hello', any, 'world']} 49 | 50 | // match any char in str at ptr 51 | function any (str, ptr) { 52 | if (str.length > ptr) { 53 | // more chars, we match 54 | // set any.pointer to where we matched to 55 | any.pointer = ptr + 1 56 | return true 57 | } 58 | return false 59 | } 60 | ``` 61 | 62 | The static patterns always have preference to the dynamic ones 63 | 64 | ## License 65 | 66 | MIT 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const genfun = require('generate-function') 2 | const toRadixTree = require('to-radix-tree') 3 | 4 | const SLICE_THRESHOLD = 16 5 | 6 | module.exports = gentree 7 | 8 | function isNotSwitchable (child) { 9 | return !isSwitchable(child) 10 | } 11 | 12 | function isSwitchable (child) { 13 | return typeof child.prefix[0] === 'string' 14 | } 15 | 16 | function gentree (strings, opts) { 17 | if (!opts) opts = {} 18 | 19 | const tree = toRadixTree(strings) 20 | const gen = opts.gen || genfun() 21 | const name = opts.name || 's' 22 | const onvalue = opts.onvalue || visitValue 23 | const syms = new Map() 24 | 25 | gen.sym(name) 26 | 27 | if (!opts.gen) { 28 | gen(`function choose (${name}) {`) 29 | } 30 | 31 | visit(tree, '', 0) 32 | 33 | if (!opts.gen) { 34 | gen(` 35 | return null 36 | }`) 37 | 38 | return gen.toFunction() 39 | } 40 | 41 | function visitValue (gen, val) { 42 | const i = strings.indexOf(val) 43 | gen.scope['result' + i] = val 44 | gen(`return result${i}`) 45 | } 46 | 47 | function visit (tree, abs, offset) { 48 | if (tree.prefix.length) { 49 | genif(tree.prefix, tree.value === null) 50 | } 51 | 52 | if (tree.value !== null) { 53 | onvalue(gen, tree.value) 54 | } else { 55 | const first = tree.children.length && tree.children[0] 56 | 57 | if (first && !first.prefix.length) { 58 | gen(`if (${name}.length === ${ptr()}) {`) 59 | visit(tree.children.shift(), abs, offset) 60 | gen('}') 61 | } 62 | 63 | const switchable = tree.children.filter(isSwitchable) 64 | const notSwitchable = tree.children.filter(isNotSwitchable) 65 | 66 | if (switchable.length > 1) { 67 | gen(`switch (${ch(ptr())}) {`) 68 | 69 | for (const node of switchable) { 70 | gen(`case ${code(node.prefix[0])}:`) 71 | node.prefix = node.prefix.slice(1) 72 | visit(node, abs, offset + 1) 73 | gen('break') 74 | } 75 | 76 | gen('}') 77 | } else if (switchable.length > 0) { 78 | visit(switchable[0], abs, offset) 79 | } 80 | 81 | for (const node of notSwitchable) { 82 | visit(node, abs, offset) 83 | } 84 | } 85 | 86 | if (tree.prefix.length) { 87 | gen('}') 88 | } 89 | 90 | function genif (match, prefix) { 91 | const m = normalize(match).map(mapMatch) 92 | if (!prefix) m.push(`${name}.length === ${ptr()}`) 93 | gen('if (' + m.join(' && ') + ') {') 94 | } 95 | 96 | function mapString (str) { 97 | if (str.length > SLICE_THRESHOLD) { 98 | const start = ptr() 99 | offset += str.length 100 | const end = ptr() 101 | return `${name}.slice(${start}, ${end}) === ${JSON.stringify(str)}` 102 | } 103 | 104 | const res = [] 105 | for (var i = 0; i < str.length; i++) { 106 | res.push(`${ch(inc())} === ${code(str[i])}`) 107 | } 108 | return res.join(' && ') 109 | } 110 | 111 | function mapMatch (x) { 112 | if (typeof x === 'string') return mapString(x) 113 | const match = syms.get(x) || gen.sym('match') 114 | syms.set(x, match) 115 | gen.scope[match] = x 116 | const src = `${match}(${name}, ${ptr()})` 117 | abs = match + '.pointer' 118 | offset = 0 119 | return src 120 | } 121 | 122 | function inc () { 123 | const p = ptr() 124 | offset++ 125 | return p 126 | } 127 | 128 | function ptr () { 129 | if (!offset && abs) return abs 130 | return (abs ? abs + ' + ' : '') + offset 131 | } 132 | } 133 | 134 | function code (c) { 135 | return c.charCodeAt(0) 136 | } 137 | 138 | function ch (i) { 139 | return `${name}.charCodeAt(${i})` 140 | } 141 | } 142 | 143 | function normalize (str) { 144 | if (typeof str === 'string') return [str] 145 | const res = [] 146 | for (var i = 0; i < str.length; i++) { 147 | if (typeof str[i] === 'string' && typeof res[res.length - 1] === 'string') { 148 | res[res.length - 1] += str[i] 149 | } else { 150 | res.push(str[i]) 151 | } 152 | } 153 | return res 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate-radix-tree", 3 | "version": "1.0.3", 4 | "description": "Generates a function that uses a radix tree to match which pattern fits the input", 5 | "main": "index.js", 6 | "dependencies": { 7 | "generate-function": "^2.3.1", 8 | "to-radix-tree": "^1.0.0" 9 | }, 10 | "devDependencies": { 11 | "standard": "^12.0.1", 12 | "tape": "^4.9.1" 13 | }, 14 | "scripts": { 15 | "test": "standard && tape test.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/mafintosh/generate-radix-tree.git" 20 | }, 21 | "author": "Mathias Buus (@mafintosh)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mafintosh/generate-radix-tree/issues" 25 | }, 26 | "homepage": "https://github.com/mafintosh/generate-radix-tree" 27 | } 28 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const gentree = require('./') 3 | 4 | tape('basic', function (t) { 5 | const choose = gentree([ 6 | 'hello world', 7 | 'hello', 8 | 'world', 9 | 'hey', 10 | { match: 'ho' } 11 | ]) 12 | 13 | t.same(choose('hello'), 'hello') 14 | t.same(choose('ho'), { match: 'ho' }) 15 | t.same(choose('hey'), 'hey') 16 | t.same(choose('world'), 'world') 17 | t.same(choose('hello world'), 'hello world') 18 | t.same(choose('else'), null) 19 | t.end() 20 | }) 21 | 22 | tape('only one', function (t) { 23 | const choose = gentree([ 'hey' ]) 24 | 25 | t.same(choose('hey'), 'hey') 26 | t.same(choose('heyho'), null) 27 | t.end() 28 | }) 29 | 30 | tape('basic with function', function (t) { 31 | const choose = gentree([ 32 | [ 'hello', any, 'world' ], 33 | 'hello world', 34 | 'hey', 35 | { match: 'ho' } 36 | ]) 37 | 38 | t.same(choose('hello world'), 'hello world') 39 | t.same(choose('hello!world'), [ 'hello', any, 'world' ]) 40 | t.same(choose('hellooworld'), [ 'hello', any, 'world' ]) 41 | t.same(choose('hey'), 'hey') 42 | t.same(choose('ho'), { match: 'ho' }) 43 | t.end() 44 | 45 | function any (s, ptr) { 46 | if (ptr >= s.length) return false 47 | any.pointer = ptr + 1 48 | return true 49 | } 50 | }) 51 | --------------------------------------------------------------------------------