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;