409 lines
8.4 KiB
JavaScript
409 lines
8.4 KiB
JavaScript
|
/*!
|
||
|
* fill-range <https://github.com/jonschlinkert/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;
|
||
|
}
|