index.js
  1  /*!
  2   * fill-range <https://github.com/jonschlinkert/fill-range>
  3   *
  4   * Copyright (c) 2014-2015, 2017, Jon Schlinkert.
  5   * Released under the MIT License.
  6   */
  7  
  8  'use strict';
  9  
 10  var util = require('util');
 11  var isNumber = require('is-number');
 12  var extend = require('extend-shallow');
 13  var repeat = require('repeat-string');
 14  var toRegex = require('to-regex-range');
 15  
 16  /**
 17   * Return a range of numbers or letters.
 18   *
 19   * @param  {String} `start` Start of the range
 20   * @param  {String} `stop` End of the range
 21   * @param  {String} `step` Increment or decrement to use.
 22   * @param  {Function} `fn` Custom function to modify each element in the range.
 23   * @return {Array}
 24   */
 25  
 26  function fillRange(start, stop, step, options) {
 27    if (typeof start === 'undefined') {
 28      return [];
 29    }
 30  
 31    if (typeof stop === 'undefined' || start === stop) {
 32      // special case, for handling negative zero
 33      var isString = typeof start === 'string';
 34      if (isNumber(start) && !toNumber(start)) {
 35        return [isString ? '0' : 0];
 36      }
 37      return [start];
 38    }
 39  
 40    if (typeof step !== 'number' && typeof step !== 'string') {
 41      options = step;
 42      step = undefined;
 43    }
 44  
 45    if (typeof options === 'function') {
 46      options = { transform: options };
 47    }
 48  
 49    var opts = extend({step: step}, options);
 50    if (opts.step && !isValidNumber(opts.step)) {
 51      if (opts.strictRanges === true) {
 52        throw new TypeError('expected options.step to be a number');
 53      }
 54      return [];
 55    }
 56  
 57    opts.isNumber = isValidNumber(start) && isValidNumber(stop);
 58    if (!opts.isNumber && !isValid(start, stop)) {
 59      if (opts.strictRanges === true) {
 60        throw new RangeError('invalid range arguments: ' + util.inspect([start, stop]));
 61      }
 62      return [];
 63    }
 64  
 65    opts.isPadded = isPadded(start) || isPadded(stop);
 66    opts.toString = opts.stringify
 67      || typeof opts.step === 'string'
 68      || typeof start === 'string'
 69      || typeof stop === 'string'
 70      || !opts.isNumber;
 71  
 72    if (opts.isPadded) {
 73      opts.maxLength = Math.max(String(start).length, String(stop).length);
 74    }
 75  
 76    // support legacy minimatch/fill-range options
 77    if (typeof opts.optimize === 'boolean') opts.toRegex = opts.optimize;
 78    if (typeof opts.makeRe === 'boolean') opts.toRegex = opts.makeRe;
 79    return expand(start, stop, opts);
 80  }
 81  
 82  function expand(start, stop, options) {
 83    var a = options.isNumber ? toNumber(start) : start.charCodeAt(0);
 84    var b = options.isNumber ? toNumber(stop) : stop.charCodeAt(0);
 85  
 86    var step = Math.abs(toNumber(options.step)) || 1;
 87    if (options.toRegex && step === 1) {
 88      return toRange(a, b, start, stop, options);
 89    }
 90  
 91    var zero = {greater: [], lesser: []};
 92    var asc = a < b;
 93    var arr = new Array(Math.round((asc ? b - a : a - b) / step));
 94    var idx = 0;
 95  
 96    while (asc ? a <= b : a >= b) {
 97      var val = options.isNumber ? a : String.fromCharCode(a);
 98      if (options.toRegex && (val >= 0 || !options.isNumber)) {
 99        zero.greater.push(val);
100      } else {
101        zero.lesser.push(Math.abs(val));
102      }
103  
104      if (options.isPadded) {
105        val = zeros(val, options);
106      }
107  
108      if (options.toString) {
109        val = String(val);
110      }
111  
112      if (typeof options.transform === 'function') {
113        arr[idx++] = options.transform(val, a, b, step, idx, arr, options);
114      } else {
115        arr[idx++] = val;
116      }
117  
118      if (asc) {
119        a += step;
120      } else {
121        a -= step;
122      }
123    }
124  
125    if (options.toRegex === true) {
126      return toSequence(arr, zero, options);
127    }
128    return arr;
129  }
130  
131  function toRange(a, b, start, stop, options) {
132    if (options.isPadded) {
133      return toRegex(start, stop, options);
134    }
135  
136    if (options.isNumber) {
137      return toRegex(Math.min(a, b), Math.max(a, b), options);
138    }
139  
140    var start = String.fromCharCode(Math.min(a, b));
141    var stop = String.fromCharCode(Math.max(a, b));
142    return '[' + start + '-' + stop + ']';
143  }
144  
145  function toSequence(arr, zeros, options) {
146    var greater = '', lesser = '';
147    if (zeros.greater.length) {
148      greater = zeros.greater.join('|');
149    }
150    if (zeros.lesser.length) {
151      lesser = '-(' + zeros.lesser.join('|') + ')';
152    }
153    var res = greater && lesser
154      ? greater + '|' + lesser
155      : greater || lesser;
156  
157    if (options.capture) {
158      return '(' + res + ')';
159    }
160    return res;
161  }
162  
163  function zeros(val, options) {
164    if (options.isPadded) {
165      var str = String(val);
166      var len = str.length;
167      var dash = '';
168      if (str.charAt(0) === '-') {
169        dash = '-';
170        str = str.slice(1);
171      }
172      var diff = options.maxLength - len;
173      var pad = repeat('0', diff);
174      val = (dash + pad + str);
175    }
176    if (options.stringify) {
177      return String(val);
178    }
179    return val;
180  }
181  
182  function toNumber(val) {
183    return Number(val) || 0;
184  }
185  
186  function isPadded(str) {
187    return /^-?0\d/.test(str);
188  }
189  
190  function isValid(min, max) {
191    return (isValidNumber(min) || isValidLetter(min))
192        && (isValidNumber(max) || isValidLetter(max));
193  }
194  
195  function isValidLetter(ch) {
196    return typeof ch === 'string' && ch.length === 1 && /^\w+$/.test(ch);
197  }
198  
199  function isValidNumber(n) {
200    return isNumber(n) && !/\./.test(n);
201  }
202  
203  /**
204   * Expose `fillRange`
205   * @type {Function}
206   */
207  
208  module.exports = fillRange;