├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── sample_diagram.png └── sample_diagram.xml ├── lib ├── generate │ ├── generateBigInt.js │ └── generateInt.js ├── index.js └── test.js ├── package.json └── src ├── generate ├── generateBigInt.js └── generateInt.js ├── index.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [src/*.js] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | 18 | # Matches the exact files either package.json or .travis.yml 19 | [{package.json,.travis.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | - "5" 7 | - "4" 8 | env: 9 | - CXX=g++-4.8 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - g++-4.8 16 | script: 17 | - npm test 18 | install: 19 | - npm install --dev 20 | before_install: 21 | - "npm update -g npm" 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Luciano Mammino 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # indexed-string-variation 2 | 3 | [![npm version](https://badge.fury.io/js/indexed-string-variation.svg)](http://badge.fury.io/js/indexed-string-variation) 4 | [![Build Status](https://travis-ci.org/lmammino/indexed-string-variation.svg?branch=master)](https://travis-ci.org/lmammino/indexed-string-variation) 5 | [![codecov.io](https://codecov.io/gh/lmammino/indexed-string-variation/coverage.svg?branch=master)](https://codecov.io/gh/lmammino/indexed-string-variation) 6 | 7 | 8 | Experimental JavaScript module to generate all possible variations of strings over an alphabet using an n-ary virtual tree. 9 | 10 | 11 | ## Install 12 | 13 | With NPM: 14 | 15 | ```bash 16 | npm install --save indexed-string-variation 17 | ``` 18 | 19 | 20 | ## Usage 21 | 22 | Generally useful to create distributed brute-force password recovery tools or 23 | other software that might require distributed generation of all possible 24 | strings on a given alphabet. 25 | 26 | ```javascript 27 | const generator = require('indexed-string-variation').generator; 28 | const variations = generator('abc1'); 29 | 30 | for (let i=0; i < 23; i++) { 31 | console.log(i, variations(i)); // generates the i-th string in the alphabet 'abc1' 32 | } 33 | ``` 34 | 35 | Will print: 36 | 37 | ```bash 38 | 0 '' 39 | 1 'a' 40 | 2 'b' 41 | 3 'c' 42 | 4 '1' 43 | 5 'aa' 44 | 6 'ab' 45 | 7 'ac' 46 | 8 'a1' 47 | 9 'ba' 48 | 10 'bb' 49 | 11 'bc' 50 | 12 'b1' 51 | 13 'ca' 52 | 14 'cb' 53 | 15 'cc' 54 | 16 'c1' 55 | 17 '1a' 56 | 18 '1b' 57 | 19 '1c' 58 | 20 '11' 59 | 21 'aaa' 60 | 22 'aab' 61 | ``` 62 | 63 | 64 | ## API 65 | 66 | The module `indexed-string-variation` exposes the following components: 67 | 68 | * `generator` (also aliased as `default` for ES2015 modules): the 69 | main generator function 70 | * `defaultAlphabet`: a constant string that contains the sequence of 71 | characters in the defaultAlphabet 72 | 73 | As you can see in the [usage example](#usage), the `generator` function takes as input the 74 | alphabet string (which is optional and it will default to `defaultAlphabet` if 75 | not provided) and returns a new function called `variations` which can be 76 | used to retrieve the indexed variation on the given alphabet. `variations` takes 77 | a non-negative integer as input which represents the index of the variations 78 | that we want to generate: 79 | 80 | ```javascript 81 | const variations = generator('XYZ'); 82 | console.log(variations(7123456789)); // "XYYZYZZZYYYZYZYXYYYYX" 83 | ``` 84 | 85 | 86 | ## How the algorithm works 87 | 88 | The way the generation algorithm work is using an n-ary tree where n is the size of the alphabet. 89 | For example, if we have an alphabet containing only `a`, `b` and `c` and we want to generate all 90 | the strings with a maximum length of 3 the algorithm will use the following tree: 91 | 92 | ![Sample ternary tree over abc alphabet](doc/sample_diagram.png) 93 | 94 | The tree is to be considered "virtual", because it's never generated in its integrity, so the 95 | used space in memory is minimal. 96 | 97 | In brevity we can describe the algorithm as follows: 98 | 99 | > Given an index **i** over an alphabet of length **n** and it's corresponding n-ary tree, 100 | the string associated to **i** corresponds to the string obtained by 101 | concatenating all the characters found in the path that goes from the root node to the **i**-th node. 102 | 103 | For example, with the alphabet in the image we can generate the following strings: 104 | 105 | | i | generated string | 106 | |---:|---| 107 | |0|| 108 | |1|a| 109 | |2|b| 110 | |3|c| 111 | |4|aa| 112 | |5|ab| 113 | |6|ac| 114 | |7|ba| 115 | |8|bb| 116 | |9|bc| 117 | |10|ca| 118 | |11|cb| 119 | |12|cc| 120 | 121 | 122 | Important note: The alphabet is always normalized (i.e. duplicates are removed) 123 | 124 | 125 | ## Use big-integer to avoid JavaScript big integers approximations 126 | 127 | Integers with more than 18 digits are approximated (e.g. `123456789012345680000 === 123456789012345678901`), so at some 128 | point the generator will start to generate a lot of duplicated strings and it will start to miss many cases. 129 | 130 | To workaround this issue you can use indexes generated with the module [big-integer](https://www.npmjs.com/package/big-integer). 131 | Internally the indexed-string-variation will take care of performing the correct 132 | operations using the library. 133 | 134 | Let's see an example: 135 | 136 | ```javascript 137 | const bigInt = require('big-integer'); // install from https://npmjs.com/package/big-integer 138 | const generator = require('indexed-string-variation').generator; 139 | const variations = generator('JKWXYZ'); 140 | 141 | // generation using regular big numbers (same result) 142 | console.log(variations(123456789012345678901)); // XJZJYXXXYYJKYZZJKZKYJWJJYW 143 | console.log(variations(123456789012345680000)); // XJZJYXXXYYJKYZZJKZKYJWJJYW 144 | 145 | // generation using big-integer numbers (correct results) 146 | console.log(variations(bigInt('123456789012345678901'))); // XJZJYXXXYYJKYZZJKZKXZKJZZJ 147 | console.log(variations(bigInt('123456789012345680000'))); // XJZJYXXXYYJKYZZJKZKXZWJJWK 148 | ``` 149 | 150 | Anyway, keep in mind that big-integers might have a relevant performance impact, 151 | so if you don't plan to use huge integers it's still recommended to use 152 | plain JavaScript numbers as indexes. 153 | 154 | 155 | ## Contributing 156 | 157 | Everyone is very welcome to contribute to this project. 158 | You can contribute just by submitting bugs or suggesting improvements by 159 | [opening an issue on GitHub](https://github.com/lmammino/indexed-string-variation/issues). 160 | 161 | 162 | ## License 163 | 164 | Licensed under [MIT License](LICENSE). © Luciano Mammino. 165 | -------------------------------------------------------------------------------- /doc/sample_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/indexed-string-variation/52fb6b5b05940138fb58f9a1c937531ad90adda5/doc/sample_diagram.png -------------------------------------------------------------------------------- /doc/sample_diagram.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/generate/generateBigInt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = generate; 7 | 8 | var _bigInteger = require('big-integer'); 9 | 10 | var _bigInteger2 = _interopRequireDefault(_bigInteger); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var zero = (0, _bigInteger2.default)('0'); 15 | 16 | // calculates the level of a given index in the current virtual tree 17 | var getLevel = function getLevel(base, index) { 18 | var level = (0, _bigInteger2.default)('0'); 19 | var current = index; 20 | var parent = void 0; 21 | while (current.gt(zero)) { 22 | parent = current.prev().divide(base); 23 | level = level.next(); 24 | current = parent; 25 | } 26 | 27 | return level; 28 | }; 29 | 30 | function generate(index, alphabet) { 31 | var n = (0, _bigInteger2.default)(alphabet.length); 32 | var result = ''; 33 | var l = void 0; 34 | var f = void 0; 35 | var rebasedPos = void 0; 36 | var rebasedIndex = void 0; 37 | 38 | while (index.gt(zero)) { 39 | // 1. calculate level 40 | l = getLevel(n, index); 41 | 42 | // 2. calculate first element in level 43 | f = (0, _bigInteger2.default)('0'); 44 | for (var i = 0; i < l; i++) { 45 | f = f.plus(n.pow((0, _bigInteger2.default)(i))); 46 | } 47 | 48 | // 3. rebase current position and calculate current letter 49 | rebasedPos = index.minus(f); 50 | rebasedIndex = rebasedPos.mod(n); 51 | result = alphabet[rebasedIndex] + result; 52 | 53 | // 4. calculate parent number in the tree (next index) 54 | index = index.prev().divide(n); 55 | } 56 | 57 | return result; 58 | } -------------------------------------------------------------------------------- /lib/generate/generateInt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // calculates the level of a given index in the current virtual tree 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = generate; 9 | var getLevel = function getLevel(base, index) { 10 | var level = 0; 11 | var current = index; 12 | var parent = void 0; 13 | while (current > 0) { 14 | parent = Math.floor((current - 1) / base); 15 | ++level; 16 | current = parent; 17 | } 18 | 19 | return level; 20 | }; 21 | 22 | function generate(index, alphabet) { 23 | if (parseInt(Number(index), 10) !== index || index < 0) { 24 | throw new TypeError('index must be a non-negative integer'); 25 | } 26 | 27 | var n = alphabet.length; 28 | var result = ''; 29 | var l = void 0; 30 | var f = void 0; 31 | var rebasedPos = void 0; 32 | var rebasedIndex = void 0; 33 | 34 | while (index > 0) { 35 | // 1. calculate level 36 | l = getLevel(n, index); 37 | 38 | // 2. calculate first element in level 39 | f = 0; 40 | for (var i = 0; i < l; i++) { 41 | f += Math.pow(n, i); 42 | } 43 | 44 | // 3. rebase current position and calculate current letter 45 | rebasedPos = index - f; 46 | rebasedIndex = rebasedPos % n; 47 | result = alphabet[rebasedIndex] + result; 48 | 49 | // 4. calculate parent number in the tree (next index) 50 | index = Math.floor((index - 1) / n); 51 | } 52 | 53 | return result; 54 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.defaultAlphabet = undefined; 7 | exports.generator = generator; 8 | 9 | var _bigInteger = require('big-integer'); 10 | 11 | var _bigInteger2 = _interopRequireDefault(_bigInteger); 12 | 13 | var _generateInt = require('./generate/generateInt'); 14 | 15 | var _generateInt2 = _interopRequireDefault(_generateInt); 16 | 17 | var _generateBigInt = require('./generate/generateBigInt'); 18 | 19 | var _generateBigInt2 = _interopRequireDefault(_generateBigInt); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | var defaultAlphabet = exports.defaultAlphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 24 | function generator(alphabet) { 25 | // remove duplicates from alphabets 26 | var cleanAlphabet = function cleanAlphabet(alphabet) { 27 | return alphabet.split('').filter(function (item, pos, self) { 28 | return self.indexOf(item) === pos; 29 | }).join(''); 30 | }; 31 | 32 | if (alphabet && typeof alphabet !== 'string') { 33 | throw new TypeError('alphabet must be a string'); 34 | } 35 | 36 | alphabet = alphabet ? cleanAlphabet(alphabet) : defaultAlphabet; 37 | 38 | // string generation function 39 | var generate = function generate(index) { 40 | return index instanceof _bigInteger2.default ? (0, _generateBigInt2.default)(index, alphabet) : (0, _generateInt2.default)(index, alphabet); 41 | }; 42 | 43 | generate.alphabet = alphabet; 44 | 45 | return generate; 46 | } 47 | 48 | exports.default = generator; -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _tap = require('tap'); 4 | 5 | var _bigInteger = require('big-integer'); 6 | 7 | var _bigInteger2 = _interopRequireDefault(_bigInteger); 8 | 9 | var _index = require('./index'); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | var dataProvider = { 14 | 'it should produce variations with digits in alphabet': { 15 | alphabet: '0123456789', 16 | expected: ['', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10'] 17 | }, 18 | 'it should produce variations with alphanumeric alphabet': { 19 | alphabet: 'abc1', 20 | expected: ['', 'a', 'b', 'c', '1', 'aa', 'ab', 'ac', 'a1', 'ba', 'bb', 'bc', 'b1', 'ca', 'cb', 'cc', 'c1', '1a', '1b', '1c', '11', 'aaa'] 21 | }, 22 | 'it should remove duplicates from alphabet': { 23 | alphabet: 'aabbbbcc1111111', 24 | expected: ['', 'a', 'b', 'c', '1', 'aa', 'ab', 'ac', 'a1', 'ba', 'bb', 'bc', 'b1', 'ca', 'cb', 'cc', 'c1', '1a', '1b', '1c', '11', 'aaa'] 25 | } 26 | }; 27 | 28 | var _loop = function _loop(testCase) { 29 | if ({}.hasOwnProperty.call(dataProvider, testCase)) { 30 | (0, _tap.test)(testCase, function (t) { 31 | var alphabet = dataProvider[testCase].alphabet; 32 | var expected = dataProvider[testCase].expected; 33 | var isvn = (0, _index.generator)(alphabet); 34 | var generatedInt = []; 35 | var generatedBigInt = []; 36 | 37 | for (var i = 0; i < expected.length; i++) { 38 | // verify that the generator using int and the generator using bigInt produce the same result 39 | generatedInt.push(isvn(i)); 40 | generatedBigInt.push(isvn((0, _bigInteger2.default)(String(i)))); 41 | } 42 | 43 | t.plan(2); 44 | t.deepEqual(expected, generatedInt, '(int) From ' + alphabet + ' generates: ' + expected.join()); 45 | t.deepEqual(expected, generatedBigInt, '(bigInt) From ' + alphabet + ' generates: ' + expected.join()); 46 | t.end(); 47 | }); 48 | } 49 | }; 50 | 51 | for (var testCase in dataProvider) { 52 | _loop(testCase); 53 | } 54 | 55 | (0, _tap.test)('it must use the default alphabet if no alphabet is given', function (t) { 56 | t.plan(2); 57 | var g = (0, _index.generator)(); 58 | t.equal('d', g(4)); 59 | t.equal(g.alphabet, _index.defaultAlphabet); 60 | }); 61 | 62 | (0, _tap.test)('it must not accept non-string values as alphabet', function (t) { 63 | t.plan(3); 64 | var expectedException = TypeError; 65 | var expectedMessage = 'alphabet must be a string'; 66 | t.throws(function () { 67 | return (0, _index.generator)(-1); 68 | }, expectedException, expectedMessage); 69 | t.throws(function () { 70 | return (0, _index.generator)([]); 71 | }, expectedException, expectedMessage); 72 | t.throws(function () { 73 | return (0, _index.generator)({}); 74 | }, expectedException, expectedMessage); 75 | }); 76 | 77 | (0, _tap.test)('it must not accept indexes that are not non-negative integers', function (t) { 78 | t.plan(3); 79 | var expectedException = TypeError; 80 | var expectedMessage = 'index must be a non-negative integer'; 81 | t.throws(function () { 82 | return (0, _index.generator)('a')(-1); 83 | }, expectedException, expectedMessage); 84 | t.throws(function () { 85 | return (0, _index.generator)('a')([]); 86 | }, expectedException, expectedMessage); 87 | t.throws(function () { 88 | return (0, _index.generator)('b')({}); 89 | }, expectedException, expectedMessage); 90 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indexed-string-variation", 3 | "version": "1.0.3", 4 | "description": "Experimental JavaScript module to generate all possible variations of strings over an alphabet using an n-ary virtual tree", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "files": [ 8 | "lib/index.js", 9 | "src/index.js", 10 | "lib/generate", 11 | "src/generate" 12 | ], 13 | "scripts": { 14 | "build": "node_modules/.bin/babel --presets=es2015 src/ -d lib", 15 | "lint": "./node_modules/.bin/eslint src/", 16 | "test": "node_modules/.bin/if-node-version '>=6' npm run lint && node_modules/.bin/tap lib/test.js --coverage | node_modules/.bin/tap-spec", 17 | "posttest": "node_modules/.bin/tap --coverage-report=lcov && node_modules/.bin/codecov", 18 | "prepublish": "npm run build" 19 | }, 20 | "author": { 21 | "name": "Luciano Mammino", 22 | "email": "lucianomammino@gmail.com", 23 | "url": "http://loige.co" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/lmammino/indexed-string-variation" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/lmammino/indexed-string-variation/issues" 31 | }, 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">=4" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "^6.14.0", 38 | "babel-eslint": "^6.1.2", 39 | "babel-preset-es2015": "^6.14.0", 40 | "codecov": "^1.0.1", 41 | "eslint": "^3.4.0", 42 | "eslint-config-xo-space": "^0.14.0", 43 | "eslint-plugin-babel": "^3.3.0", 44 | "if-node-version": "^1.0.0", 45 | "tap": "^7.0.0", 46 | "tap-spec": "^4.1.1" 47 | }, 48 | "keywords": [ 49 | "variation", 50 | "string", 51 | "variants", 52 | "generator", 53 | "generation", 54 | "brute force", 55 | "cracker", 56 | "n-ary", 57 | "tree" 58 | ], 59 | "eslintConfig": { 60 | "extends": "xo-space/esnext" 61 | }, 62 | "dependencies": { 63 | "big-integer": "^1.6.16" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/generate/generateBigInt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import bigInt from 'big-integer'; 4 | 5 | const zero = bigInt('0'); 6 | 7 | // calculates the level of a given index in the current virtual tree 8 | const getLevel = (base, index) => { 9 | let level = bigInt('0'); 10 | let current = index; 11 | let parent; 12 | while (current.gt(zero)) { 13 | parent = current.prev().divide(base); 14 | level = level.next(); 15 | current = parent; 16 | } 17 | 18 | return level; 19 | }; 20 | 21 | export default function generate(index, alphabet) { 22 | const n = bigInt(alphabet.length); 23 | let result = ''; 24 | let l; 25 | let f; 26 | let rebasedPos; 27 | let rebasedIndex; 28 | 29 | while (index.gt(zero)) { 30 | // 1. calculate level 31 | l = getLevel(n, index); 32 | 33 | // 2. calculate first element in level 34 | f = bigInt('0'); 35 | for (let i = 0; i < l; i++) { 36 | f = f.plus(n.pow(bigInt(i))); 37 | } 38 | 39 | // 3. rebase current position and calculate current letter 40 | rebasedPos = index.minus(f); 41 | rebasedIndex = rebasedPos.mod(n); 42 | result = alphabet[rebasedIndex] + result; 43 | 44 | // 4. calculate parent number in the tree (next index) 45 | index = index.prev().divide(n); 46 | } 47 | 48 | return result; 49 | } 50 | -------------------------------------------------------------------------------- /src/generate/generateInt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // calculates the level of a given index in the current virtual tree 4 | const getLevel = (base, index) => { 5 | let level = 0; 6 | let current = index; 7 | let parent; 8 | while (current > 0) { 9 | parent = Math.floor((current - 1) / base); 10 | ++level; 11 | current = parent; 12 | } 13 | 14 | return level; 15 | }; 16 | 17 | export default function generate(index, alphabet) { 18 | if (parseInt(Number(index), 10) !== index || index < 0) { 19 | throw new TypeError('index must be a non-negative integer'); 20 | } 21 | 22 | const n = alphabet.length; 23 | let result = ''; 24 | let l; 25 | let f; 26 | let rebasedPos; 27 | let rebasedIndex; 28 | 29 | while (index > 0) { 30 | // 1. calculate level 31 | l = getLevel(n, index); 32 | 33 | // 2. calculate first element in level 34 | f = 0; 35 | for (let i = 0; i < l; i++) { 36 | f += Math.pow(n, i); 37 | } 38 | 39 | // 3. rebase current position and calculate current letter 40 | rebasedPos = index - f; 41 | rebasedIndex = rebasedPos % n; 42 | result = alphabet[rebasedIndex] + result; 43 | 44 | // 4. calculate parent number in the tree (next index) 45 | index = Math.floor((index - 1) / n); 46 | } 47 | 48 | return result; 49 | } 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import bigInt from 'big-integer'; 4 | import generateInt from './generate/generateInt'; 5 | import generateBigInt from './generate/generateBigInt'; 6 | 7 | export const defaultAlphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 8 | export function generator(alphabet) { 9 | // remove duplicates from alphabets 10 | const cleanAlphabet = alphabet => { 11 | return alphabet 12 | .split('') 13 | .filter((item, pos, self) => self.indexOf(item) === pos) 14 | .join('') 15 | ; 16 | }; 17 | 18 | if (alphabet && typeof alphabet !== 'string') { 19 | throw new TypeError('alphabet must be a string'); 20 | } 21 | 22 | alphabet = alphabet ? cleanAlphabet(alphabet) : defaultAlphabet; 23 | 24 | // string generation function 25 | const generate = index => { 26 | return index instanceof bigInt ? generateBigInt(index, alphabet) : generateInt(index, alphabet); 27 | }; 28 | 29 | generate.alphabet = alphabet; 30 | 31 | return generate; 32 | } 33 | 34 | export default generator; 35 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {test} from 'tap'; 4 | import bigInt from 'big-integer'; 5 | import {generator, defaultAlphabet} from './index'; 6 | 7 | const dataProvider = { 8 | 'it should produce variations with digits in alphabet': { 9 | alphabet: '0123456789', 10 | expected: ['', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '00', '01', '02', '03', '04', '05', '06', 11 | '07', '08', '09', '10'] 12 | }, 13 | 'it should produce variations with alphanumeric alphabet': { 14 | alphabet: 'abc1', 15 | expected: ['', 'a', 'b', 'c', '1', 'aa', 'ab', 'ac', 'a1', 'ba', 'bb', 'bc', 'b1', 'ca', 'cb', 'cc', 'c1', '1a', 16 | '1b', '1c', '11', 'aaa'] 17 | }, 18 | 'it should remove duplicates from alphabet': { 19 | alphabet: 'aabbbbcc1111111', 20 | expected: ['', 'a', 'b', 'c', '1', 'aa', 'ab', 'ac', 'a1', 'ba', 'bb', 'bc', 'b1', 'ca', 'cb', 'cc', 'c1', '1a', 21 | '1b', '1c', '11', 'aaa'] 22 | } 23 | }; 24 | 25 | for (const testCase in dataProvider) { 26 | if (({}).hasOwnProperty.call(dataProvider, testCase)) { 27 | test(testCase, t => { 28 | const alphabet = dataProvider[testCase].alphabet; 29 | const expected = dataProvider[testCase].expected; 30 | const isvn = generator(alphabet); 31 | const generatedInt = []; 32 | const generatedBigInt = []; 33 | 34 | for (let i = 0; i < expected.length; i++) { 35 | // verify that the generator using int and the generator using bigInt produce the same result 36 | generatedInt.push(isvn(i)); 37 | generatedBigInt.push(isvn(bigInt(String(i)))); 38 | } 39 | 40 | t.plan(2); 41 | t.deepEqual(expected, generatedInt, `(int) From ${alphabet} generates: ${expected.join()}`); 42 | t.deepEqual(expected, generatedBigInt, `(bigInt) From ${alphabet} generates: ${expected.join()}`); 43 | t.end(); 44 | }); 45 | } 46 | } 47 | 48 | test('it must use the default alphabet if no alphabet is given', t => { 49 | t.plan(2); 50 | const g = generator(); 51 | t.equal('d', g(4)); 52 | t.equal(g.alphabet, defaultAlphabet); 53 | }); 54 | 55 | test('it must not accept non-string values as alphabet', t => { 56 | t.plan(3); 57 | const expectedException = TypeError; 58 | const expectedMessage = 'alphabet must be a string'; 59 | t.throws(() => generator(-1), expectedException, expectedMessage); 60 | t.throws(() => generator([]), expectedException, expectedMessage); 61 | t.throws(() => generator({}), expectedException, expectedMessage); 62 | }); 63 | 64 | test('it must not accept indexes that are not non-negative integers', t => { 65 | t.plan(3); 66 | const expectedException = TypeError; 67 | const expectedMessage = 'index must be a non-negative integer'; 68 | t.throws(() => generator('a')(-1), expectedException, expectedMessage); 69 | t.throws(() => generator('a')([]), expectedException, expectedMessage); 70 | t.throws(() => generator('b')({}), expectedException, expectedMessage); 71 | }); 72 | --------------------------------------------------------------------------------