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;
}
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}