text: 'use strict';
  
  // Load Date class extensions
  var CronDate = require('./date');
  
  // Get Number.isNaN or the polyfill
  var safeIsNaN = require('is-nan');
  
  var stringifyField = require('./field_stringify');
  
  /**
   * Cron iteration loop safety limit
   */
  var LOOP_LIMIT = 10000;
  
  /**
   * Construct a new expression parser
   *
   * Options:
   *   currentDate: iterator start date
   *   endDate: iterator end date
   *
   * @constructor
   * @private
   * @param {Object} fields  Expression fields parsed values
   * @param {Object} options Parser options
   */
  function CronExpression (fields, options) {
    this._options = options;
    this._utc = options.utc || false;
    this._tz = this._utc ? 'UTC' : options.tz;
    this._currentDate = new CronDate(options.currentDate, this._tz);
    this._startDate = options.startDate ? new CronDate(options.startDate, this._tz) : null;
    this._endDate = options.endDate ? new CronDate(options.endDate, this._tz) : null;
    this._isIterator = options.iterator || false;
    this._hasIterated = false;
    this._nthDayOfWeek = options.nthDayOfWeek || 0;
    this.fields = CronExpression._freezeFields(fields);
  }
  
  /**
   * Field mappings
   * @type {Array}
   */
  CronExpression.map = [ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek' ];
  
  /**
   * Prefined intervals
   * @type {Object}
   */
  CronExpression.predefined = {
    '@yearly': '0 0 1 1 *',
    '@monthly': '0 0 1 * *',
    '@weekly': '0 0 * * 0',
    '@daily': '0 0 * * *',
    '@hourly': '0 * * * *'
  };
  
  /**
   * Fields constraints
   * @type {Array}
   */
  CronExpression.constraints = [
    { min: 0, max: 59, chars: [] }, // Second
    { min: 0, max: 59, chars: [] }, // Minute
    { min: 0, max: 23, chars: [] }, // Hour
    { min: 1, max: 31, chars: ['L'] }, // Day of month
    { min: 1, max: 12, chars: [] }, // Month
    { min: 0, max: 7, chars: [] }, // Day of week
  ];
  
  /**
   * Days in month
   * @type {number[]}
   */
  CronExpression.daysInMonth = [
    31,
    29,
    31,
    30,
    31,
    30,
    31,
    31,
    30,
    31,
    30,
    31
  ];
  
  /**
   * Field aliases
   * @type {Object}
   */
  CronExpression.aliases = {
    month: {
      jan: 1,
      feb: 2,
      mar: 3,
      apr: 4,
      may: 5,
      jun: 6,
      jul: 7,
      aug: 8,
      sep: 9,
      oct: 10,
      nov: 11,
      dec: 12
    },
  
    dayOfWeek: {
      sun: 0,
      mon: 1,
      tue: 2,
      wed: 3,
      thu: 4,
      fri: 5,
      sat: 6
    }
  };
  
  /**
   * Field defaults
   * @type {Array}
   */
  CronExpression.parseDefaults = [ '0', '*', '*', '*', '*', '*' ];
  
  CronExpression.standardValidCharacters = /^[\d|/|*|\-|,]+$/;
  CronExpression.dayOfWeekValidCharacters = /^[\d|/|*|\-|,|\?]+$/;
  CronExpression.dayOfMonthValidCharacters = /^[\d|L|/|*|\-|,|\?]+$/;
  CronExpression.validCharacters = {
    second: CronExpression.standardValidCharacters,
    minute: CronExpression.standardValidCharacters,
    hour: CronExpression.standardValidCharacters,
    dayOfMonth: CronExpression.dayOfMonthValidCharacters,
    month: CronExpression.standardValidCharacters,
    dayOfWeek: CronExpression.dayOfWeekValidCharacters,
  };
  
  /**
   * Parse input interval
   *
   * @param {String} field Field symbolic name
   * @param {String} value Field value
   * @param {Array} constraints Range upper and lower constraints
   * @return {Array} Sequence of sorted values
   * @private
   */
  CronExpression._parseField = function _parseField (field, value, constraints) {
    // Replace aliases
    switch (field) {
      case 'month':
      case 'dayOfWeek':
        var aliases = CronExpression.aliases[field];
  
        value = value.replace(/[a-z]{1,3}/gi, function(match) {
          match = match.toLowerCase();
  
          if (typeof aliases[match] !== 'undefined') {
            return aliases[match];
          } else {
            throw new Error('Validation error, cannot resolve alias "' + match + '"');
          }
        });
        break;
    }
  
    // Check for valid characters.
    if (!(CronExpression.validCharacters[field].test(value))) {
      throw new Error('Invalid characters, got value: ' + value);
    }
  
    // Replace '*' and '?'
    if (value.indexOf('*') !== -1) {
      value = value.replace(/\*/g, constraints.min + '-' + constraints.max);
    } else if (value.indexOf('?') !== -1) {
      value = value.replace(/\?/g, constraints.min + '-' + constraints.max);
    }
  
    //
    // Inline parsing functions
    //
    // Parser path:
    //  - parseSequence
    //    - parseRepeat
    //      - parseRange
  
    /**
     * Parse sequence
     *
     * @param {String} val
     * @return {Array}
     * @private
     */
    function parseSequence (val) {
      var stack = [];
  
      function handleResult (result) {
        if (result instanceof Array) { // Make sequence linear
          for (var i = 0, c = result.length; i < c; i++) {
            var value = result[i];
  
            if (typeof value === 'string' && constraints.chars.indexOf(value) > -1) {
              stack.push(value);
              continue;
            }
            // Check constraints
            if (typeof value !== 'number' || safeIsNaN(value) || value < constraints.min || value > constraints.max) {
              throw new Error(
                  'Constraint error, got value ' + value + ' expected range ' +
                  constraints.min + '-' + constraints.max
              );
            }
  
            stack.push(value);
          }
        } else { // Scalar value
  
          if (typeof result === 'string' && constraints.chars.indexOf(result) > -1) {
            stack.push(result);
            return;
          }
  
          var numResult = +result;
  
          // Check constraints
          if (safeIsNaN(numResult) || numResult < constraints.min || numResult > constraints.max) {
            throw new Error(
              'Constraint error, got value ' + result + ' expected range ' +
              constraints.min + '-' + constraints.max
            );
          }
  
          if (field === 'dayOfWeek') {
            numResult = numResult % 7;
          }
  
          stack.push(numResult);
        }
      }
  
      var atoms = val.split(',');
      if (!atoms.every(function (atom) {
        return atom.length > 0;
      })) {
        throw new Error('Invalid list value format');
      }
  
      if (atoms.length > 1) {
        for (var i = 0, c = atoms.length; i < c; i++) {
          handleResult(parseRepeat(atoms[i]));
        }
      } else {
        handleResult(parseRepeat(val));
      }
  
      stack.sort(CronExpression._sortCompareFn);
  
      return stack;
    }
  
    /**
     * Parse repetition interval
     *
     * @param {String} val
     * @return {Array}
     */
    function parseRepeat (val) {
      var repeatInterval = 1;
      var atoms = val.split('/');
  
      if (atoms.length > 1) {
        if (atoms[0] == +atoms[0]) {
          atoms = [atoms[0] + '-' + constraints.max, atoms[1]];
        }
        return parseRange(atoms[0], atoms[atoms.length - 1]);
      }
  
      return parseRange(val, repeatInterval);
    }
  
    /**
     * Parse range
     *
     * @param {String} val
     * @param {Number} repeatInterval Repetition interval
     * @return {Array}
     * @private
     */
    function parseRange (val, repeatInterval) {
      var stack = [];
      var atoms = val.split('-');
  
      if (atoms.length > 1 ) {
        // Invalid range, return value
        if (atoms.length < 2) {
          return +val;
        }
  
        if (!atoms[0].length) {
          if (!atoms[1].length) {
            throw new Error('Invalid range: ' + val);
          }
  
          return +val;
        }
  
        // Validate range
        var min = +atoms[0];
        var max = +atoms[1];
  
        if (safeIsNaN(min) || safeIsNaN(max) ||
            min < constraints.min || max > constraints.max) {
          throw new Error(
            'Constraint error, got range ' +
            min + '-' + max +
            ' expected range ' +
            constraints.min + '-' + constraints.max
          );
        } else if (min >= max) {
          throw new Error('Invalid range: ' + val);
        }
  
        // Create range
        var repeatIndex = +repeatInterval;
  
        if (safeIsNaN(repeatIndex) || repeatIndex <= 0) {
          throw new Error('Constraint error, cannot repeat at every ' + repeatIndex + ' time.');
        }
  
        for (var index = min, count = max; index <= count; index++) {
          if (repeatIndex > 0 && (repeatIndex % repeatInterval) === 0) {
            repeatIndex = 1;
            stack.push(index);
          } else {
            repeatIndex++;
          }
        }
  
        return stack;
      }
  
      return safeIsNaN(+val) ? val : +val;
    }
  
    return parseSequence(value);
  };
  
  CronExpression._sortCompareFn = function(a, b) {
    var aIsNumber = typeof a === 'number';
    var bIsNumber = typeof b === 'number';
  
    if (aIsNumber && bIsNumber) {
      return a - b;
    }
  
    if (!aIsNumber && bIsNumber) {
      return 1;
    }
  
    if (aIsNumber && !bIsNumber) {
      return -1;
    }
  
    return a.localeCompare(b);
  };
  
  CronExpression._handleMaxDaysInMonth = function(mappedFields) {
    // Filter out any day of month value that is larger than given month expects
    if (mappedFields.month.length === 1) {
      var daysInMonth = CronExpression.daysInMonth[mappedFields.month[0] - 1];
  
      if (mappedFields.dayOfMonth[0] > daysInMonth) {
        throw new Error('Invalid explicit day of month definition');
      }
  
      return mappedFields.dayOfMonth
        .filter(function(dayOfMonth) {
          return dayOfMonth === 'L' ? true : dayOfMonth <= daysInMonth;
        })
        .sort(CronExpression._sortCompareFn);
    }
  };
  
  CronExpression._freezeFields = function(fields) {
    for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
      var field = CronExpression.map[i]; // Field name
      var value = fields[field];
      fields[field] = Object.freeze(value);
    }
    return Object.freeze(fields);
  };
  
  CronExpression.prototype._applyTimezoneShift = function(currentDate, dateMathVerb, method) {
    if ((method === 'Month') || (method === 'Day')) {
      var prevTime = currentDate.getTime();
      currentDate[dateMathVerb + method]();
      var currTime = currentDate.getTime();
      if (prevTime === currTime) {
        // Jumped into a not existent date due to a DST transition
        if ((currentDate.getMinutes() === 0) &&
            (currentDate.getSeconds() === 0)) {
          currentDate.addHour();
        } else if ((currentDate.getMinutes() === 59) &&
                   (currentDate.getSeconds() === 59)) {
          currentDate.subtractHour();
        }
      }
    } else {
      var previousHour = currentDate.getHours();
      currentDate[dateMathVerb + method]();
      var currentHour = currentDate.getHours();
      var diff = currentHour - previousHour;
      if (diff === 2) {
          // Starting DST
          if (this.fields.hour.length !== 24) {
            // Hour is specified
            this._dstStart = currentHour;
          }
        } else if ((diff === 0) &&
                   (currentDate.getMinutes() === 0) &&
                   (currentDate.getSeconds() === 0)) {
          // Ending DST
          if (this.fields.hour.length !== 24) {
            // Hour is specified
            this._dstEnd = currentHour;
          }
        }
    }
  };
  
  
  /**
   * Find next or previous matching schedule date
   *
   * @return {CronDate}
   * @private
   */
  CronExpression.prototype._findSchedule = function _findSchedule (reverse) {
  
    /**
     * Match field value
     *
     * @param {String} value
     * @param {Array} sequence
     * @return {Boolean}
     * @private
     */
    function matchSchedule (value, sequence) {
      for (var i = 0, c = sequence.length; i < c; i++) {
        if (sequence[i] >= value) {
          return sequence[i] === value;
        }
      }
  
      return sequence[0] === value;
    }
  
    /**
     * Helps determine if the provided date is the correct nth occurence of the
     * desired day of week.
     *
     * @param {CronDate} date
     * @param {Number} nthDayOfWeek
     * @return {Boolean}
     * @private
     */
    function isNthDayMatch(date, nthDayOfWeek) {
      if (nthDayOfWeek < 6) {
        if (
          date.getDate() < 8 &&
          nthDayOfWeek === 1 // First occurence has to happen in first 7 days of the month
        ) {
          return true;
        }
  
        var offset = date.getDate() % 7 ? 1 : 0; // Math is off by 1 when dayOfWeek isn't divisible by 7
        var adjustedDate = date.getDate() - (date.getDate() % 7); // find the first occurance
        var occurrence = Math.floor(adjustedDate / 7) + offset;
  
        return occurrence === nthDayOfWeek;
      }
  
      return false;
    }
  
    /**
     * Helper function that checks if 'L' is in the array
     *
     * @param {Array} dayOfMonth
     */
    function isLInDayOfMonth(dayOfMonth) {
      return dayOfMonth.length > 0 && dayOfMonth.indexOf('L') >= 0;
    }
  
    // Whether to use backwards directionality when searching
    reverse = reverse || false;
    var dateMathVerb = reverse ? 'subtract' : 'add';
  
    var currentDate = new CronDate(this._currentDate, this._tz);
    var startDate = this._startDate;
    var endDate = this._endDate;
  
    // Find matching schedule
    var startTimestamp = currentDate.getTime();
    var stepCount = 0;
  
    while (stepCount < LOOP_LIMIT) {
      stepCount++;
  
      // Validate timespan
      if (reverse) {
        if (startDate && (currentDate.getTime() - startDate.getTime() < 0)) {
          throw new Error('Out of the timespan range');
        }
      } else {
        if (endDate && (endDate.getTime() - currentDate.getTime()) < 0) {
          throw new Error('Out of the timespan range');
        }
      }
  
      // Day of month and week matching:
      //
      // "The day of a command's execution can be specified by two fields --
      // day of month, and day of week.  If  both	 fields	 are  restricted  (ie,
      // aren't  *),  the command will be run when either field matches the cur-
      // rent time.  For example, "30 4 1,15 * 5" would cause a command to be
      // run at 4:30 am on the  1st and 15th of each month, plus every Friday."
      //
      // http://unixhelp.ed.ac.uk/CGI/man-cgi?crontab+5
      //
  
      var dayOfMonthMatch = matchSchedule(currentDate.getDate(), this.fields.dayOfMonth);
      if (isLInDayOfMonth(this.fields.dayOfMonth)) {
        dayOfMonthMatch = dayOfMonthMatch || currentDate.isLastDayOfMonth();
      }
      var dayOfWeekMatch = matchSchedule(currentDate.getDay(), this.fields.dayOfWeek);
      var isDayOfMonthWildcardMatch = this.fields.dayOfMonth.length >= CronExpression.daysInMonth[currentDate.getMonth()];
      var isDayOfWeekWildcardMatch = this.fields.dayOfWeek.length === CronExpression.constraints[5].max - CronExpression.constraints[5].min + 1;
      var currentHour = currentDate.getHours();
  
      // Add or subtract day if select day not match with month (according to calendar)
      if (!dayOfMonthMatch && !dayOfWeekMatch) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
        continue;
      }
  
      // Add or subtract day if not day of month is set (and no match) and day of week is wildcard
      if (!isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch && !dayOfMonthMatch) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
        continue;
      }
  
      // Add or subtract day if not day of week is set (and no match) and day of month is wildcard
      if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && !dayOfWeekMatch) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
        continue;
      }
  
      // Add or subtract day if day of week & nthDayOfWeek are set (and no match)
      if (
        this._nthDayOfWeek > 0 &&
        !isNthDayMatch(currentDate, this._nthDayOfWeek)
      ) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
        continue;
      }
  
      // Match month
      if (!matchSchedule(currentDate.getMonth() + 1, this.fields.month)) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Month');
        continue;
      }
  
      // Match hour
      if (!matchSchedule(currentHour, this.fields.hour)) {
        if (this._dstStart !== currentHour) {
          this._dstStart = null;
          this._applyTimezoneShift(currentDate, dateMathVerb, 'Hour');
          continue;
        } else if (!matchSchedule(currentHour - 1, this.fields.hour)) {
          currentDate[dateMathVerb + 'Hour']();
          continue;
        }
      } else if (this._dstEnd === currentHour) {
        if (!reverse) {
          this._dstEnd = null;
          this._applyTimezoneShift(currentDate, 'add', 'Hour');
          continue;
        }
      }
  
      // Match minute
      if (!matchSchedule(currentDate.getMinutes(), this.fields.minute)) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Minute');
        continue;
      }
  
      // Match second
      if (!matchSchedule(currentDate.getSeconds(), this.fields.second)) {
        this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
        continue;
      }
  
      // Increase a second in case in the first iteration the currentDate was not
      // modified
      if (startTimestamp === currentDate.getTime()) {
        if ((dateMathVerb === 'add') || (currentDate.getMilliseconds() === 0)) {
          this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
        } else {
          currentDate.setMilliseconds(0);
        }
  
        continue;
      }
  
      break;
    }
  
    if (stepCount >= LOOP_LIMIT) {
      throw new Error('Invalid expression, loop limit exceeded');
    }
  
    this._currentDate = new CronDate(currentDate, this._tz);
    this._hasIterated = true;
  
    return currentDate;
  };
  
  /**
   * Find next suitable date
   *
   * @public
   * @return {CronDate|Object}
   */
  CronExpression.prototype.next = function next () {
    var schedule = this._findSchedule();
  
    // Try to return ES6 compatible iterator
    if (this._isIterator) {
      return {
        value: schedule,
        done: !this.hasNext()
      };
    }
  
    return schedule;
  };
  
  /**
   * Find previous suitable date
   *
   * @public
   * @return {CronDate|Object}
   */
  CronExpression.prototype.prev = function prev () {
    var schedule = this._findSchedule(true);
  
    // Try to return ES6 compatible iterator
    if (this._isIterator) {
      return {
        value: schedule,
        done: !this.hasPrev()
      };
    }
  
    return schedule;
  };
  
  /**
   * Check if next suitable date exists
   *
   * @public
   * @return {Boolean}
   */
  CronExpression.prototype.hasNext = function() {
    var current = this._currentDate;
    var hasIterated = this._hasIterated;
  
    try {
      this._findSchedule();
      return true;
    } catch (err) {
      return false;
    } finally {
      this._currentDate = current;
      this._hasIterated = hasIterated;
    }
  };
  
  /**
   * Check if previous suitable date exists
   *
   * @public
   * @return {Boolean}
   */
  CronExpression.prototype.hasPrev = function() {
    var current = this._currentDate;
    var hasIterated = this._hasIterated;
  
    try {
      this._findSchedule(true);
      return true;
    } catch (err) {
      return false;
    } finally {
      this._currentDate = current;
      this._hasIterated = hasIterated;
    }
  };
  
  /**
   * Iterate over expression iterator
   *
   * @public
   * @param {Number} steps Numbers of steps to iterate
   * @param {Function} callback Optional callback
   * @return {Array} Array of the iterated results
   */
  CronExpression.prototype.iterate = function iterate (steps, callback) {
    var dates = [];
  
    if (steps >= 0) {
      for (var i = 0, c = steps; i < c; i++) {
        try {
          var item = this.next();
          dates.push(item);
  
          // Fire the callback
          if (callback) {
            callback(item, i);
          }
        } catch (err) {
          break;
        }
      }
    } else {
      for (var i = 0, c = steps; i > c; i--) {
        try {
          var item = this.prev();
          dates.push(item);
  
          // Fire the callback
          if (callback) {
            callback(item, i);
          }
        } catch (err) {
          break;
        }
      }
    }
  
    return dates;
  };
  
  /**
   * Reset expression iterator state
   *
   * @public
   */
  CronExpression.prototype.reset = function reset (newDate) {
    this._currentDate = new CronDate(newDate || this._options.currentDate);
  };
  
  /**
   * Stringify the expression
   *
   * @public
   * @param {Boolean} [includeSeconds] Should stringify seconds
   * @return {String}
   */
  CronExpression.prototype.stringify = function stringify(includeSeconds) {
    var resultArr = [];
    for (var i = includeSeconds ? 0 : 1, c = CronExpression.map.length; i < c; ++i) {
      var field = CronExpression.map[i];
      var value = this.fields[field];
      var constraint = CronExpression.constraints[i];
      resultArr.push(stringifyField(value, constraint.min, constraint.max));
    }
    return resultArr.join(' ');
  };
  
  /**
   * Parse input expression (async)
   *
   * @public
   * @param {String} expression Input expression
   * @param {Object} [options] Parsing options
   */
  CronExpression.parse = function parse(expression, options) {
    var self = this;
    if (typeof options === 'function') {
      options = {};
    }
  
    function parse (expression, options) {
      if (!options) {
        options = {};
      }
  
      if (typeof options.currentDate === 'undefined') {
        options.currentDate = new CronDate(undefined, self._tz);
      }
  
      // Is input expression predefined?
      if (CronExpression.predefined[expression]) {
        expression = CronExpression.predefined[expression];
      }
  
      // Split fields
      var fields = [];
      var atoms = (expression + '').trim().split(/\s+/);
  
      if (atoms.length > 6) {
        throw new Error('Invalid cron expression');
      }
  
      // Resolve fields
      var start = (CronExpression.map.length - atoms.length);
      for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
        var field = CronExpression.map[i]; // Field name
        var value = atoms[atoms.length > c ? i : i - start]; // Field value
  
        if (i < start || !value) { // Use default value
          fields.push(CronExpression._parseField(
            field,
            CronExpression.parseDefaults[i],
            CronExpression.constraints[i]
            )
          );
        } else {
          var val = field === 'dayOfWeek' ? parseNthDay(value) : value;
  
          fields.push(CronExpression._parseField(
            field,
            val,
            CronExpression.constraints[i]
            )
          );
        }
      }
  
      var mappedFields = {};
      for (var i = 0, c = CronExpression.map.length; i < c; i++) {
        var key = CronExpression.map[i];
        mappedFields[key] = fields[i];
      }
  
      var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
      mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
      return new CronExpression(mappedFields, options);
  
      /**
       * Parses out the # special character for the dayOfWeek field & adds it to options.
       *
       * @param {String} val
       * @return {String}
       * @private
       */
      function parseNthDay(val) {
        var atoms = val.split('#');
        if (atoms.length > 1) {
          var nthValue = +atoms[atoms.length - 1];
          if(/,/.test(val)) {
            throw new Error('Constraint error, invalid dayOfWeek `#` and `,` '
              + 'special characters are incompatible');
          }
          if(/\//.test(val)) {
            throw new Error('Constraint error, invalid dayOfWeek `#` and `/` '
              + 'special characters are incompatible');
          }
          if(/-/.test(val)) {
            throw new Error('Constraint error, invalid dayOfWeek `#` and `-` '
              + 'special characters are incompatible');
          }
          if (atoms.length > 2 || safeIsNaN(nthValue) || (nthValue < 1 || nthValue > 5)) {
            throw new Error('Constraint error, invalid dayOfWeek occurrence number (#)');
          }
  
          options.nthDayOfWeek = nthValue;
          return atoms[0];
        }
        return val;
      }
    }
  
    return parse(expression, options);
  };
  
  /**
   * Convert cron fields back to Cron Expression
   *
   * @public
   * @param {Object} fields Input fields
   * @param {Object} [options] Parsing options
   * @return {Object}
   */
  CronExpression.fieldsToExpression = function fieldsToExpression(fields, options) {
    function validateConstraints (field, values, constraints) {
      if (!values) {
        throw new Error('Validation error, Field ' + field + ' is missing');
      }
      if (values.length === 0) {
        throw new Error('Validation error, Field ' + field + ' contains no values');
      }
      for (var i = 0, c = values.length; i < c; i++) {
        var value = values[i];
  
        if (typeof value === 'string' && constraints.chars.indexOf(value) > -1) {
          continue;
        }
        // Check constraints
        if (typeof value !== 'number' || safeIsNaN(value) || value < constraints.min || value > constraints.max) {
          throw new Error(
            'Constraint error, got value ' + value + ' expected range ' +
            constraints.min + '-' + constraints.max
          );
        }
      }
    }
  
    var mappedFields = {};
    for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
      var field = CronExpression.map[i]; // Field name
      var values = fields[field];
      validateConstraints(
        field,
        values,
        CronExpression.constraints[i]
      );
      var copy = [];
      var j = -1;
      while (++j < values.length) {
        copy[j] = values[j];
      }
      values = copy.sort(CronExpression._sortCompareFn)
        .filter(function(item, pos, ary) {
          return !pos || item !== ary[pos - 1];
        });
      if (values.length !== copy.length) {
        throw new Error('Validation error, Field ' + field + ' contains duplicate values');
      }
      mappedFields[field] = values;
    }
    var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
    mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
    return new CronExpression(mappedFields, options || {});
  };
  
  module.exports = CronExpression;
  