├── doc ├── sample_diagram.png └── sample_diagram.xml ├── vitest.config.ts ├── tsconfig.json ├── biome.json ├── .gitignore ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── lefthook.yml ├── LICENSE ├── package.json ├── src ├── index.ts └── test.ts └── README.md /doc/sample_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/indexed-string-variation/HEAD/doc/sample_diagram.png -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | include: ['src/**/*.test.ts', 'src/**/test.ts'], 8 | coverage: { 9 | provider: 'v8', 10 | reporter: ['text', 'json', 'html', 'clover', 'lcov'], 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["lib", "dist", "node_modules", "test", "*.test.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["dist/", "coverage/"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "single", 28 | "semicolons": "asNeeded" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.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 | 39 | lib/ 40 | dist/ 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '24' 20 | cache: 'npm' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Run tests with coverage 29 | run: npm run test:unit -- --coverage 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v5 33 | env: 34 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to npm 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | contents: read 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '24' 21 | cache: 'npm' 22 | 23 | - name: Set npm registry 24 | run: npm config set registry https://registry.npmjs.org/ 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Publish to npm 33 | run: | 34 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc 35 | npm publish --provenance --access public 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | jobs: 3 | - name: lint frontend 4 | run: npm run lint:fix 5 | # 6 | # Refer for explanation to following link: 7 | # https://lefthook.dev/configuration/ 8 | # 9 | # pre-push: 10 | # jobs: 11 | # - name: packages audit 12 | # tags: 13 | # - frontend 14 | # - security 15 | # run: yarn audit 16 | # 17 | # - name: gems audit 18 | # tags: 19 | # - backend 20 | # - security 21 | # run: bundle audit 22 | # 23 | # pre-commit: 24 | # parallel: true 25 | # jobs: 26 | # - run: yarn eslint {staged_files} 27 | # glob: "*.{js,ts,jsx,tsx}" 28 | # 29 | # - name: rubocop 30 | # glob: "*.rb" 31 | # exclude: 32 | # - config/application.rb 33 | # - config/routes.rb 34 | # run: bundle exec rubocop --force-exclusion {all_files} 35 | # 36 | # - name: govet 37 | # files: git ls-files -m 38 | # glob: "*.go" 39 | # run: go vet {files} 40 | # 41 | # - script: "hello.js" 42 | # runner: node 43 | # 44 | # - script: "hello.go" 45 | # runner: go run 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indexed-string-variation", 3 | "version": "2.1.0", 4 | "description": "Experimental JavaScript module to generate all possible variations of strings over an alphabet using an n-ary virtual tree", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.js" 11 | } 12 | }, 13 | "files": ["dist/"], 14 | "scripts": { 15 | "build": "tsc", 16 | "lint:fix": "biome check --write --organize-imports-enabled=true .", 17 | "test:lint": "biome check --organize-imports-enabled=true .", 18 | "test:typecheck": "tsc --noEmit", 19 | "test:unit": "vitest run", 20 | "test": "npm run test:lint && npm run test:typecheck && npm run test:unit" 21 | }, 22 | "author": { 23 | "name": "Luciano Mammino", 24 | "email": "lucianomammino@gmail.com", 25 | "url": "http://loige.co" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/lmammino/indexed-string-variation" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/lmammino/indexed-string-variation/issues" 33 | }, 34 | "license": "MIT", 35 | "engines": { 36 | "node": ">=22" 37 | }, 38 | "devDependencies": { 39 | "@biomejs/biome": "1.9.4", 40 | "@types/node": "^22.0.0", 41 | "@vitest/coverage-v8": "^3.2.3", 42 | "lefthook": "^1.11.13", 43 | "typescript": "^5.0.0", 44 | "vitest": "^3.2.3" 45 | }, 46 | "keywords": [ 47 | "variation", 48 | "string", 49 | "variants", 50 | "generator", 51 | "generation", 52 | "brute force", 53 | "cracker", 54 | "n-ary", 55 | "tree" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export const defaultAlphabet = 2 | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 3 | 4 | function cleanAlphabet(alphabet: string) { 5 | return Array.from(new Set(alphabet.split(''))).join('') 6 | } 7 | 8 | // calculates the level of a given index in the current virtual tree 9 | const getLevel = (base: bigint, index: bigint): bigint => { 10 | let level = 0n 11 | let current = index 12 | let parent: bigint 13 | while (current > 0n) { 14 | parent = (current - 1n) / base 15 | level++ 16 | current = parent 17 | } 18 | return level 19 | } 20 | 21 | // Generates a string based on the given index and alphabet 22 | function generateString(startIndex: bigint, alphabet: string): string { 23 | if (startIndex === 0n) return '' 24 | const n = BigInt(alphabet.length) 25 | let result = '' 26 | let l: bigint 27 | let f: bigint 28 | let rebasedPos: bigint 29 | let rebasedIndex: bigint 30 | let index = startIndex 31 | while (index > 0n) { 32 | l = getLevel(n, index) 33 | f = 0n 34 | for (let i = 0n; i < l; i++) { 35 | f += n ** i 36 | } 37 | rebasedPos = index - f 38 | rebasedIndex = ((rebasedPos % n) + n) % n // ensure non-negative 39 | result = alphabet[Number(rebasedIndex)] + result 40 | index = (index - 1n) / n 41 | } 42 | return result 43 | } 44 | 45 | export type GenOptions = { 46 | alphabet?: string 47 | from?: bigint 48 | to?: bigint 49 | maxLen?: number 50 | maxIterations?: number 51 | } 52 | 53 | export function* indexedStringVariation(options: GenOptions = {}) { 54 | const alphabet = options.alphabet 55 | ? cleanAlphabet(options.alphabet) 56 | : defaultAlphabet 57 | const from = options.from ?? 0n 58 | 59 | let iterations = 0 60 | let i = from 61 | 62 | while (true) { 63 | const str = generateString(i, alphabet) 64 | 65 | if (options.maxLen !== undefined && str.length > options.maxLen) { 66 | break 67 | } 68 | yield str 69 | 70 | i++ 71 | iterations++ 72 | 73 | if (options.to !== undefined && i > options.to) { 74 | break 75 | } 76 | 77 | if ( 78 | options.maxIterations !== undefined && 79 | iterations >= options.maxIterations 80 | ) { 81 | break 82 | } 83 | } 84 | } 85 | 86 | export default indexedStringVariation 87 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { type GenOptions, indexedStringVariation } from './index.js' 3 | 4 | const cases: [string, GenOptions, string[]][] = [ 5 | [ 6 | 'it should produce variations with digits in alphabet', 7 | { alphabet: '0123456789', from: 0n, to: 21n }, 8 | [ 9 | '', 10 | '0', 11 | '1', 12 | '2', 13 | '3', 14 | '4', 15 | '5', 16 | '6', 17 | '7', 18 | '8', 19 | '9', 20 | '00', 21 | '01', 22 | '02', 23 | '03', 24 | '04', 25 | '05', 26 | '06', 27 | '07', 28 | '08', 29 | '09', 30 | '10', 31 | ], 32 | ], 33 | [ 34 | 'it should produce variations with alphanumeric alphabet', 35 | { alphabet: 'abc1', from: 0n, to: 21n }, 36 | [ 37 | '', 38 | 'a', 39 | 'b', 40 | 'c', 41 | '1', 42 | 'aa', 43 | 'ab', 44 | 'ac', 45 | 'a1', 46 | 'ba', 47 | 'bb', 48 | 'bc', 49 | 'b1', 50 | 'ca', 51 | 'cb', 52 | 'cc', 53 | 'c1', 54 | '1a', 55 | '1b', 56 | '1c', 57 | '11', 58 | 'aaa', 59 | ], 60 | ], 61 | [ 62 | 'it should remove duplicates from alphabet', 63 | { alphabet: 'aabbbbcc1111111', from: 0n, to: 21n }, 64 | [ 65 | '', 66 | 'a', 67 | 'b', 68 | 'c', 69 | '1', 70 | 'aa', 71 | 'ab', 72 | 'ac', 73 | 'a1', 74 | 'ba', 75 | 'bb', 76 | 'bc', 77 | 'b1', 78 | 'ca', 79 | 'cb', 80 | 'cc', 81 | 'c1', 82 | '1a', 83 | '1b', 84 | '1c', 85 | '11', 86 | 'aaa', 87 | ], 88 | ], 89 | [ 90 | 'it uses the default alphabet if none is provided', 91 | { from: 0n, to: 10n }, 92 | ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], 93 | ], 94 | [ 95 | 'it stops at maxLen if provided', 96 | { alphabet: 'ab', maxLen: 2 }, 97 | ['', 'a', 'b', 'aa', 'ab', 'ba', 'bb'], 98 | ], 99 | [ 100 | 'it stops at maxIterations if provided', 101 | { alphabet: 'ab', maxIterations: 5 }, 102 | ['', 'a', 'b', 'aa', 'ab'], 103 | ], 104 | ] 105 | 106 | describe('indexed-string-variation', () => { 107 | it.each(cases)( 108 | '%s', 109 | (_title: string, options: GenOptions, expected: string[]) => { 110 | const isvn = indexedStringVariation(options) 111 | const generatedStrings = [...isvn] 112 | expect(generatedStrings).toEqual(expected) 113 | }, 114 | ) 115 | 116 | it('Can be used with the explicit iterator interface', () => { 117 | // endless 118 | const isvn = indexedStringVariation({ alphabet: 'ab' }) 119 | expect(isvn.next()).toEqual({ value: '', done: false }) 120 | expect(isvn.next()).toEqual({ value: 'a', done: false }) 121 | expect(isvn.next()).toEqual({ value: 'b', done: false }) 122 | expect(isvn.next()).toEqual({ value: 'aa', done: false }) 123 | expect(isvn.next()).toEqual({ value: 'ab', done: false }) 124 | expect(isvn.next()).toEqual({ value: 'ba', done: false }) 125 | expect(isvn.next()).toEqual({ value: 'bb', done: false }) 126 | //... 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /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 | [![CI](https://github.com/lmammino/indexed-string-variation/actions/workflows/ci.yml/badge.svg)](https://github.com/lmammino/indexed-string-variation/actions/workflows/ci.yml) 5 | [![codecov](https://codecov.io/gh/lmammino/indexed-string-variation/graph/badge.svg?token=4zplgm5bBj)](https://codecov.io/gh/lmammino/indexed-string-variation) 6 | 7 | JavaScript module to generate all possible variations of strings over an 8 | alphabet using an n-ary virtual tree. 9 | 10 | ## Quick start example: 11 | 12 | ```js 13 | // generate all strings of max length 3 using the alphabet "ab" 14 | import isv from "indexed-string-variation"; 15 | 16 | for (const str of isv({ alphabet: "ab", maxLen: 3 })) { 17 | console.log(str); 18 | } 19 | ``` 20 | 21 | Output: 22 | 23 | ```plain 24 | (empty string) 25 | a 26 | b 27 | aa 28 | ab 29 | ba 30 | bb 31 | aaa 32 | aab 33 | aba 34 | abb 35 | baa 36 | bab 37 | bba 38 | bbb 39 | ``` 40 | 41 | > [!IMPORTANT]\ 42 | > Note that the first result is always an empty string! If you want to start 43 | > from the first non-empty string, you can use the `from` option to specify the 44 | > starting index of `1n`. 45 | 46 | ## Requirements 47 | 48 | - Node.js >= 22 49 | 50 | ## Install 51 | 52 | With NPM: 53 | 54 | ```bash 55 | npm install indexed-string-variation 56 | ``` 57 | 58 | ## Usage 59 | 60 | This library is ESM-only and written in TypeScript. You can import and use it as 61 | follows: 62 | 63 | ```js 64 | import isv from "indexed-string-variation"; 65 | 66 | // Basic usage: generate the first 23 variations for a given alphabet 67 | for ( 68 | const str of isv({ alphabet: "abc1", maxIterations: 23 }) 69 | ) { 70 | console.log(str); 71 | } 72 | 73 | // Generate 5 variations starting from a specific index (using BigInt) 74 | for ( 75 | const str of isv({ 76 | alphabet: "abc1", 77 | from: 20n, 78 | maxIterations: 5, 79 | }) 80 | ) { 81 | console.log(str); 82 | } 83 | 84 | // Generate variations up to a maximum string length of 2 chars 85 | for (const str of isv({ alphabet: "abc1", maxLen: 2 })) { 86 | console.log(str); 87 | } 88 | 89 | // endless variations (careful if you use a `for ... of` loop because it will never end unless you have a break condition!) 90 | const values = isv({ 91 | alphabet: "abc1", 92 | }); 93 | 94 | // pull values from the iterator one by one 95 | console.log(values.next()); // { value: 'a', done: false } 96 | console.log(values.next()); // { value: 'b', done: false } 97 | 98 | // use iterator helpers and the spread operator to pull multiple values 99 | // at once into an array 100 | console.log([...isv({ alphabet: "abc1" }).take(10)]); 101 | // [ 102 | // '', 'a', 'b', 103 | // 'c', '1', 'aa', 104 | // 'ab', 'ac', 'a1', 105 | // 'ba' 106 | // ] 107 | ``` 108 | 109 | > [!TIP]\ 110 | > Find more about iterator helpers the 111 | > [Iterators MDN page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator). 112 | 113 | ### Options 114 | 115 | The `isv` generator function accepts options that allow you to configure how the 116 | generation will behave: 117 | 118 | - `alphabet`: a `string` containing the characters that will be used to generate 119 | the variations. The order of the characters in the string defines their 120 | lexicographic order (defaults to 121 | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`). 122 | - `from`: a `bigint`representing the index from which to start generating the 123 | variations (defaults to `0n`). 124 | - `to?`: a `bigint` representing the index at which to stop generating the 125 | variations (optional, defaults to `undefined`, which indicates infinity). 126 | - maxLen: a `number` representing the maximum length of the generated strings 127 | (optional, defaults to `undefined`, which means no limit). 128 | - maxIterations: a `number` representing the maximum number of iterations to run 129 | (optional, defaults to `undefined`, which means no limit). 130 | 131 | > [!IMPORTANT]\ 132 | > All the options are optional and by default the generator will be endless (it 133 | > will keep generating variations), so if you use it in a `for ... of` loop it 134 | > will never end unless you have an explicit mechanism to break the loop! 135 | > Alternatively, you can use iterator helpers such as 136 | > [`Iterator.prototype.take`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/take) 137 | > to limit the number of iterations. 138 | 139 | ## How the algorithm works 140 | 141 | The way the generation algorithm work is using an n-ary tree where n is the size 142 | of the alphabet. For example, if we have an alphabet containing only `a`, `b` 143 | and `c`, and we want to generate all the strings with a maximum length of 3 the 144 | algorithm will use the following tree: 145 | 146 | ![Sample ternary tree over abc alphabet](doc/sample_diagram.png) 147 | 148 | The tree is to be considered "virtual", because it's never generated in its 149 | integrity, so the used space in memory is minimal. 150 | 151 | In summary, we can describe the algorithm as follows: 152 | 153 | > Given an index **i** over an alphabet of length **n**, and it's corresponding 154 | > n-ary tree, the string associated to **i** corresponds to the string obtained 155 | > by concatenating all the characters found in the path that goes from the root 156 | > node to the **i**-th node. 157 | > 158 | > Note that since the library exposes a generator/iterator interface, the value 159 | > of **i** is managed internally be the iterator. 160 | 161 | ## TypeScript 162 | 163 | Type definitions are included. You can use this library with full type safety in 164 | TypeScript projects. 165 | 166 | ## Testing 167 | 168 | This project uses [Vitest](https://vitest.dev/): 169 | 170 | ```bash 171 | npm test 172 | ``` 173 | 174 | ## Development 175 | 176 | - Source code is in `src/` (TypeScript) 177 | - Build output is in `dist/` 178 | - Tests are in `src/test.ts` 179 | 180 | ## Migration notes 181 | 182 | - The library now uses native JavaScript `BigInt` instead of the `big-integer` 183 | dependency. 184 | - Only ESM is supported (no CommonJS `require`). 185 | - Node.js 22 or newer is required. 186 | 187 | ## License 188 | 189 | Licensed under [MIT License](LICENSE). © Luciano Mammino. 190 | -------------------------------------------------------------------------------- /doc/sample_diagram.xml: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------