├── .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 [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W8YFZ425KND68) [](https://www.npmjs.com/package/to-regex-range) [](https://npmjs.org/package/to-regex-range) [](https://npmjs.org/package/to-regex-range) [](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 |
--------------------------------------------------------------------------------