index.js
1 /*! 2 * to-regex-range <https://github.com/jonschlinkert/to-regex-range> 3 * 4 * Copyright (c) 2015, 2017, Jon Schlinkert. 5 * Released under the MIT License. 6 */ 7 8 'use strict'; 9 10 var repeat = require('repeat-string'); 11 var isNumber = require('is-number'); 12 var cache = {}; 13 14 function toRegexRange(min, max, options) { 15 if (isNumber(min) === false) { 16 throw new RangeError('toRegexRange: first argument is invalid.'); 17 } 18 19 if (typeof max === 'undefined' || min === max) { 20 return String(min); 21 } 22 23 if (isNumber(max) === false) { 24 throw new RangeError('toRegexRange: second argument is invalid.'); 25 } 26 27 options = options || {}; 28 var relax = String(options.relaxZeros); 29 var shorthand = String(options.shorthand); 30 var capture = String(options.capture); 31 var key = min + ':' + max + '=' + relax + shorthand + capture; 32 if (cache.hasOwnProperty(key)) { 33 return cache[key].result; 34 } 35 36 var a = Math.min(min, max); 37 var b = Math.max(min, max); 38 39 if (Math.abs(a - b) === 1) { 40 var result = min + '|' + max; 41 if (options.capture) { 42 return '(' + result + ')'; 43 } 44 return result; 45 } 46 47 var isPadded = padding(min) || padding(max); 48 var positives = []; 49 var negatives = []; 50 51 var tok = {min: min, max: max, a: a, b: b}; 52 if (isPadded) { 53 tok.isPadded = isPadded; 54 tok.maxLen = String(tok.max).length; 55 } 56 57 if (a < 0) { 58 var newMin = b < 0 ? Math.abs(b) : 1; 59 var newMax = Math.abs(a); 60 negatives = splitToPatterns(newMin, newMax, tok, options); 61 a = tok.a = 0; 62 } 63 64 if (b >= 0) { 65 positives = splitToPatterns(a, b, tok, options); 66 } 67 68 tok.negatives = negatives; 69 tok.positives = positives; 70 tok.result = siftPatterns(negatives, positives, options); 71 72 if (options.capture && (positives.length + negatives.length) > 1) { 73 tok.result = '(' + tok.result + ')'; 74 } 75 76 cache[key] = tok; 77 return tok.result; 78 } 79 80 function siftPatterns(neg, pos, options) { 81 var onlyNegative = filterPatterns(neg, pos, '-', false, options) || []; 82 var onlyPositive = filterPatterns(pos, neg, '', false, options) || []; 83 var intersected = filterPatterns(neg, pos, '-?', true, options) || []; 84 var subpatterns = onlyNegative.concat(intersected).concat(onlyPositive); 85 return subpatterns.join('|'); 86 } 87 88 function splitToRanges(min, max) { 89 min = Number(min); 90 max = Number(max); 91 92 var nines = 1; 93 var stops = [max]; 94 var stop = +countNines(min, nines); 95 96 while (min <= stop && stop <= max) { 97 stops = push(stops, stop); 98 nines += 1; 99 stop = +countNines(min, nines); 100 } 101 102 var zeros = 1; 103 stop = countZeros(max + 1, zeros) - 1; 104 105 while (min < stop && stop <= max) { 106 stops = push(stops, stop); 107 zeros += 1; 108 stop = countZeros(max + 1, zeros) - 1; 109 } 110 111 stops.sort(compare); 112 return stops; 113 } 114 115 /** 116 * Convert a range to a regex pattern 117 * @param {Number} `start` 118 * @param {Number} `stop` 119 * @return {String} 120 */ 121 122 function rangeToPattern(start, stop, options) { 123 if (start === stop) { 124 return {pattern: String(start), digits: []}; 125 } 126 127 var zipped = zip(String(start), String(stop)); 128 var len = zipped.length, i = -1; 129 130 var pattern = ''; 131 var digits = 0; 132 133 while (++i < len) { 134 var numbers = zipped[i]; 135 var startDigit = numbers[0]; 136 var stopDigit = numbers[1]; 137 138 if (startDigit === stopDigit) { 139 pattern += startDigit; 140 141 } else if (startDigit !== '0' || stopDigit !== '9') { 142 pattern += toCharacterClass(startDigit, stopDigit); 143 144 } else { 145 digits += 1; 146 } 147 } 148 149 if (digits) { 150 pattern += options.shorthand ? '\\d' : '[0-9]'; 151 } 152 153 return { pattern: pattern, digits: [digits] }; 154 } 155 156 function splitToPatterns(min, max, tok, options) { 157 var ranges = splitToRanges(min, max); 158 var len = ranges.length; 159 var idx = -1; 160 161 var tokens = []; 162 var start = min; 163 var prev; 164 165 while (++idx < len) { 166 var range = ranges[idx]; 167 var obj = rangeToPattern(start, range, options); 168 var zeros = ''; 169 170 if (!tok.isPadded && prev && prev.pattern === obj.pattern) { 171 if (prev.digits.length > 1) { 172 prev.digits.pop(); 173 } 174 prev.digits.push(obj.digits[0]); 175 prev.string = prev.pattern + toQuantifier(prev.digits); 176 start = range + 1; 177 continue; 178 } 179 180 if (tok.isPadded) { 181 zeros = padZeros(range, tok); 182 } 183 184 obj.string = zeros + obj.pattern + toQuantifier(obj.digits); 185 tokens.push(obj); 186 start = range + 1; 187 prev = obj; 188 } 189 190 return tokens; 191 } 192 193 function filterPatterns(arr, comparison, prefix, intersection, options) { 194 var res = []; 195 196 for (var i = 0; i < arr.length; i++) { 197 var tok = arr[i]; 198 var ele = tok.string; 199 200 if (options.relaxZeros !== false) { 201 if (prefix === '-' && ele.charAt(0) === '0') { 202 if (ele.charAt(1) === '{') { 203 ele = '0*' + ele.replace(/^0\{\d+\}/, ''); 204 } else { 205 ele = '0*' + ele.slice(1); 206 } 207 } 208 } 209 210 if (!intersection && !contains(comparison, 'string', ele)) { 211 res.push(prefix + ele); 212 } 213 214 if (intersection && contains(comparison, 'string', ele)) { 215 res.push(prefix + ele); 216 } 217 } 218 return res; 219 } 220 221 /** 222 * Zip strings (`for in` can be used on string characters) 223 */ 224 225 function zip(a, b) { 226 var arr = []; 227 for (var ch in a) arr.push([a[ch], b[ch]]); 228 return arr; 229 } 230 231 function compare(a, b) { 232 return a > b ? 1 : b > a ? -1 : 0; 233 } 234 235 function push(arr, ele) { 236 if (arr.indexOf(ele) === -1) arr.push(ele); 237 return arr; 238 } 239 240 function contains(arr, key, val) { 241 for (var i = 0; i < arr.length; i++) { 242 if (arr[i][key] === val) { 243 return true; 244 } 245 } 246 return false; 247 } 248 249 function countNines(min, len) { 250 return String(min).slice(0, -len) + repeat('9', len); 251 } 252 253 function countZeros(integer, zeros) { 254 return integer - (integer % Math.pow(10, zeros)); 255 } 256 257 function toQuantifier(digits) { 258 var start = digits[0]; 259 var stop = digits[1] ? (',' + digits[1]) : ''; 260 if (!stop && (!start || start === 1)) { 261 return ''; 262 } 263 return '{' + start + stop + '}'; 264 } 265 266 function toCharacterClass(a, b) { 267 return '[' + a + ((b - a === 1) ? '' : '-') + b + ']'; 268 } 269 270 function padding(str) { 271 return /^-?(0+)\d/.exec(str); 272 } 273 274 function padZeros(val, tok) { 275 if (tok.isPadded) { 276 var diff = Math.abs(tok.maxLen - String(val).length); 277 switch (diff) { 278 case 0: 279 return ''; 280 case 1: 281 return '0'; 282 default: { 283 return '0{' + diff + '}'; 284 } 285 } 286 } 287 return val; 288 } 289 290 /** 291 * Expose `toRegexRange` 292 */ 293 294 module.exports = toRegexRange;