text: 'use strict';
  
  let path = require('doc-path'),
      constants = require('./constants.json');
  
  const dateStringRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
  
  module.exports = {
      isStringRepresentation,
      isDateRepresentation,
      computeSchemaDifferences,
      deepCopy,
      convert,
      isEmptyField,
      removeEmptyFields,
      getNCharacters,
      unwind,
      isInvalid,
  
      // underscore replacements:
      isString,
      isNull,
      isError,
      isDate,
      isUndefined,
      isObject,
      unique,
      flatten
  };
  
  /**
   * Build the options to be passed to the appropriate function
   * If a user does not provide custom options, then we use our default
   * If options are provided, then we set each valid key that was passed
   * @param opts {Object} options object
   * @return {Object} options object
   */
  function buildOptions(opts) {
      opts = {...constants.defaultOptions, ...opts || {}};
  
      // Note: Object.assign does a shallow default, we need to deep copy the delimiter object
      opts.delimiter = {...constants.defaultOptions.delimiter, ...opts.delimiter};
  
      // Otherwise, send the options back
      return opts;
  }
  
  /**
   * When promisified, the callback and options argument ordering is swapped, so
   * this function is intended to determine which argument is which and return
   * them in the correct order
   * @param arg1 {Object|Function} options or callback
   * @param arg2 {Object|Function} options or callback
   */
  function parseArguments(arg1, arg2) {
      // If this was promisified (callback and opts are swapped) then fix the argument order.
      if (isObject(arg1) && !isFunction(arg1)) {
          return {
              options: arg1,
              callback: arg2
          };
      }
      // Regular ordering where the callback is provided before the options object
      return {
          options: arg2,
          callback: arg1
      };
  }
  
  /**
   * Validates the parameters passed in to json2csv and csv2json
   * @param config {Object} of the form: { data: {Any}, callback: {Function}, dataCheckFn: Function, errorMessages: {Object} }
   */
  function validateParameters(config) {
      // If a callback wasn't provided, throw an error
      if (!config.callback) {
          throw new Error(constants.errors.callbackRequired);
      }
  
      // If we don't receive data, report an error
      if (!config.data) {
          config.callback(new Error(config.errorMessages.cannotCallOn + config.data + '.'));
          return false;
      }
  
      // The data provided data does not meet the type check requirement
      if (!config.dataCheckFn(config.data)) {
          config.callback(new Error(config.errorMessages.dataCheckFailure));
          return false;
      }
  
      // If we didn't hit any known error conditions, then the data is so far determined to be valid
      // Note: json2csv/csv2json may perform additional validity checks on the data
      return true;
  }
  
  /**
   * Abstracted function to perform the conversion of json-->csv or csv-->json
   * depending on the converter class that is passed via the params object
   * @param params {Object}
   */
  function convert(params) {
      let {options, callback} = parseArguments(params.callback, params.options);
      options = buildOptions(options);
  
      let converter = new params.converter(options),
  
          // Validate the parameters before calling the converter's convert function
          valid = validateParameters({
              data: params.data,
              callback,
              errorMessages: converter.validationMessages,
              dataCheckFn: converter.validationFn
          });
  
      if (valid) converter.convert(params.data, callback);
  }
  
  /**
   * Utility function to deep copy an object, used by the module tests
   * @param obj
   * @returns {any}
   */
  function deepCopy(obj) {
      return JSON.parse(JSON.stringify(obj));
  }
  
  /**
   * Helper function that determines whether the provided value is a representation
   *   of a string. Given the RFC4180 requirements, that means that the value is
   *   wrapped in value wrap delimiters (usually a quotation mark on each side).
   * @param fieldValue
   * @param options
   * @returns {boolean}
   */
  function isStringRepresentation(fieldValue, options) {
      const firstChar = fieldValue[0],
          lastIndex = fieldValue.length - 1,
          lastChar = fieldValue[lastIndex];
  
      // If the field starts and ends with a wrap delimiter
      return firstChar === options.delimiter.wrap && lastChar === options.delimiter.wrap;
  }
  
  /**
   * Helper function that determines whether the provided value is a representation
   *   of a date.
   * @param fieldValue
   * @returns {boolean}
   */
  function isDateRepresentation(fieldValue) {
      return dateStringRegex.test(fieldValue);
  }
  
  /**
   * Helper function that determines the schema differences between two objects.
   * @param schemaA
   * @param schemaB
   * @returns {*}
   */
  function computeSchemaDifferences(schemaA, schemaB) {
      return arrayDifference(schemaA, schemaB)
          .concat(arrayDifference(schemaB, schemaA));
  }
  
  /**
   * Utility function to check if a field is considered empty so that the emptyFieldValue can be used instead
   * @param fieldValue
   * @returns {boolean}
   */
  function isEmptyField(fieldValue) {
      return isUndefined(fieldValue) || isNull(fieldValue) || fieldValue === '';
  }
  
  /**
   * Helper function that removes empty field values from an array.
   * @param fields
   * @returns {Array}
   */
  function removeEmptyFields(fields) {
      return fields.filter((field) => !isEmptyField(field));
  }
  
  /**
   * Helper function that retrieves the next n characters from the start index in
   *   the string including the character at the start index. This is used to
   *   check if are currently at an EOL value, since it could be multiple
   *   characters in length (eg. '\r\n')
   * @param str
   * @param start
   * @param n
   * @returns {string}
   */
  function getNCharacters(str, start, n) {
      return str.substring(start, start + n);
  }
  
  /**
   * The following unwind functionality is a heavily modified version of @edwincen's
   * unwind extension for lodash. Since lodash is a large package to require in,
   * and all of the required functionality was already being imported, either
   * natively or with doc-path, I decided to rewrite the majority of the logic
   * so that an additional dependency would not be required. The original code
   * with the lodash dependency can be found here:
   *
   * https://github.com/edwincen/unwind/blob/master/index.js
   */
  
  /**
   * Core function that unwinds an item at the provided path
   * @param accumulator {Array<any>}
   * @param item {any}
   * @param fieldPath {String}
   */
  function unwindItem(accumulator, item, fieldPath) {
      const valueToUnwind = path.evaluatePath(item, fieldPath);
      let cloned = deepCopy(item);
  
      if (Array.isArray(valueToUnwind) && valueToUnwind.length) {
          valueToUnwind.forEach((val) => {
              cloned = deepCopy(item);
              accumulator.push(path.setPath(cloned, fieldPath, val));
          });
      } else if (Array.isArray(valueToUnwind) && valueToUnwind.length === 0) {
          // Push an empty string so the value is empty since there are no values
          path.setPath(cloned, fieldPath, '');
          accumulator.push(cloned);
      } else {
          accumulator.push(cloned);
      }
  }
  
  /**
   * Main unwind function which takes an array and a field to unwind.
   * @param array {Array<any>}
   * @param field {String}
   * @returns {Array<any>}
   */
  function unwind(array, field) {
      const result = [];
      array.forEach((item) => {
          unwindItem(result, item, field);
      });
      return result;
  }
  
  /*
   * Helper functions which were created to remove underscorejs from this package.
   */
  
  function isString(value) {
      return typeof value === 'string';
  }
  
  function isObject(value) {
      return typeof value === 'object';
  }
  
  function isFunction(value) {
      return typeof value === 'function';
  }
  
  function isNull(value) {
      return value === null;
  }
  
  function isDate(value) {
      return value instanceof Date;
  }
  
  function isUndefined(value) {
      return typeof value === 'undefined';
  }
  
  function isError(value) {
      return Object.prototype.toString.call(value) === '[object Error]';
  }
  
  function arrayDifference(a, b) {
      return a.filter((x) => !b.includes(x));
  }
  
  function unique(array) {
      return [...new Set(array)];
  }
  
  function flatten(array) {
      return [].concat(...array);
  }
  
  /**
   * Used to help avoid incorrect values returned by JSON.parse when converting
   * CSV back to JSON, such as '39e1804' which JSON.parse converts to Infinity
   */
  function isInvalid(parsedJson) {
      return parsedJson === Infinity ||
          parsedJson === -Infinity;
  }
  