2 * fill-range <https://github.com/jonschlinkert/fill-range>
4 * Copyright (c) 2014-2015, Jon Schlinkert.
5 * Licensed under the MIT License.
10 var isObject = require('isobject');
11 var isNumber = require('is-number');
12 var randomize = require('randomatic');
13 var repeatStr = require('repeat-string');
14 var repeat = require('repeat-element');
20 module.exports = fillRange;
23 * Return a range of numbers or letters.
25 * @param {String} `a` Start of the range
26 * @param {String} `b` End of the range
27 * @param {String} `step` Increment or decrement to use.
28 * @param {Function} `fn` Custom function to modify each element in the range.
32 function fillRange(a, b, step, options, fn) {
33 if (a == null || b == null) {
34 throw new Error('fill-range expects the first and second args to be strings.');
37 if (typeof step === 'function') {
38 fn = step; options = {}; step = null;
41 if (typeof options === 'function') {
42 fn = options; options = {};
46 options = step; step = '';
49 var expand, regex = false, sep = '';
50 var opts = options || {};
52 if (typeof opts.silent === 'undefined') {
56 step = step || opts.step;
58 // store a ref to unmodified arg
59 var origA = a, origB = b;
61 b = (b.toString() === '-0') ? 0 : b;
63 if (opts.optimize || opts.makeRe) {
64 step = step ? (step += '~') : step;
70 // handle special step characters
71 if (typeof step === 'string') {
72 var match = stepRe().exec(step);
82 // randomize a, `b` times
83 } else if (m === '?') {
84 return [randomize(a, b)];
86 // expand right, no regex reduction
87 } else if (m === '>') {
88 step = step.substr(0, i) + step.substr(i + 1);
91 // expand to an array, or if valid create a reduced
92 // string for a regex logic `or`
93 } else if (m === '|') {
94 step = step.substr(0, i) + step.substr(i + 1);
99 // expand to an array, or if valid create a reduced
100 // string for a regex range
101 } else if (m === '~') {
102 step = step.substr(0, i) + step.substr(i + 1);
107 } else if (!isNumber(step)) {
109 throw new TypeError('fill-range: invalid step.');
115 if (/[.&*()[\]^%$#@!]/.test(a) || /[.&*()[\]^%$#@!]/.test(b)) {
117 throw new RangeError('fill-range: invalid range arguments.');
122 // has neither a letter nor number, or has both letters and numbers
123 // this needs to be after the step logic
124 if (!noAlphaNum(a) || !noAlphaNum(b) || hasBoth(a) || hasBoth(b)) {
126 throw new RangeError('fill-range: invalid range arguments.');
131 // validate arguments
132 var isNumA = isNumber(zeros(a));
133 var isNumB = isNumber(zeros(b));
135 if ((!isNumA && isNumB) || (isNumA && !isNumB)) {
137 throw new TypeError('fill-range: first range argument is incompatible with second.');
142 // by this point both are the same, so we
143 // can use A to check going forward.
145 var num = formatStep(step);
147 // is the range alphabetical? or numeric?
149 // if numeric, coerce to an integer
152 // otherwise, get the charCode to expand alpha ranges
157 // is the pattern descending?
158 var isDescending = a > b;
160 // don't create a character class if the args are < 0
161 if (a < 0 || b < 0) {
167 var padding = isPadded(origA, origB);
168 var res, pad, arr = [];
171 // character classes, ranges and logical `or`
173 if (shouldExpand(a, b, num, isNum, padding, opts)) {
174 // make sure the correct separator is used
175 if (sep === '|' || sep === '~') {
176 sep = detectSeparator(a, b, num, isNum, isDescending);
178 return wrap([origA, origB], sep, opts);
182 while (isDescending ? (a >= b) : (a <= b)) {
183 if (padding && isNum) {
188 if (typeof fn === 'function') {
189 res = fn(a, isNum, pad, ii++);
193 if (regex && isInvalidChar(a)) {
196 res = String.fromCharCode(a);
201 res = formatPadding(a, pad);
204 // add result to the array, filtering any nulled values
205 if (res !== null) arr.push(res);
207 // increment or decrement
215 // now that the array is expanded, we need to handle regex
216 // character classes, ranges or logical `or` that wasn't
217 // already handled before the loop
218 if ((regex || expand) && !opts.noexpand) {
219 // make sure the correct separator is used
220 if (sep === '|' || sep === '~') {
221 sep = detectSeparator(a, b, num, isNum, isDescending);
223 if (arr.length === 1 || a < 0 || b < 0) { return arr; }
224 return wrap(arr, sep, opts);
231 * Wrap the string with the correct regex
235 function wrap(arr, sep, opts) {
236 if (sep === '~') { sep = '-'; }
237 var str = arr.join(sep);
238 var pre = opts && opts.regexPrefix;
240 // regex logical `or`
242 str = pre ? pre + str : str;
243 str = '(' + str + ')';
246 // regex character class
248 str = (pre && pre === '^')
251 str = '[' + str + ']';
257 * Check for invalid characters
260 function isCharClass(a, b, step, isNum, isDescending) {
261 if (isDescending) { return false; }
262 if (isNum) { return a <= 9 && b <= 9; }
263 if (a < b) { return step === 1; }
268 * Detect the correct separator to use
271 function shouldExpand(a, b, num, isNum, padding, opts) {
272 if (isNum && (a > 9 || b > 9)) { return false; }
273 return !padding && num === 1 && a < b;
277 * Detect the correct separator to use
280 function detectSeparator(a, b, step, isNum, isDescending) {
281 var isChar = isCharClass(a, b, step, isNum, isDescending);
289 * Correctly format the step based on type
292 function formatStep(step) {
293 return Math.abs(step >> 0) || 1;
297 * Format padding, taking leading `-` into account
300 function formatPadding(ch, pad) {
301 var res = pad ? pad + ch : ch;
302 if (pad && ch.toString().charAt(0) === '-') {
303 res = '-' + pad + ch.toString().substr(1);
305 return res.toString();
309 * Check for invalid characters
312 function isInvalidChar(str) {
324 * Convert to a string from a charCode
328 return String.fromCharCode(ch);
337 return /\?|>|\||\+|\~/g;
341 * Return true if `val` has either a letter
345 function noAlphaNum(val) {
346 return /[a-z0-9]/i.test(val);
350 * Return true if `val` has both a letter and
354 function hasBoth(val) {
355 return /[a-z][0-9]|[0-9][a-z]/i.test(val);
359 * Normalize zeros for checks
362 function zeros(val) {
363 if (/^-*0+$/.test(val.toString())) {
370 * Return true if `val` has leading zeros,
371 * or a similar valid pattern.
374 function hasZeros(val) {
375 return /[^.]\.|^-*0+[0-9]/.test(val);
379 * If the string is padded, returns a curried function with
380 * the a cached padding string, or `false` if no padding.
382 * @param {*} `origA` String or number.
383 * @return {String|Boolean}
386 function isPadded(origA, origB) {
387 if (hasZeros(origA) || hasZeros(origB)) {
388 var alen = length(origA);
389 var blen = length(origB);
391 var len = alen >= blen
395 return function (a) {
396 return repeatStr('0', len - length(a));
403 * Get the string length of `val`
406 function length(val) {
407 return val.toString().length;