/*! * fill-range * * Copyright (c) 2014-2015, Jon Schlinkert. * Licensed under the MIT License. */ 'use strict'; var isObject = require('isobject'); var isNumber = require('is-number'); var randomize = require('randomatic'); var repeatStr = require('repeat-string'); var repeat = require('repeat-element'); /** * Expose `fillRange` */ module.exports = fillRange; /** * Return a range of numbers or letters. * * @param {String} `a` Start of the range * @param {String} `b` End of the range * @param {String} `step` Increment or decrement to use. * @param {Function} `fn` Custom function to modify each element in the range. * @return {Array} */ function fillRange(a, b, step, options, fn) { if (a == null || b == null) { throw new Error('fill-range expects the first and second args to be strings.'); } if (typeof step === 'function') { fn = step; options = {}; step = null; } if (typeof options === 'function') { fn = options; options = {}; } if (isObject(step)) { options = step; step = ''; } var expand, regex = false, sep = ''; var opts = options || {}; if (typeof opts.silent === 'undefined') { opts.silent = true; } step = step || opts.step; // store a ref to unmodified arg var origA = a, origB = b; b = (b.toString() === '-0') ? 0 : b; if (opts.optimize || opts.makeRe) { step = step ? (step += '~') : step; expand = true; regex = true; sep = '~'; } // handle special step characters if (typeof step === 'string') { var match = stepRe().exec(step); if (match) { var i = match.index; var m = match[0]; // repeat string if (m === '+') { return repeat(a, b); // randomize a, `b` times } else if (m === '?') { return [randomize(a, b)]; // expand right, no regex reduction } else if (m === '>') { step = step.substr(0, i) + step.substr(i + 1); expand = true; // expand to an array, or if valid create a reduced // string for a regex logic `or` } else if (m === '|') { step = step.substr(0, i) + step.substr(i + 1); expand = true; regex = true; sep = m; // expand to an array, or if valid create a reduced // string for a regex range } else if (m === '~') { step = step.substr(0, i) + step.substr(i + 1); expand = true; regex = true; sep = m; } } else if (!isNumber(step)) { if (!opts.silent) { throw new TypeError('fill-range: invalid step.'); } return null; } } if (/[.&*()[\]^%$#@!]/.test(a) || /[.&*()[\]^%$#@!]/.test(b)) { if (!opts.silent) { throw new RangeError('fill-range: invalid range arguments.'); } return null; } // has neither a letter nor number, or has both letters and numbers // this needs to be after the step logic if (!noAlphaNum(a) || !noAlphaNum(b) || hasBoth(a) || hasBoth(b)) { if (!opts.silent) { throw new RangeError('fill-range: invalid range arguments.'); } return null; } // validate arguments var isNumA = isNumber(zeros(a)); var isNumB = isNumber(zeros(b)); if ((!isNumA && isNumB) || (isNumA && !isNumB)) { if (!opts.silent) { throw new TypeError('fill-range: first range argument is incompatible with second.'); } return null; } // by this point both are the same, so we // can use A to check going forward. var isNum = isNumA; var num = formatStep(step); // is the range alphabetical? or numeric? if (isNum) { // if numeric, coerce to an integer a = +a; b = +b; } else { // otherwise, get the charCode to expand alpha ranges a = a.charCodeAt(0); b = b.charCodeAt(0); } // is the pattern descending? var isDescending = a > b; // don't create a character class if the args are < 0 if (a < 0 || b < 0) { expand = false; regex = false; } // detect padding var padding = isPadded(origA, origB); var res, pad, arr = []; var ii = 0; // character classes, ranges and logical `or` if (regex) { if (shouldExpand(a, b, num, isNum, padding, opts)) { // make sure the correct separator is used if (sep === '|' || sep === '~') { sep = detectSeparator(a, b, num, isNum, isDescending); } return wrap([origA, origB], sep, opts); } } while (isDescending ? (a >= b) : (a <= b)) { if (padding && isNum) { pad = padding(a); } // custom function if (typeof fn === 'function') { res = fn(a, isNum, pad, ii++); // letters } else if (!isNum) { if (regex && isInvalidChar(a)) { res = null; } else { res = String.fromCharCode(a); } // numbers } else { res = formatPadding(a, pad); } // add result to the array, filtering any nulled values if (res !== null) arr.push(res); // increment or decrement if (isDescending) { a -= num; } else { a += num; } } // now that the array is expanded, we need to handle regex // character classes, ranges or logical `or` that wasn't // already handled before the loop if ((regex || expand) && !opts.noexpand) { // make sure the correct separator is used if (sep === '|' || sep === '~') { sep = detectSeparator(a, b, num, isNum, isDescending); } if (arr.length === 1 || a < 0 || b < 0) { return arr; } return wrap(arr, sep, opts); } return arr; } /** * Wrap the string with the correct regex * syntax. */ function wrap(arr, sep, opts) { if (sep === '~') { sep = '-'; } var str = arr.join(sep); var pre = opts && opts.regexPrefix; // regex logical `or` if (sep === '|') { str = pre ? pre + str : str; str = '(' + str + ')'; } // regex character class if (sep === '-') { str = (pre && pre === '^') ? pre + str : str; str = '[' + str + ']'; } return [str]; } /** * Check for invalid characters */ function isCharClass(a, b, step, isNum, isDescending) { if (isDescending) { return false; } if (isNum) { return a <= 9 && b <= 9; } if (a < b) { return step === 1; } return false; } /** * Detect the correct separator to use */ function shouldExpand(a, b, num, isNum, padding, opts) { if (isNum && (a > 9 || b > 9)) { return false; } return !padding && num === 1 && a < b; } /** * Detect the correct separator to use */ function detectSeparator(a, b, step, isNum, isDescending) { var isChar = isCharClass(a, b, step, isNum, isDescending); if (!isChar) { return '|'; } return '~'; } /** * Correctly format the step based on type */ function formatStep(step) { return Math.abs(step >> 0) || 1; } /** * Format padding, taking leading `-` into account */ function formatPadding(ch, pad) { var res = pad ? pad + ch : ch; if (pad && ch.toString().charAt(0) === '-') { res = '-' + pad + ch.toString().substr(1); } return res.toString(); } /** * Check for invalid characters */ function isInvalidChar(str) { var ch = toStr(str); return ch === '\\' || ch === '[' || ch === ']' || ch === '^' || ch === '(' || ch === ')' || ch === '`'; } /** * Convert to a string from a charCode */ function toStr(ch) { return String.fromCharCode(ch); } /** * Step regex */ function stepRe() { return /\?|>|\||\+|\~/g; } /** * Return true if `val` has either a letter * or a number */ function noAlphaNum(val) { return /[a-z0-9]/i.test(val); } /** * Return true if `val` has both a letter and * a number (invalid) */ function hasBoth(val) { return /[a-z][0-9]|[0-9][a-z]/i.test(val); } /** * Normalize zeros for checks */ function zeros(val) { if (/^-*0+$/.test(val.toString())) { return '0'; } return val; } /** * Return true if `val` has leading zeros, * or a similar valid pattern. */ function hasZeros(val) { return /[^.]\.|^-*0+[0-9]/.test(val); } /** * If the string is padded, returns a curried function with * the a cached padding string, or `false` if no padding. * * @param {*} `origA` String or number. * @return {String|Boolean} */ function isPadded(origA, origB) { if (hasZeros(origA) || hasZeros(origB)) { var alen = length(origA); var blen = length(origB); var len = alen >= blen ? alen : blen; return function (a) { return repeatStr('0', len - length(a)); }; } return false; } /** * Get the string length of `val` */ function length(val) { return val.toString().length; }