index.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /*!
  2. * fill-range <https://github.com/jonschlinkert/fill-range>
  3. *
  4. * Copyright (c) 2014-2015, Jon Schlinkert.
  5. * Licensed under the MIT License.
  6. */
  7. 'use strict';
  8. var isObject = require('isobject');
  9. var isNumber = require('is-number');
  10. var randomize = require('randomatic');
  11. var repeatStr = require('repeat-string');
  12. var repeat = require('repeat-element');
  13. /**
  14. * Expose `fillRange`
  15. */
  16. module.exports = fillRange;
  17. /**
  18. * Return a range of numbers or letters.
  19. *
  20. * @param {String} `a` Start of the range
  21. * @param {String} `b` End of the range
  22. * @param {String} `step` Increment or decrement to use.
  23. * @param {Function} `fn` Custom function to modify each element in the range.
  24. * @return {Array}
  25. */
  26. function fillRange(a, b, step, options, fn) {
  27. if (a == null || b == null) {
  28. throw new Error('fill-range expects the first and second args to be strings.');
  29. }
  30. if (typeof step === 'function') {
  31. fn = step; options = {}; step = null;
  32. }
  33. if (typeof options === 'function') {
  34. fn = options; options = {};
  35. }
  36. if (isObject(step)) {
  37. options = step; step = '';
  38. }
  39. var expand, regex = false, sep = '';
  40. var opts = options || {};
  41. if (typeof opts.silent === 'undefined') {
  42. opts.silent = true;
  43. }
  44. step = step || opts.step;
  45. // store a ref to unmodified arg
  46. var origA = a, origB = b;
  47. b = (b.toString() === '-0') ? 0 : b;
  48. if (opts.optimize || opts.makeRe) {
  49. step = step ? (step += '~') : step;
  50. expand = true;
  51. regex = true;
  52. sep = '~';
  53. }
  54. // handle special step characters
  55. if (typeof step === 'string') {
  56. var match = stepRe().exec(step);
  57. if (match) {
  58. var i = match.index;
  59. var m = match[0];
  60. // repeat string
  61. if (m === '+') {
  62. return repeat(a, b);
  63. // randomize a, `b` times
  64. } else if (m === '?') {
  65. return [randomize(a, b)];
  66. // expand right, no regex reduction
  67. } else if (m === '>') {
  68. step = step.substr(0, i) + step.substr(i + 1);
  69. expand = true;
  70. // expand to an array, or if valid create a reduced
  71. // string for a regex logic `or`
  72. } else if (m === '|') {
  73. step = step.substr(0, i) + step.substr(i + 1);
  74. expand = true;
  75. regex = true;
  76. sep = m;
  77. // expand to an array, or if valid create a reduced
  78. // string for a regex range
  79. } else if (m === '~') {
  80. step = step.substr(0, i) + step.substr(i + 1);
  81. expand = true;
  82. regex = true;
  83. sep = m;
  84. }
  85. } else if (!isNumber(step)) {
  86. if (!opts.silent) {
  87. throw new TypeError('fill-range: invalid step.');
  88. }
  89. return null;
  90. }
  91. }
  92. if (/[.&*()[\]^%$#@!]/.test(a) || /[.&*()[\]^%$#@!]/.test(b)) {
  93. if (!opts.silent) {
  94. throw new RangeError('fill-range: invalid range arguments.');
  95. }
  96. return null;
  97. }
  98. // has neither a letter nor number, or has both letters and numbers
  99. // this needs to be after the step logic
  100. if (!noAlphaNum(a) || !noAlphaNum(b) || hasBoth(a) || hasBoth(b)) {
  101. if (!opts.silent) {
  102. throw new RangeError('fill-range: invalid range arguments.');
  103. }
  104. return null;
  105. }
  106. // validate arguments
  107. var isNumA = isNumber(zeros(a));
  108. var isNumB = isNumber(zeros(b));
  109. if ((!isNumA && isNumB) || (isNumA && !isNumB)) {
  110. if (!opts.silent) {
  111. throw new TypeError('fill-range: first range argument is incompatible with second.');
  112. }
  113. return null;
  114. }
  115. // by this point both are the same, so we
  116. // can use A to check going forward.
  117. var isNum = isNumA;
  118. var num = formatStep(step);
  119. // is the range alphabetical? or numeric?
  120. if (isNum) {
  121. // if numeric, coerce to an integer
  122. a = +a; b = +b;
  123. } else {
  124. // otherwise, get the charCode to expand alpha ranges
  125. a = a.charCodeAt(0);
  126. b = b.charCodeAt(0);
  127. }
  128. // is the pattern descending?
  129. var isDescending = a > b;
  130. // don't create a character class if the args are < 0
  131. if (a < 0 || b < 0) {
  132. expand = false;
  133. regex = false;
  134. }
  135. // detect padding
  136. var padding = isPadded(origA, origB);
  137. var res, pad, arr = [];
  138. var ii = 0;
  139. // character classes, ranges and logical `or`
  140. if (regex) {
  141. if (shouldExpand(a, b, num, isNum, padding, opts)) {
  142. // make sure the correct separator is used
  143. if (sep === '|' || sep === '~') {
  144. sep = detectSeparator(a, b, num, isNum, isDescending);
  145. }
  146. return wrap([origA, origB], sep, opts);
  147. }
  148. }
  149. while (isDescending ? (a >= b) : (a <= b)) {
  150. if (padding && isNum) {
  151. pad = padding(a);
  152. }
  153. // custom function
  154. if (typeof fn === 'function') {
  155. res = fn(a, isNum, pad, ii++);
  156. // letters
  157. } else if (!isNum) {
  158. if (regex && isInvalidChar(a)) {
  159. res = null;
  160. } else {
  161. res = String.fromCharCode(a);
  162. }
  163. // numbers
  164. } else {
  165. res = formatPadding(a, pad);
  166. }
  167. // add result to the array, filtering any nulled values
  168. if (res !== null) arr.push(res);
  169. // increment or decrement
  170. if (isDescending) {
  171. a -= num;
  172. } else {
  173. a += num;
  174. }
  175. }
  176. // now that the array is expanded, we need to handle regex
  177. // character classes, ranges or logical `or` that wasn't
  178. // already handled before the loop
  179. if ((regex || expand) && !opts.noexpand) {
  180. // make sure the correct separator is used
  181. if (sep === '|' || sep === '~') {
  182. sep = detectSeparator(a, b, num, isNum, isDescending);
  183. }
  184. if (arr.length === 1 || a < 0 || b < 0) { return arr; }
  185. return wrap(arr, sep, opts);
  186. }
  187. return arr;
  188. }
  189. /**
  190. * Wrap the string with the correct regex
  191. * syntax.
  192. */
  193. function wrap(arr, sep, opts) {
  194. if (sep === '~') { sep = '-'; }
  195. var str = arr.join(sep);
  196. var pre = opts && opts.regexPrefix;
  197. // regex logical `or`
  198. if (sep === '|') {
  199. str = pre ? pre + str : str;
  200. str = '(' + str + ')';
  201. }
  202. // regex character class
  203. if (sep === '-') {
  204. str = (pre && pre === '^')
  205. ? pre + str
  206. : str;
  207. str = '[' + str + ']';
  208. }
  209. return [str];
  210. }
  211. /**
  212. * Check for invalid characters
  213. */
  214. function isCharClass(a, b, step, isNum, isDescending) {
  215. if (isDescending) { return false; }
  216. if (isNum) { return a <= 9 && b <= 9; }
  217. if (a < b) { return step === 1; }
  218. return false;
  219. }
  220. /**
  221. * Detect the correct separator to use
  222. */
  223. function shouldExpand(a, b, num, isNum, padding, opts) {
  224. if (isNum && (a > 9 || b > 9)) { return false; }
  225. return !padding && num === 1 && a < b;
  226. }
  227. /**
  228. * Detect the correct separator to use
  229. */
  230. function detectSeparator(a, b, step, isNum, isDescending) {
  231. var isChar = isCharClass(a, b, step, isNum, isDescending);
  232. if (!isChar) {
  233. return '|';
  234. }
  235. return '~';
  236. }
  237. /**
  238. * Correctly format the step based on type
  239. */
  240. function formatStep(step) {
  241. return Math.abs(step >> 0) || 1;
  242. }
  243. /**
  244. * Format padding, taking leading `-` into account
  245. */
  246. function formatPadding(ch, pad) {
  247. var res = pad ? pad + ch : ch;
  248. if (pad && ch.toString().charAt(0) === '-') {
  249. res = '-' + pad + ch.toString().substr(1);
  250. }
  251. return res.toString();
  252. }
  253. /**
  254. * Check for invalid characters
  255. */
  256. function isInvalidChar(str) {
  257. var ch = toStr(str);
  258. return ch === '\\'
  259. || ch === '['
  260. || ch === ']'
  261. || ch === '^'
  262. || ch === '('
  263. || ch === ')'
  264. || ch === '`';
  265. }
  266. /**
  267. * Convert to a string from a charCode
  268. */
  269. function toStr(ch) {
  270. return String.fromCharCode(ch);
  271. }
  272. /**
  273. * Step regex
  274. */
  275. function stepRe() {
  276. return /\?|>|\||\+|\~/g;
  277. }
  278. /**
  279. * Return true if `val` has either a letter
  280. * or a number
  281. */
  282. function noAlphaNum(val) {
  283. return /[a-z0-9]/i.test(val);
  284. }
  285. /**
  286. * Return true if `val` has both a letter and
  287. * a number (invalid)
  288. */
  289. function hasBoth(val) {
  290. return /[a-z][0-9]|[0-9][a-z]/i.test(val);
  291. }
  292. /**
  293. * Normalize zeros for checks
  294. */
  295. function zeros(val) {
  296. if (/^-*0+$/.test(val.toString())) {
  297. return '0';
  298. }
  299. return val;
  300. }
  301. /**
  302. * Return true if `val` has leading zeros,
  303. * or a similar valid pattern.
  304. */
  305. function hasZeros(val) {
  306. return /[^.]\.|^-*0+[0-9]/.test(val);
  307. }
  308. /**
  309. * If the string is padded, returns a curried function with
  310. * the a cached padding string, or `false` if no padding.
  311. *
  312. * @param {*} `origA` String or number.
  313. * @return {String|Boolean}
  314. */
  315. function isPadded(origA, origB) {
  316. if (hasZeros(origA) || hasZeros(origB)) {
  317. var alen = length(origA);
  318. var blen = length(origB);
  319. var len = alen >= blen
  320. ? alen
  321. : blen;
  322. return function (a) {
  323. return repeatStr('0', len - length(a));
  324. };
  325. }
  326. return false;
  327. }
  328. /**
  329. * Get the string length of `val`
  330. */
  331. function length(val) {
  332. return val.toString().length;
  333. }