123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- /*!
- * 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;
- }
|