├── .npmrc ├── .travis.yml ├── .gitattributes ├── .editorconfig ├── .gitignore ├── LICENSE ├── examples.js ├── package.json ├── .eslintrc.json ├── .verb.md ├── index.js ├── README.md └── test └── test.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - linux 4 | - osx 5 | - windows 6 | language: node_js 7 | node_js: 8 | - node 9 | - '10' 10 | - '9' 11 | - '8' 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text eol=lf 3 | 4 | # binaries 5 | *.ai binary 6 | *.psd binary 7 | *.jpg binary 8 | *.gif binary 9 | *.png binary 10 | *.jpeg binary 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [{**/{actual,fixtures,expected,templates}/**,*.md}] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | .idea 4 | .vscode 5 | *.sublime-* 6 | 7 | # test related, or directories generated by tests 8 | test/actual 9 | actual 10 | coverage 11 | .nyc* 12 | 13 | # npm 14 | node_modules 15 | npm-debug.log 16 | 17 | # yarn 18 | yarn.lock 19 | yarn-error.log 20 | 21 | # misc 22 | _gh_pages 23 | _draft 24 | _drafts 25 | bower_components 26 | vendor 27 | temp 28 | tmp 29 | TODO.md 30 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Jon Schlinkert. 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 | -------------------------------------------------------------------------------- /examples.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toRegexRange = require('./'); 4 | const table = require('text-table'); 5 | const Time = require('time-diff'); 6 | const time = new Time(); 7 | 8 | /** 9 | * $ node examples.js 10 | */ 11 | 12 | const toRange = (min, max) => { 13 | let key = 'to-range' + min + max; 14 | time.start(key); 15 | 16 | return [ 17 | '', 18 | `\`toRegexRange(${min}, ${max})\``, 19 | `\`${toRegexRange(min, max, { wrap: false }).split('|').join('\\|')}\``, 20 | `_${time.end(key)}_`, 21 | '' 22 | ]; 23 | }; 24 | 25 | toRange('1', '3'); 26 | 27 | let rows = [ 28 | ['', '**Range**', '**Result**', '**Compile time**', ''], 29 | ['', '--- ', '--- ', '---', ''], 30 | ]; 31 | 32 | let examples = [ 33 | ['-10', '10'], 34 | ['-100', '-10'], 35 | ['-100', '100'], 36 | 37 | ['001', '100'], 38 | ['001', '555'], 39 | ['0010', '1000'], 40 | 41 | ['1', '50'], 42 | ['1', '55'], 43 | ['1', '555'], 44 | ['1', '5555'], 45 | ['111', '555'], 46 | ['29', '51'], 47 | ['31', '877'], 48 | 49 | ['5', '5'], 50 | ['5', '6'], 51 | ['1', '2'], 52 | ['1', '5'], 53 | ['1', '10'], 54 | ['1', '100'], 55 | ['1', '1000'], 56 | ['1', '10000'], 57 | ['1', '100000'], 58 | ['1', '1000000'], 59 | ['1', '10000000'], 60 | ].forEach(args => { 61 | rows.push(toRange.apply(null, args)); 62 | }); 63 | 64 | let text = table(rows, { hsep: ' | ' }); 65 | console.log(text); 66 | 67 | /** 68 | * This method is exposed as a helper, which is picked up 69 | * by verb and used in the .verb.md readme template 70 | */ 71 | 72 | module.exports = () => { 73 | return text.split('\n').map(line => line.replace(/^ +/, '')).join('\n'); 74 | }; 75 | 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "to-regex-range", 3 | "description": "Pass two numbers, get a regex-compatible source string for matching ranges. Validated against more than 2.78 million test assertions.", 4 | "version": "5.0.1", 5 | "homepage": "https://github.com/micromatch/to-regex-range", 6 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", 7 | "contributors": [ 8 | "Jon Schlinkert (http://twitter.com/jonschlinkert)", 9 | "Rouven Weßling (www.rouvenwessling.de)" 10 | ], 11 | "repository": "micromatch/to-regex-range", 12 | "bugs": { 13 | "url": "https://github.com/micromatch/to-regex-range/issues" 14 | }, 15 | "license": "MIT", 16 | "files": [ 17 | "index.js" 18 | ], 19 | "main": "index.js", 20 | "engines": { 21 | "node": ">=8.0" 22 | }, 23 | "scripts": { 24 | "test": "mocha" 25 | }, 26 | "devDependencies": { 27 | "fill-range": "^6.0.0", 28 | "gulp-format-md": "^2.0.0", 29 | "mocha": "^6.0.2", 30 | "text-table": "^0.2.0", 31 | "time-diff": "^0.3.1" 32 | }, 33 | "keywords": [ 34 | "bash", 35 | "date", 36 | "expand", 37 | "expansion", 38 | "expression", 39 | "glob", 40 | "match", 41 | "match date", 42 | "match number", 43 | "match numbers", 44 | "match year", 45 | "matches", 46 | "matching", 47 | "number", 48 | "numbers", 49 | "numerical", 50 | "range", 51 | "ranges", 52 | "regex", 53 | "regexp", 54 | "regular", 55 | "regular expression", 56 | "sequence" 57 | ], 58 | "verb": { 59 | "layout": "default", 60 | "toc": false, 61 | "tasks": [ 62 | "readme" 63 | ], 64 | "plugins": [ 65 | "gulp-format-md" 66 | ], 67 | "lint": { 68 | "reflinks": true 69 | }, 70 | "helpers": { 71 | "examples": { 72 | "displayName": "examples" 73 | } 74 | }, 75 | "related": { 76 | "list": [ 77 | "expand-range", 78 | "fill-range", 79 | "micromatch", 80 | "repeat-element", 81 | "repeat-string" 82 | ] 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "browser": false, 8 | "es6": true, 9 | "node": true, 10 | "mocha": true 11 | }, 12 | 13 | "parserOptions":{ 14 | "ecmaVersion": 9, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "modules": true, 18 | "experimentalObjectRestSpread": true 19 | } 20 | }, 21 | 22 | "globals": { 23 | "document": false, 24 | "navigator": false, 25 | "window": false 26 | }, 27 | 28 | "rules": { 29 | "accessor-pairs": 2, 30 | "arrow-spacing": [2, { "before": true, "after": true }], 31 | "block-spacing": [2, "always"], 32 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 33 | "comma-dangle": [2, "never"], 34 | "comma-spacing": [2, { "before": false, "after": true }], 35 | "comma-style": [2, "last"], 36 | "constructor-super": 2, 37 | "curly": [2, "multi-line"], 38 | "dot-location": [2, "property"], 39 | "eol-last": 2, 40 | "eqeqeq": [2, "allow-null"], 41 | "generator-star-spacing": [2, { "before": true, "after": true }], 42 | "handle-callback-err": [2, "^(err|error)$" ], 43 | "indent": [2, 2, { "SwitchCase": 1 }], 44 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 45 | "keyword-spacing": [2, { "before": true, "after": true }], 46 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 47 | "new-parens": 2, 48 | "no-array-constructor": 2, 49 | "no-caller": 2, 50 | "no-class-assign": 2, 51 | "no-cond-assign": 2, 52 | "no-const-assign": 2, 53 | "no-control-regex": 2, 54 | "no-debugger": 2, 55 | "no-delete-var": 2, 56 | "no-dupe-args": 2, 57 | "no-dupe-class-members": 2, 58 | "no-dupe-keys": 2, 59 | "no-duplicate-case": 2, 60 | "no-empty-character-class": 2, 61 | "no-eval": 2, 62 | "no-ex-assign": 2, 63 | "no-extend-native": 2, 64 | "no-extra-bind": 2, 65 | "no-extra-boolean-cast": 2, 66 | "no-extra-parens": [2, "functions"], 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 2, 69 | "no-func-assign": 2, 70 | "no-implied-eval": 2, 71 | "no-inner-declarations": [2, "functions"], 72 | "no-invalid-regexp": 2, 73 | "no-irregular-whitespace": 2, 74 | "no-iterator": 2, 75 | "no-label-var": 2, 76 | "no-labels": 2, 77 | "no-lone-blocks": 2, 78 | "no-mixed-spaces-and-tabs": 2, 79 | "no-multi-spaces": 2, 80 | "no-multi-str": 2, 81 | "no-multiple-empty-lines": [2, { "max": 1 }], 82 | "no-native-reassign": 0, 83 | "no-negated-in-lhs": 2, 84 | "no-new": 2, 85 | "no-new-func": 2, 86 | "no-new-object": 2, 87 | "no-new-require": 2, 88 | "no-new-wrappers": 2, 89 | "no-obj-calls": 2, 90 | "no-octal": 2, 91 | "no-octal-escape": 2, 92 | "no-proto": 0, 93 | "no-redeclare": 2, 94 | "no-regex-spaces": 2, 95 | "no-return-assign": 2, 96 | "no-self-compare": 2, 97 | "no-sequences": 2, 98 | "no-shadow-restricted-names": 2, 99 | "no-spaced-func": 2, 100 | "no-sparse-arrays": 2, 101 | "no-this-before-super": 2, 102 | "no-throw-literal": 2, 103 | "no-trailing-spaces": 0, 104 | "no-undef": 2, 105 | "no-undef-init": 2, 106 | "no-unexpected-multiline": 2, 107 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 108 | "no-unreachable": 2, 109 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 110 | "no-useless-call": 0, 111 | "no-with": 2, 112 | "one-var": [0, { "initialized": "never" }], 113 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 114 | "padded-blocks": [0, "never"], 115 | "quotes": [2, "single", "avoid-escape"], 116 | "radix": 2, 117 | "semi": [2, "always"], 118 | "semi-spacing": [2, { "before": false, "after": true }], 119 | "space-before-blocks": [2, "always"], 120 | "space-before-function-paren": [2, "never"], 121 | "space-in-parens": [2, "never"], 122 | "space-infix-ops": 2, 123 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 124 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 125 | "use-isnan": 2, 126 | "valid-typeof": 2, 127 | "wrap-iife": [2, "any"], 128 | "yoda": [2, "never"] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 |
2 | What does this do? 3 | 4 |
5 | 6 | This libary generates the `source` string to be passed to `new RegExp()` for matching a range of numbers. 7 | 8 | **Example** 9 | 10 | ```js 11 | const toRegexRange = require('{%= name %}'); 12 | const regex = new RegExp(toRegexRange('15', '95')); 13 | ``` 14 | 15 | A string is returned so that you can do whatever you need with it before passing it to `new RegExp()` (like adding `^` or `$` boundaries, defining flags, or combining it another string). 16 | 17 |
18 | 19 |
20 | 21 |
22 | Why use this library? 23 | 24 |
25 | 26 | ### Convenience 27 | 28 | Creating regular expressions for matching numbers gets deceptively complicated pretty fast. 29 | 30 | For example, let's say you need a validation regex for matching part of a user-id, postal code, social security number, tax id, etc: 31 | 32 | - regex for matching `1` => `/1/` (easy enough) 33 | - regex for matching `1` through `5` => `/[1-5]/` (not bad...) 34 | - regex for matching `1` or `5` => `/(1|5)/` (still easy...) 35 | - regex for matching `1` through `50` => `/([1-9]|[1-4][0-9]|50)/` (uh-oh...) 36 | - regex for matching `1` through `55` => `/([1-9]|[1-4][0-9]|5[0-5])/` (no prob, I can do this...) 37 | - regex for matching `1` through `555` => `/([1-9]|[1-9][0-9]|[1-4][0-9]{2}|5[0-4][0-9]|55[0-5])/` (maybe not...) 38 | - regex for matching `0001` through `5555` => `/(0{3}[1-9]|0{2}[1-9][0-9]|0[1-9][0-9]{2}|[1-4][0-9]{3}|5[0-4][0-9]{2}|55[0-4][0-9]|555[0-5])/` (okay, I get the point!) 39 | 40 | The numbers are contrived, but they're also really basic. In the real world you might need to generate a regex on-the-fly for validation. 41 | 42 | **Learn more** 43 | 44 | If you're interested in learning more about [character classes](http://www.regular-expressions.info/charclass.html) and other regex features, I personally have always found [regular-expressions.info](http://www.regular-expressions.info/charclass.html) to be pretty useful. 45 | 46 | 47 | ### Heavily tested 48 | 49 | As of {%= date() %}, this library runs [>1m test assertions](./test/test.js) against generated regex-ranges to provide brute-force verification that results are correct. 50 | 51 | Tests run in ~280ms on my MacBook Pro, 2.5 GHz Intel Core i7. 52 | 53 | ### Optimized 54 | 55 | Generated regular expressions are optimized: 56 | 57 | - duplicate sequences and character classes are reduced using quantifiers 58 | - smart enough to use `?` conditionals when number(s) or range(s) can be positive or negative 59 | - uses fragment caching to avoid processing the same exact string more than once 60 | 61 |
62 | 63 |
64 | 65 | ## Usage 66 | 67 | Add this library to your javascript application with the following line of code 68 | 69 | ```js 70 | const toRegexRange = require('{%= name %}'); 71 | ``` 72 | 73 | The main export is a function that takes two integers: the `min` value and `max` value (formatted as strings or numbers). 74 | 75 | ```js 76 | const source = toRegexRange('15', '95'); 77 | //=> 1[5-9]|[2-8][0-9]|9[0-5] 78 | 79 | const regex = new RegExp(`^${source}$`); 80 | console.log(regex.test('14')); //=> false 81 | console.log(regex.test('50')); //=> true 82 | console.log(regex.test('94')); //=> true 83 | console.log(regex.test('96')); //=> false 84 | ``` 85 | 86 | ## Options 87 | 88 | ### options.capture 89 | 90 | **Type**: `boolean` 91 | 92 | **Deafault**: `undefined` 93 | 94 | Wrap the returned value in parentheses when there is more than one regex condition. Useful when you're dynamically generating ranges. 95 | 96 | ```js 97 | console.log(toRegexRange('-10', '10')); 98 | //=> -[1-9]|-?10|[0-9] 99 | 100 | console.log(toRegexRange('-10', '10', { capture: true })); 101 | //=> (-[1-9]|-?10|[0-9]) 102 | ``` 103 | 104 | ### options.shorthand 105 | 106 | **Type**: `boolean` 107 | 108 | **Deafault**: `undefined` 109 | 110 | Use the regex shorthand for `[0-9]`: 111 | 112 | ```js 113 | console.log(toRegexRange('0', '999999')); 114 | //=> [0-9]|[1-9][0-9]{1,5} 115 | 116 | console.log(toRegexRange('0', '999999', { shorthand: true })); 117 | //=> \d|[1-9]\d{1,5} 118 | ``` 119 | 120 | ### options.relaxZeros 121 | 122 | **Type**: `boolean` 123 | 124 | **Default**: `true` 125 | 126 | This option relaxes matching for leading zeros when when ranges are zero-padded. 127 | 128 | ```js 129 | const source = toRegexRange('-0010', '0010'); 130 | const regex = new RegExp(`^${source}$`); 131 | console.log(regex.test('-10')); //=> true 132 | console.log(regex.test('-010')); //=> true 133 | console.log(regex.test('-0010')); //=> true 134 | console.log(regex.test('10')); //=> true 135 | console.log(regex.test('010')); //=> true 136 | console.log(regex.test('0010')); //=> true 137 | ``` 138 | 139 | When `relaxZeros` is false, matching is strict: 140 | 141 | ```js 142 | const source = toRegexRange('-0010', '0010', { relaxZeros: false }); 143 | const regex = new RegExp(`^${source}$`); 144 | console.log(regex.test('-10')); //=> false 145 | console.log(regex.test('-010')); //=> false 146 | console.log(regex.test('-0010')); //=> true 147 | console.log(regex.test('10')); //=> false 148 | console.log(regex.test('010')); //=> false 149 | console.log(regex.test('0010')); //=> true 150 | ``` 151 | 152 | ## Examples 153 | 154 | {%= examples() %} 155 | 156 | ## Heads up! 157 | 158 | **Order of arguments** 159 | 160 | When the `min` is larger than the `max`, values will be flipped to create a valid range: 161 | 162 | ```js 163 | toRegexRange('51', '29'); 164 | ``` 165 | 166 | Is effectively flipped to: 167 | 168 | ```js 169 | toRegexRange('29', '51'); 170 | //=> 29|[3-4][0-9]|5[0-1] 171 | ``` 172 | 173 | **Steps / increments** 174 | 175 | This library does not support steps (increments). A pr to add support would be welcome. 176 | 177 | 178 | ## History 179 | 180 | ### v5.0.0 - 2019-04-07 181 | 182 | Optimizations. Updated code to use newer ES features. 183 | 184 | ### v2.0.0 - 2017-04-21 185 | 186 | **New features** 187 | 188 | Adds support for zero-padding! 189 | 190 | ### v1.0.0 191 | 192 | **Optimizations** 193 | 194 | Repeating ranges are now grouped using quantifiers. rocessing time is roughly the same, but the generated regex is much smaller, which should result in faster matching. 195 | 196 | ## Attribution 197 | 198 | Inspired by the python library [range-regex](https://github.com/dimka665/range-regex). -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * to-regex-range 3 | * 4 | * Copyright (c) 2015-present, Jon Schlinkert. 5 | * Released under the MIT License. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const isNumber = (v) => (typeof v === "number" && v - v === 0) || (typeof v === "string" && Number.isFinite(+v) && v.trim() !== ""); 11 | 12 | const toRegexRange = (min, max, options) => { 13 | if (isNumber(min) === false) { 14 | throw new TypeError('toRegexRange: expected the first argument to be a number'); 15 | } 16 | 17 | if (max === void 0 || min === max) { 18 | return String(min); 19 | } 20 | 21 | if (isNumber(max) === false) { 22 | throw new TypeError('toRegexRange: expected the second argument to be a number.'); 23 | } 24 | 25 | let opts = { relaxZeros: true, ...options }; 26 | if (typeof opts.strictZeros === 'boolean') { 27 | opts.relaxZeros = opts.strictZeros === false; 28 | } 29 | 30 | let relax = String(opts.relaxZeros); 31 | let shorthand = String(opts.shorthand); 32 | let capture = String(opts.capture); 33 | let wrap = String(opts.wrap); 34 | let cacheKey = min + ':' + max + '=' + relax + shorthand + capture + wrap; 35 | 36 | if (toRegexRange.cache.hasOwnProperty(cacheKey)) { 37 | return toRegexRange.cache[cacheKey].result; 38 | } 39 | 40 | let a = Math.min(min, max); 41 | let b = Math.max(min, max); 42 | 43 | if (Math.abs(a - b) === 1) { 44 | let result = min + '|' + max; 45 | if (opts.capture) { 46 | return `(${result})`; 47 | } 48 | if (opts.wrap === false) { 49 | return result; 50 | } 51 | return `(?:${result})`; 52 | } 53 | 54 | let isPadded = hasPadding(min) || hasPadding(max); 55 | let state = { min, max, a, b }; 56 | let positives = []; 57 | let negatives = []; 58 | 59 | if (isPadded) { 60 | state.isPadded = isPadded; 61 | state.maxLen = String(state.max).length; 62 | } 63 | 64 | if (a < 0) { 65 | let newMin = b < 0 ? Math.abs(b) : 1; 66 | negatives = splitToPatterns(newMin, Math.abs(a), state, opts); 67 | a = state.a = 0; 68 | } 69 | 70 | if (b >= 0) { 71 | positives = splitToPatterns(a, b, state, opts); 72 | } 73 | 74 | state.negatives = negatives; 75 | state.positives = positives; 76 | state.result = collatePatterns(negatives, positives, opts); 77 | 78 | if (opts.capture === true) { 79 | state.result = `(${state.result})`; 80 | } else if (opts.wrap !== false && (positives.length + negatives.length) > 1) { 81 | state.result = `(?:${state.result})`; 82 | } 83 | 84 | toRegexRange.cache[cacheKey] = state; 85 | return state.result; 86 | }; 87 | 88 | function collatePatterns(neg, pos, options) { 89 | let onlyNegative = filterPatterns(neg, pos, '-', false, options) || []; 90 | let onlyPositive = filterPatterns(pos, neg, '', false, options) || []; 91 | let intersected = filterPatterns(neg, pos, '-?', true, options) || []; 92 | let subpatterns = onlyNegative.concat(intersected).concat(onlyPositive); 93 | return subpatterns.join('|'); 94 | } 95 | 96 | function splitToRanges(min, max) { 97 | let nines = 1; 98 | let zeros = 1; 99 | 100 | let stop = countNines(min, nines); 101 | let stops = new Set([max]); 102 | 103 | while (min <= stop && stop <= max) { 104 | stops.add(stop); 105 | nines += 1; 106 | stop = countNines(min, nines); 107 | } 108 | 109 | stop = countZeros(max + 1, zeros) - 1; 110 | 111 | while (min < stop && stop <= max) { 112 | stops.add(stop); 113 | zeros += 1; 114 | stop = countZeros(max + 1, zeros) - 1; 115 | } 116 | 117 | stops = [...stops]; 118 | stops.sort(compare); 119 | return stops; 120 | } 121 | 122 | /** 123 | * Convert a range to a regex pattern 124 | * @param {Number} `start` 125 | * @param {Number} `stop` 126 | * @return {String} 127 | */ 128 | 129 | function rangeToPattern(start, stop, options) { 130 | if (start === stop) { 131 | return { pattern: start, count: [], digits: 0 }; 132 | } 133 | 134 | let zipped = zip(start, stop); 135 | let digits = zipped.length; 136 | let pattern = ''; 137 | let count = 0; 138 | 139 | for (let i = 0; i < digits; i++) { 140 | let [startDigit, stopDigit] = zipped[i]; 141 | 142 | if (startDigit === stopDigit) { 143 | pattern += startDigit; 144 | 145 | } else if (startDigit !== '0' || stopDigit !== '9') { 146 | pattern += toCharacterClass(startDigit, stopDigit, options); 147 | 148 | } else { 149 | count++; 150 | } 151 | } 152 | 153 | if (count) { 154 | pattern += options.shorthand === true ? '\\d' : '[0-9]'; 155 | } 156 | 157 | return { pattern, count: [count], digits }; 158 | } 159 | 160 | function splitToPatterns(min, max, tok, options) { 161 | let ranges = splitToRanges(min, max); 162 | let tokens = []; 163 | let start = min; 164 | let prev; 165 | 166 | for (let i = 0; i < ranges.length; i++) { 167 | let max = ranges[i]; 168 | let obj = rangeToPattern(String(start), String(max), options); 169 | let zeros = ''; 170 | 171 | if (!tok.isPadded && prev && prev.pattern === obj.pattern) { 172 | if (prev.count.length > 1) { 173 | prev.count.pop(); 174 | } 175 | 176 | prev.count.push(obj.count[0]); 177 | prev.string = prev.pattern + toQuantifier(prev.count); 178 | start = max + 1; 179 | continue; 180 | } 181 | 182 | if (tok.isPadded) { 183 | zeros = padZeros(max, tok, options); 184 | } 185 | 186 | obj.string = zeros + obj.pattern + toQuantifier(obj.count); 187 | tokens.push(obj); 188 | start = max + 1; 189 | prev = obj; 190 | } 191 | 192 | return tokens; 193 | } 194 | 195 | function filterPatterns(arr, comparison, prefix, intersection, options) { 196 | let result = []; 197 | 198 | for (let ele of arr) { 199 | let { string } = ele; 200 | 201 | // only push if _both_ are negative... 202 | if (!intersection && !contains(comparison, 'string', string)) { 203 | result.push(prefix + string); 204 | } 205 | 206 | // or _both_ are positive 207 | if (intersection && contains(comparison, 'string', string)) { 208 | result.push(prefix + string); 209 | } 210 | } 211 | return result; 212 | } 213 | 214 | /** 215 | * Zip strings 216 | */ 217 | 218 | function zip(a, b) { 219 | let arr = []; 220 | for (let i = 0; i < a.length; i++) arr.push([a[i], b[i]]); 221 | return arr; 222 | } 223 | 224 | function compare(a, b) { 225 | return a > b ? 1 : b > a ? -1 : 0; 226 | } 227 | 228 | function contains(arr, key, val) { 229 | return arr.some(ele => ele[key] === val); 230 | } 231 | 232 | function countNines(min, len) { 233 | return Number(String(min).slice(0, -len) + '9'.repeat(len)); 234 | } 235 | 236 | function countZeros(integer, zeros) { 237 | return integer - (integer % Math.pow(10, zeros)); 238 | } 239 | 240 | function toQuantifier(digits) { 241 | let [start = 0, stop = ''] = digits; 242 | if (stop || start > 1) { 243 | return `{${start + (stop ? ',' + stop : '')}}`; 244 | } 245 | return ''; 246 | } 247 | 248 | function toCharacterClass(a, b, options) { 249 | return `[${a}${(b - a === 1) ? '' : '-'}${b}]`; 250 | } 251 | 252 | function hasPadding(str) { 253 | return /^-?(0+)\d/.test(str); 254 | } 255 | 256 | function padZeros(value, tok, options) { 257 | if (!tok.isPadded) { 258 | return value; 259 | } 260 | 261 | let diff = Math.abs(tok.maxLen - String(value).length); 262 | let relax = options.relaxZeros !== false; 263 | 264 | switch (diff) { 265 | case 0: 266 | return ''; 267 | case 1: 268 | return relax ? '0?' : '0'; 269 | case 2: 270 | return relax ? '0{0,2}' : '00'; 271 | default: { 272 | return relax ? `0{0,${diff}}` : `0{${diff}}`; 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * Cache 279 | */ 280 | 281 | toRegexRange.cache = {}; 282 | toRegexRange.clearCache = () => (toRegexRange.cache = {}); 283 | 284 | /** 285 | * Expose `toRegexRange` 286 | */ 287 | 288 | module.exports = toRegexRange; 289 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # to-regex-range [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W8YFZ425KND68) [![NPM version](https://img.shields.io/npm/v/to-regex-range.svg?style=flat)](https://www.npmjs.com/package/to-regex-range) [![NPM monthly downloads](https://img.shields.io/npm/dm/to-regex-range.svg?style=flat)](https://npmjs.org/package/to-regex-range) [![NPM total downloads](https://img.shields.io/npm/dt/to-regex-range.svg?style=flat)](https://npmjs.org/package/to-regex-range) [![Linux Build Status](https://img.shields.io/travis/micromatch/to-regex-range.svg?style=flat&label=Travis)](https://travis-ci.org/micromatch/to-regex-range) 2 | 3 | > Pass two numbers, get a regex-compatible source string for matching ranges. Validated against more than 2.78 million test assertions. 4 | 5 | Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. 6 | 7 | ## Install 8 | 9 | Install with [npm](https://www.npmjs.com/): 10 | 11 | ```sh 12 | $ npm install --save to-regex-range 13 | ``` 14 | 15 |
16 | What does this do? 17 | 18 |
19 | 20 | This libary generates the `source` string to be passed to `new RegExp()` for matching a range of numbers. 21 | 22 | **Example** 23 | 24 | ```js 25 | const toRegexRange = require('to-regex-range'); 26 | const regex = new RegExp(toRegexRange('15', '95')); 27 | ``` 28 | 29 | A string is returned so that you can do whatever you need with it before passing it to `new RegExp()` (like adding `^` or `$` boundaries, defining flags, or combining it another string). 30 | 31 |
32 | 33 |
34 | 35 |
36 | Why use this library? 37 | 38 |
39 | 40 | ### Convenience 41 | 42 | Creating regular expressions for matching numbers gets deceptively complicated pretty fast. 43 | 44 | For example, let's say you need a validation regex for matching part of a user-id, postal code, social security number, tax id, etc: 45 | 46 | * regex for matching `1` => `/1/` (easy enough) 47 | * regex for matching `1` through `5` => `/[1-5]/` (not bad...) 48 | * regex for matching `1` or `5` => `/(1|5)/` (still easy...) 49 | * regex for matching `1` through `50` => `/([1-9]|[1-4][0-9]|50)/` (uh-oh...) 50 | * regex for matching `1` through `55` => `/([1-9]|[1-4][0-9]|5[0-5])/` (no prob, I can do this...) 51 | * regex for matching `1` through `555` => `/([1-9]|[1-9][0-9]|[1-4][0-9]{2}|5[0-4][0-9]|55[0-5])/` (maybe not...) 52 | * regex for matching `0001` through `5555` => `/(0{3}[1-9]|0{2}[1-9][0-9]|0[1-9][0-9]{2}|[1-4][0-9]{3}|5[0-4][0-9]{2}|55[0-4][0-9]|555[0-5])/` (okay, I get the point!) 53 | 54 | The numbers are contrived, but they're also really basic. In the real world you might need to generate a regex on-the-fly for validation. 55 | 56 | **Learn more** 57 | 58 | If you're interested in learning more about [character classes](http://www.regular-expressions.info/charclass.html) and other regex features, I personally have always found [regular-expressions.info](http://www.regular-expressions.info/charclass.html) to be pretty useful. 59 | 60 | ### Heavily tested 61 | 62 | As of April 07, 2019, this library runs [>1m test assertions](./test/test.js) against generated regex-ranges to provide brute-force verification that results are correct. 63 | 64 | Tests run in ~280ms on my MacBook Pro, 2.5 GHz Intel Core i7. 65 | 66 | ### Optimized 67 | 68 | Generated regular expressions are optimized: 69 | 70 | * duplicate sequences and character classes are reduced using quantifiers 71 | * smart enough to use `?` conditionals when number(s) or range(s) can be positive or negative 72 | * uses fragment caching to avoid processing the same exact string more than once 73 | 74 |
75 | 76 |
77 | 78 | ## Usage 79 | 80 | Add this library to your javascript application with the following line of code 81 | 82 | ```js 83 | const toRegexRange = require('to-regex-range'); 84 | ``` 85 | 86 | The main export is a function that takes two integers: the `min` value and `max` value (formatted as strings or numbers). 87 | 88 | ```js 89 | const source = toRegexRange('15', '95'); 90 | //=> 1[5-9]|[2-8][0-9]|9[0-5] 91 | 92 | const regex = new RegExp(`^${source}$`); 93 | console.log(regex.test('14')); //=> false 94 | console.log(regex.test('50')); //=> true 95 | console.log(regex.test('94')); //=> true 96 | console.log(regex.test('96')); //=> false 97 | ``` 98 | 99 | ## Options 100 | 101 | ### options.capture 102 | 103 | **Type**: `boolean` 104 | 105 | **Deafault**: `undefined` 106 | 107 | Wrap the returned value in parentheses when there is more than one regex condition. Useful when you're dynamically generating ranges. 108 | 109 | ```js 110 | console.log(toRegexRange('-10', '10')); 111 | //=> -[1-9]|-?10|[0-9] 112 | 113 | console.log(toRegexRange('-10', '10', { capture: true })); 114 | //=> (-[1-9]|-?10|[0-9]) 115 | ``` 116 | 117 | ### options.shorthand 118 | 119 | **Type**: `boolean` 120 | 121 | **Deafault**: `undefined` 122 | 123 | Use the regex shorthand for `[0-9]`: 124 | 125 | ```js 126 | console.log(toRegexRange('0', '999999')); 127 | //=> [0-9]|[1-9][0-9]{1,5} 128 | 129 | console.log(toRegexRange('0', '999999', { shorthand: true })); 130 | //=> \d|[1-9]\d{1,5} 131 | ``` 132 | 133 | ### options.relaxZeros 134 | 135 | **Type**: `boolean` 136 | 137 | **Default**: `true` 138 | 139 | This option relaxes matching for leading zeros when when ranges are zero-padded. 140 | 141 | ```js 142 | const source = toRegexRange('-0010', '0010'); 143 | const regex = new RegExp(`^${source}$`); 144 | console.log(regex.test('-10')); //=> true 145 | console.log(regex.test('-010')); //=> true 146 | console.log(regex.test('-0010')); //=> true 147 | console.log(regex.test('10')); //=> true 148 | console.log(regex.test('010')); //=> true 149 | console.log(regex.test('0010')); //=> true 150 | ``` 151 | 152 | When `relaxZeros` is false, matching is strict: 153 | 154 | ```js 155 | const source = toRegexRange('-0010', '0010', { relaxZeros: false }); 156 | const regex = new RegExp(`^${source}$`); 157 | console.log(regex.test('-10')); //=> false 158 | console.log(regex.test('-010')); //=> false 159 | console.log(regex.test('-0010')); //=> true 160 | console.log(regex.test('10')); //=> false 161 | console.log(regex.test('010')); //=> false 162 | console.log(regex.test('0010')); //=> true 163 | ``` 164 | 165 | ## Examples 166 | 167 | | **Range** | **Result** | **Compile time** | 168 | | --- | --- | --- | 169 | | `toRegexRange(-10, 10)` | `-[1-9]\|-?10\|[0-9]` | _132μs_ | 170 | | `toRegexRange(-100, -10)` | `-1[0-9]\|-[2-9][0-9]\|-100` | _50μs_ | 171 | | `toRegexRange(-100, 100)` | `-[1-9]\|-?[1-9][0-9]\|-?100\|[0-9]` | _42μs_ | 172 | | `toRegexRange(001, 100)` | `0{0,2}[1-9]\|0?[1-9][0-9]\|100` | _109μs_ | 173 | | `toRegexRange(001, 555)` | `0{0,2}[1-9]\|0?[1-9][0-9]\|[1-4][0-9]{2}\|5[0-4][0-9]\|55[0-5]` | _51μs_ | 174 | | `toRegexRange(0010, 1000)` | `0{0,2}1[0-9]\|0{0,2}[2-9][0-9]\|0?[1-9][0-9]{2}\|1000` | _31μs_ | 175 | | `toRegexRange(1, 50)` | `[1-9]\|[1-4][0-9]\|50` | _24μs_ | 176 | | `toRegexRange(1, 55)` | `[1-9]\|[1-4][0-9]\|5[0-5]` | _23μs_ | 177 | | `toRegexRange(1, 555)` | `[1-9]\|[1-9][0-9]\|[1-4][0-9]{2}\|5[0-4][0-9]\|55[0-5]` | _30μs_ | 178 | | `toRegexRange(1, 5555)` | `[1-9]\|[1-9][0-9]{1,2}\|[1-4][0-9]{3}\|5[0-4][0-9]{2}\|55[0-4][0-9]\|555[0-5]` | _43μs_ | 179 | | `toRegexRange(111, 555)` | `11[1-9]\|1[2-9][0-9]\|[2-4][0-9]{2}\|5[0-4][0-9]\|55[0-5]` | _38μs_ | 180 | | `toRegexRange(29, 51)` | `29\|[34][0-9]\|5[01]` | _24μs_ | 181 | | `toRegexRange(31, 877)` | `3[1-9]\|[4-9][0-9]\|[1-7][0-9]{2}\|8[0-6][0-9]\|87[0-7]` | _32μs_ | 182 | | `toRegexRange(5, 5)` | `5` | _8μs_ | 183 | | `toRegexRange(5, 6)` | `5\|6` | _11μs_ | 184 | | `toRegexRange(1, 2)` | `1\|2` | _6μs_ | 185 | | `toRegexRange(1, 5)` | `[1-5]` | _15μs_ | 186 | | `toRegexRange(1, 10)` | `[1-9]\|10` | _22μs_ | 187 | | `toRegexRange(1, 100)` | `[1-9]\|[1-9][0-9]\|100` | _25μs_ | 188 | | `toRegexRange(1, 1000)` | `[1-9]\|[1-9][0-9]{1,2}\|1000` | _31μs_ | 189 | | `toRegexRange(1, 10000)` | `[1-9]\|[1-9][0-9]{1,3}\|10000` | _34μs_ | 190 | | `toRegexRange(1, 100000)` | `[1-9]\|[1-9][0-9]{1,4}\|100000` | _36μs_ | 191 | | `toRegexRange(1, 1000000)` | `[1-9]\|[1-9][0-9]{1,5}\|1000000` | _42μs_ | 192 | | `toRegexRange(1, 10000000)` | `[1-9]\|[1-9][0-9]{1,6}\|10000000` | _42μs_ | 193 | 194 | ## Heads up! 195 | 196 | **Order of arguments** 197 | 198 | When the `min` is larger than the `max`, values will be flipped to create a valid range: 199 | 200 | ```js 201 | toRegexRange('51', '29'); 202 | ``` 203 | 204 | Is effectively flipped to: 205 | 206 | ```js 207 | toRegexRange('29', '51'); 208 | //=> 29|[3-4][0-9]|5[0-1] 209 | ``` 210 | 211 | **Steps / increments** 212 | 213 | This library does not support steps (increments). A pr to add support would be welcome. 214 | 215 | ## History 216 | 217 | ### v2.0.0 - 2017-04-21 218 | 219 | **New features** 220 | 221 | Adds support for zero-padding! 222 | 223 | ### v1.0.0 224 | 225 | **Optimizations** 226 | 227 | Repeating ranges are now grouped using quantifiers. rocessing time is roughly the same, but the generated regex is much smaller, which should result in faster matching. 228 | 229 | ## Attribution 230 | 231 | Inspired by the python library [range-regex](https://github.com/dimka665/range-regex). 232 | 233 | ## About 234 | 235 |
236 | Contributing 237 | 238 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 239 | 240 |
241 | 242 |
243 | Running Tests 244 | 245 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: 246 | 247 | ```sh 248 | $ npm install && npm test 249 | ``` 250 | 251 |
252 | 253 |
254 | Building docs 255 | 256 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ 257 | 258 | To generate the readme, run the following command: 259 | 260 | ```sh 261 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 262 | ``` 263 | 264 |
265 | 266 | ### Related projects 267 | 268 | You might also be interested in these projects: 269 | 270 | * [expand-range](https://www.npmjs.com/package/expand-range): Fast, bash-like range expansion. Expand a range of numbers or letters, uppercase or lowercase. Used… [more](https://github.com/jonschlinkert/expand-range) | [homepage](https://github.com/jonschlinkert/expand-range "Fast, bash-like range expansion. Expand a range of numbers or letters, uppercase or lowercase. Used by micromatch.") 271 | * [fill-range](https://www.npmjs.com/package/fill-range): Fill in a range of numbers or letters, optionally passing an increment or `step` to… [more](https://github.com/jonschlinkert/fill-range) | [homepage](https://github.com/jonschlinkert/fill-range "Fill in a range of numbers or letters, optionally passing an increment or `step` to use, or create a regex-compatible range with `options.toRegex`") 272 | * [micromatch](https://www.npmjs.com/package/micromatch): Glob matching for javascript/node.js. A drop-in replacement and faster alternative to minimatch and multimatch. | [homepage](https://github.com/micromatch/micromatch "Glob matching for javascript/node.js. A drop-in replacement and faster alternative to minimatch and multimatch.") 273 | * [repeat-element](https://www.npmjs.com/package/repeat-element): Create an array by repeating the given value n times. | [homepage](https://github.com/jonschlinkert/repeat-element "Create an array by repeating the given value n times.") 274 | * [repeat-string](https://www.npmjs.com/package/repeat-string): Repeat the given string n times. Fastest implementation for repeating a string. | [homepage](https://github.com/jonschlinkert/repeat-string "Repeat the given string n times. Fastest implementation for repeating a string.") 275 | 276 | ### Contributors 277 | 278 | | **Commits** | **Contributor** | 279 | | --- | --- | 280 | | 63 | [jonschlinkert](https://github.com/jonschlinkert) | 281 | | 3 | [doowb](https://github.com/doowb) | 282 | | 2 | [realityking](https://github.com/realityking) | 283 | 284 | ### Author 285 | 286 | **Jon Schlinkert** 287 | 288 | * [GitHub Profile](https://github.com/jonschlinkert) 289 | * [Twitter Profile](https://twitter.com/jonschlinkert) 290 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) 291 | 292 | Please consider supporting me on Patreon, or [start your own Patreon page](https://patreon.com/invite/bxpbvm)! 293 | 294 | 295 | 296 | 297 | 298 | ### License 299 | 300 | Copyright © 2019, [Jon Schlinkert](https://github.com/jonschlinkert). 301 | Released under the [MIT License](LICENSE). 302 | 303 | *** 304 | 305 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 07, 2019._ -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert').strict; 5 | const fill = require('fill-range'); 6 | const toRange = require('..'); 7 | let count = 0; 8 | 9 | const inRange = (min, max, num) => min <= num && max >= num; 10 | const toRegex = str => new RegExp(`^${str}$`); 11 | const toRangeRegex = (min, max, options) => { 12 | return toRegex(toRange(min, max, { wrap: true, ...options })); 13 | }; 14 | 15 | const matcher = (...args) => { 16 | const regex = toRangeRegex(...args); 17 | return num => regex.test(String(num)); 18 | }; 19 | 20 | const matchRange = (min, max, expected, match, notMatch) => { 21 | if (max - min >= 1000000) { 22 | throw new RangeError('range is too big'); 23 | } 24 | 25 | let actual = toRange(min, max); 26 | let msg = actual + ' => ' + expected; 27 | 28 | // test expected string 29 | assert.equal(actual, expected, msg); 30 | 31 | let re = toRegex(actual); 32 | for (let i = 0; i < match.length; i++) { 33 | assert(re.test(match[i]), 'should match ' + msg); 34 | count++; 35 | } 36 | 37 | if (!Array.isArray(notMatch)) return; 38 | for (let j = 0; j < notMatch.length; j++) { 39 | assert(!re.test(notMatch[j]), 'should not match ' + msg); 40 | count++; 41 | } 42 | } 43 | 44 | const verifyRange = (min, max, from, to) => { 45 | let isMatch = matcher(min, max); 46 | let minNum = Math.min(min, max); 47 | let maxNum = Math.max(min, max); 48 | let num = from - 1; 49 | 50 | while (++num < to) { 51 | let n = Number(num); 52 | if (inRange(minNum, maxNum, n)) { 53 | assert(isMatch(num), `should match "${num}"`); 54 | } else { 55 | assert(!isMatch(num), `should not match "${num}"`); 56 | } 57 | count++; 58 | } 59 | }; 60 | 61 | const verifyZeros = (min, max, from, to) => { 62 | let range = fill(from, to); 63 | let len = range.length; 64 | let idx = -1; 65 | 66 | let isMatch = matcher(min, max); 67 | let minNum = Math.min(min, max); 68 | let maxNum = Math.max(min, max); 69 | 70 | while (++idx < len) { 71 | let num = range[idx]; 72 | let n = Number(num); 73 | if (inRange(minNum, maxNum, n)) { 74 | assert(isMatch(num), `should match "${num}"`); 75 | } else { 76 | assert(!isMatch(num), `should not match "${num}"`); 77 | } 78 | count++; 79 | } 80 | }; 81 | 82 | describe('to-regex-range', () => { 83 | after(() => { 84 | console.log(); 85 | console.log(' ', (+(+count.toFixed(2))).toLocaleString(), 'assertions'); 86 | }); 87 | 88 | describe('range', () => { 89 | it('should throw an error when the first arg is invalid:', () => { 90 | assert.throws(() => toRange(), /expected/); 91 | }); 92 | 93 | it('should throw an error when the second arg is invalid:', () => { 94 | assert.throws(() => toRange(1, {}), /expected/); 95 | }); 96 | 97 | it('should match the given numbers', () => { 98 | let oneFifty = toRegex(toRange(1, 150)); 99 | assert(oneFifty.test('125')); 100 | assert(!oneFifty.test('0')); 101 | assert(oneFifty.test('1')); 102 | assert(oneFifty.test('126')); 103 | assert(oneFifty.test('150')); 104 | assert(!oneFifty.test('151')); 105 | 106 | let oneTwentyFive = toRegex(toRange(1, 125)); 107 | assert(oneTwentyFive.test('125')); 108 | assert(!oneTwentyFive.test('0')); 109 | assert(oneTwentyFive.test('1')); 110 | assert(!oneTwentyFive.test('126')); 111 | assert(!oneTwentyFive.test('150')); 112 | assert(!oneTwentyFive.test('151')); 113 | }); 114 | }); 115 | 116 | describe('minimum / maximum', () => { 117 | it('should reverse `min/max` when the min is larger than the max:', () => { 118 | assert.equal(toRange(55, 10), '(?:1[0-9]|[2-4][0-9]|5[0-5])'); 119 | }); 120 | }); 121 | 122 | describe('ranges', () => { 123 | it('should return the number when only one argument is passed:', () => { 124 | assert.equal(toRange(5), '5'); 125 | }); 126 | 127 | it('should return a single number when both numbers are equal', () => { 128 | assert.equal(toRange('1', '1'), '1'); 129 | assert.equal(toRange('65443', '65443'), '65443'); 130 | assert.equal(toRange('192', '192'), '192'); 131 | verifyRange(1, 1, 0, 100); 132 | verifyRange(65443, 65443, 65000, 66000); 133 | verifyRange(192, 192, 0, 1000); 134 | }); 135 | 136 | it('should not return a range when both numbers are the same:', () => { 137 | assert.equal(toRange(5, 5), '5'); 138 | }); 139 | 140 | it('should return regex character classes when both args are less than 10', () => { 141 | assert.equal(toRange(0, 9), '[0-9]'); 142 | assert.equal(toRange(1, 5), '[1-5]'); 143 | assert.equal(toRange(1, 7), '[1-7]'); 144 | assert.equal(toRange(2, 6), '[2-6]'); 145 | }); 146 | 147 | it('should support string numbers', () => { 148 | assert.equal(toRange('1', '5'), '[1-5]'); 149 | assert.equal(toRange('10', '50'), '(?:1[0-9]|[2-4][0-9]|50)'); 150 | }); 151 | 152 | it('should support padded ranges:', () => { 153 | assert.equal(toRange('001', '005'), '0{0,2}[1-5]'); 154 | assert.equal(toRange('01', '05'), '0?[1-5]'); 155 | assert.equal(toRange('001', '100'), '(?:0{0,2}[1-9]|0?[1-9][0-9]|100)'); 156 | assert.equal(toRange('0001', '1000'), '(?:0{0,3}[1-9]|0{0,2}[1-9][0-9]|0?[1-9][0-9]{2}|1000)'); 157 | }); 158 | 159 | it('should work when padding is imbalanced:', () => { 160 | assert.equal(toRange('001', '105'), '(?:0{0,2}[1-9]|0?[1-9][0-9]|10[0-5])'); 161 | assert.equal(toRange('01', '105'), '(?:0{0,2}[1-9]|0?[1-9][0-9]|10[0-5])'); 162 | assert.equal(toRange('010', '105'), '(?:0?1[0-9]|0?[2-9][0-9]|10[0-5])'); 163 | assert.equal(toRange('010', '1005'), '(?:0{0,2}1[0-9]|0{0,2}[2-9][0-9]|0?[1-9][0-9]{2}|100[0-5])'); 164 | assert.equal(toRange('0001', '1000'), toRange('001', '1000')); 165 | assert.equal(toRange('0001', '1000'), toRange('01', '1000')); 166 | }); 167 | 168 | it('should generate regex strings for negative patterns', () => { 169 | assert.equal(toRange(-1, 0), '(?:-1|0)'); 170 | assert.equal(toRange(-1, 1), '(?:-1|[01])'); 171 | assert.equal(toRange(-4, -2), '-[2-4]'); 172 | assert.equal(toRange(-3, 1), '(?:-[1-3]|[01])'); 173 | assert.equal(toRange(-2, 0), '(?:-[12]|0)'); 174 | assert.equal(toRange(-1, 3), '(?:-1|[0-3])'); 175 | matchRange(-1, -1, '-1', [-1], [-2, 0, 1]); 176 | matchRange(-1, -10, '(?:-[1-9]|-10)', [-1, -5, -10], [-11, 0]); 177 | matchRange(-1, 3, '(?:-1|[0-3])', [-1, 0, 1, 2, 3], [-2, 4]); 178 | }); 179 | 180 | it('should wrap patterns when options.capture is true', () => { 181 | assert.equal(toRange(-1, 0, { capture: true }), '(-1|0)'); 182 | assert.equal(toRange(-1, 1, { capture: true }), '(-1|[01])'); 183 | assert.equal(toRange(-4, -2, { capture: true }), '(-[2-4])'); 184 | assert.equal(toRange(-3, 1, { capture: true }), '(-[1-3]|[01])'); 185 | assert.equal(toRange(-2, 0, { capture: true }), '(-[12]|0)'); 186 | assert.equal(toRange(-1, 3, { capture: true }), '(-1|[0-3])'); 187 | }); 188 | 189 | it('should generate regex strings for positive patterns', () => { 190 | assert.equal(toRange(1, 1), '1'); 191 | assert.equal(toRange(0, 1), '(?:0|1)'); 192 | assert.equal(toRange(0, 2), '[0-2]'); 193 | assert.equal(toRange(65666, 65667), '(?:65666|65667)'); 194 | assert.equal(toRange(12, 3456), '(?:1[2-9]|[2-9][0-9]|[1-9][0-9]{2}|[12][0-9]{3}|3[0-3][0-9]{2}|34[0-4][0-9]|345[0-6])'); 195 | assert.equal(toRange(1, 3456), '(?:[1-9]|[1-9][0-9]{1,2}|[12][0-9]{3}|3[0-3][0-9]{2}|34[0-4][0-9]|345[0-6])'); 196 | assert.equal(toRange(1, 10), '(?:[1-9]|10)'); 197 | assert.equal(toRange(1, 19), '(?:[1-9]|1[0-9])'); 198 | assert.equal(toRange(1, 99), '(?:[1-9]|[1-9][0-9])'); 199 | assert.equal(toRange(1, 100), '(?:[1-9]|[1-9][0-9]|100)'); 200 | assert.equal(toRange(1, 1000), '(?:[1-9]|[1-9][0-9]{1,2}|1000)'); 201 | assert.equal(toRange(1, 10000), '(?:[1-9]|[1-9][0-9]{1,3}|10000)'); 202 | assert.equal(toRange(1, 100000), '(?:[1-9]|[1-9][0-9]{1,4}|100000)'); 203 | assert.equal(toRange(1, 9999999), '(?:[1-9]|[1-9][0-9]{1,6})'); 204 | assert.equal(toRange(99, 100000), '(?:99|[1-9][0-9]{2,4}|100000)'); 205 | 206 | matchRange(99, 100000, '(?:99|[1-9][0-9]{2,4}|100000)', [99, 999, 989, 100, 9999, 9899, 10009, 10999, 100000], [0, 9, 100001, 100009]); 207 | }); 208 | 209 | it('should optimize regexes', () => { 210 | assert.equal(toRange(-9, 9), '(?:-[1-9]|[0-9])'); 211 | assert.equal(toRange(-19, 19), '(?:-[1-9]|-?1[0-9]|[0-9])'); 212 | assert.equal(toRange(-29, 29), '(?:-[1-9]|-?[12][0-9]|[0-9])'); 213 | assert.equal(toRange(-99, 99), '(?:-[1-9]|-?[1-9][0-9]|[0-9])'); 214 | assert.equal(toRange(-999, 999), '(?:-[1-9]|-?[1-9][0-9]{1,2}|[0-9])'); 215 | assert.equal(toRange(-9999, 9999), '(?:-[1-9]|-?[1-9][0-9]{1,3}|[0-9])'); 216 | assert.equal(toRange(-99999, 99999), '(?:-[1-9]|-?[1-9][0-9]{1,4}|[0-9])'); 217 | }); 218 | }); 219 | 220 | describe('validate ranges', () => { 221 | it('should match all numbers in the given range', () => { 222 | let isMatch = matcher(1, 59); 223 | for (let i = 0; i < 100; i++) { 224 | if (i >= 1 && i <= 59) { 225 | assert(isMatch(i)); 226 | } else { 227 | assert(!isMatch(i)); 228 | } 229 | } 230 | }); 231 | 232 | it('should support negative ranges:', () => { 233 | verifyRange(-9, -1, -100, 100); 234 | verifyRange(-99, -1, -1000, 1000); 235 | verifyRange(-999, -1, -1000, 1000); 236 | verifyRange(-9999, -1, -10000, 10000); 237 | verifyRange(-99999, -1, -100999, 100999); 238 | }); 239 | 240 | it('should support negative-to-positive ranges:', () => { 241 | verifyRange(-9, 9, -100, 100); 242 | verifyRange(-99, 99, -1000, 1000); 243 | verifyRange(-999, 999, -1000, 1000); 244 | verifyRange(-9999, 9999, -10000, 10000); 245 | verifyRange(-99999, 99999, -100999, 100999); 246 | }); 247 | 248 | it('should support large numbers:', () => { 249 | verifyRange(100019999300000, 100020000300000, 1000199992999900, 100020000200000); 250 | }); 251 | 252 | it('should support large ranges:', () => { 253 | verifyRange(1, 100000, 1, 1000); 254 | verifyRange(1, 100000, 10000, 11000); 255 | verifyRange(1, 100000, 99000, 100000); 256 | verifyRange(1, 100000, 1000, 2000); 257 | verifyRange(1, 100000, 10000, 12000); 258 | verifyRange(1, 100000, 50000, 60000); 259 | verifyRange(1, 100000, 99999, 101000); 260 | verifyRange(10331, 20381, 0, 99999); 261 | }); 262 | 263 | it('should support repeated digits:', () => { 264 | verifyRange(111, 222, 0, 999); 265 | verifyRange(111, 333, 0, 999); 266 | verifyRange(111, 444, 0, 999); 267 | verifyRange(111, 555, 0, 999); 268 | verifyRange(111, 666, 0, 999); 269 | verifyRange(111, 777, 0, 999); 270 | verifyRange(111, 888, 0, 999); 271 | verifyRange(111, 999, 0, 999); 272 | verifyRange(0, 111, -99, 999); 273 | verifyRange(0, 222, -99, 999); 274 | verifyRange(0, 333, -99, 999); 275 | verifyRange(0, 444, -99, 999); 276 | verifyRange(0, 555, -99, 999); 277 | verifyRange(0, 666, -99, 999); 278 | verifyRange(0, 777, -99, 999); 279 | verifyRange(0, 888, -99, 999); 280 | verifyRange(0, 999, -99, 999); 281 | }); 282 | 283 | it('should support repeated zeros:', () => { 284 | verifyRange(10031, 20081, 0, 59999); 285 | verifyRange(10000, 20000, 0, 59999); 286 | }); 287 | 288 | it('should support zero one:', () => { 289 | verifyRange(10301, 20101, 0, 99999); 290 | verifyRange(101010, 101210, 101009, 101300); 291 | }); 292 | 293 | it('should support repeated ones:', () => { 294 | verifyRange(1, 11111, 0, 1000); 295 | verifyRange(1, 1111, 0, 1000); 296 | verifyRange(1, 111, 0, 1000); 297 | verifyRange(1, 11, 0, 1000); 298 | verifyRange(1, 1, 0, 1000); 299 | }); 300 | 301 | it('should support small diffs:', () => { 302 | verifyRange(102, 103, 0, 1000); 303 | verifyRange(102, 110, 0, 1000); 304 | verifyRange(102, 130, 0, 1000); 305 | }); 306 | 307 | it('should support random ranges:', () => { 308 | verifyRange(4173, 7981, 0, 99999); 309 | }); 310 | 311 | it('should support one digit numbers:', () => { 312 | verifyRange(3, 7, 0, 99); 313 | }); 314 | 315 | it('should support one digit at bounds:', () => { 316 | verifyRange(1, 9, 0, 1000); 317 | }); 318 | 319 | it('should support power of ten:', () => { 320 | verifyRange(1000, 8632, 0, 99999); 321 | }); 322 | 323 | it('should not match the negative of the same number', () => { 324 | verifyRange(1, 1000, -1000, 1000); 325 | verifyRange(1, 1000, '-1000', '1000'); 326 | }); 327 | 328 | it('should work with numbers of varying lengths:', () => { 329 | verifyRange(1030, 20101, 0, 99999); 330 | verifyRange(13, 8632, 0, 10000); 331 | }); 332 | 333 | it('should support small ranges:', () => { 334 | verifyRange(9, 11, 0, 100); 335 | verifyRange(19, 21, 0, 100); 336 | }); 337 | 338 | it('should support big ranges:', () => { 339 | verifyRange(90, 98009, 0, 98999); 340 | verifyRange(999, 10000, 1, 20000); 341 | }); 342 | 343 | it('should create valid regex ranges with zero-padding:', () => { 344 | verifyZeros('001', '100', '001', 100); 345 | verifyZeros('001', '100', '001', '100'); 346 | verifyZeros('0001', '1000', '01', 1000); 347 | verifyZeros('0001', '1000', '-01', 1000); 348 | verifyZeros('0001', '1000', '-099', '1000'); 349 | verifyZeros('0001', '1000', '-010', 1000); 350 | verifyZeros('0001', '1000', '-010', 1000); 351 | verifyZeros('0001', '1000', '0001', '1000'); 352 | verifyZeros('01', '1000', '-01', '1000'); 353 | verifyZeros('000000001', '1000', '-010', '1000'); 354 | verifyZeros('00000001', '1000', '-010', '1000'); 355 | verifyZeros('0000001', '1000', '-010', '1000'); 356 | verifyZeros('000001', '1000', '-010', '1000'); 357 | verifyZeros('00001', '1000', '-010', '1000'); 358 | verifyZeros('0001', '1000', '-010', '1000'); 359 | verifyZeros('001', '1000', '-010', '1000'); 360 | verifyZeros('01', '1000', '-010', '1000'); 361 | verifyZeros('0001', '1000', '-010', '1000'); 362 | }); 363 | 364 | it('should create valid regex ranges with negative padding:', () => { 365 | verifyZeros('-00001', '-1000', -1000, 1000); 366 | verifyZeros('-0001', '-1000', -1000, 1000); 367 | verifyZeros('-001', '-1000', -1000, 1000); 368 | verifyZeros('-01', '-1000', -1000, 1000); 369 | }); 370 | 371 | it('should create valid ranges with neg && pos zero-padding:', () => { 372 | verifyZeros('-01', '10', '-1', '01'); 373 | verifyZeros('-1000', '100', -1000, 1000); 374 | verifyZeros('-1000', '0100', '-010', '1000'); 375 | verifyZeros('-0100', '100', '-01', '100'); 376 | verifyZeros('-010', '100', '-01', '100'); 377 | verifyZeros('-01', '100', '-01', '100'); 378 | verifyZeros('-01000', '1000', '-010', '1000'); 379 | verifyZeros('-0100', '1000', '-010', '1000'); 380 | verifyZeros('-010', '1000', '-010', '1000'); 381 | verifyZeros('-01', '1000', '-010', '1000'); 382 | }); 383 | }); 384 | }); 385 | --------------------------------------------------------------------------------