text: import PropTypes from 'prop-types';
  
  /*
   * decaffeinate suggestions:
   * DS101: Remove unnecessary use of Array.from
   * DS102: Remove unnecessary code created because of implicit returns
   * DS104: Avoid inline assignments
   * DS201: Simplify complex destructure assignments
   * DS203: Remove `|| {}` from converted for-own loops
   * DS205: Consider reworking code to avoid use of IIFEs
   * DS207: Consider shorter variations of null checks
   * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
   */
  
  const addSeparators = function(nStr, thousandsSep, decimalSep) {
    const x = String(nStr).split('.');
    let x1 = x[0];
    const x2 = x.length > 1 ? decimalSep + x[1] : '';
    const rgx = /(\d+)(\d{3})/;
    while (rgx.test(x1)) {
      x1 = x1.replace(rgx, `$1${thousandsSep}$2`);
    }
    return x1 + x2;
  };
  
  const numberFormat = function(opts_in) {
    const defaults = {
      digitsAfterDecimal: 2,
      scaler: 1,
      thousandsSep: ',',
      decimalSep: '.',
      prefix: '',
      suffix: '',
    };
    const opts = Object.assign({}, defaults, opts_in);
    return function(x) {
      if (isNaN(x) || !isFinite(x)) {
        return '';
      }
      const result = addSeparators(
        (opts.scaler * x).toFixed(opts.digitsAfterDecimal),
        opts.thousandsSep,
        opts.decimalSep
      );
      return `${opts.prefix}${result}${opts.suffix}`;
    };
  };
  
  const rx = /(\d+)|(\D+)/g;
  const rd = /\d/;
  const rz = /^0/;
  const naturalSort = (as, bs) => {
    // nulls first
    if (bs !== null && as === null) {
      return -1;
    }
    if (as !== null && bs === null) {
      return 1;
    }
  
    // then raw NaNs
    if (typeof as === 'number' && isNaN(as)) {
      return -1;
    }
    if (typeof bs === 'number' && isNaN(bs)) {
      return 1;
    }
  
    // numbers and numbery strings group together
    const nas = Number(as);
    const nbs = Number(bs);
    if (nas < nbs) {
      return -1;
    }
    if (nas > nbs) {
      return 1;
    }
  
    // within that, true numbers before numbery strings
    if (typeof as === 'number' && typeof bs !== 'number') {
      return -1;
    }
    if (typeof bs === 'number' && typeof as !== 'number') {
      return 1;
    }
    if (typeof as === 'number' && typeof bs === 'number') {
      return 0;
    }
  
    // 'Infinity' is a textual number, so less than 'A'
    if (isNaN(nbs) && !isNaN(nas)) {
      return -1;
    }
    if (isNaN(nas) && !isNaN(nbs)) {
      return 1;
    }
  
    // finally, "smart" string sorting per http://stackoverflow.com/a/4373421/112871
    let a = String(as);
    let b = String(bs);
    if (a === b) {
      return 0;
    }
    if (!rd.test(a) || !rd.test(b)) {
      return a > b ? 1 : -1;
    }
  
    // special treatment for strings containing digits
    a = a.match(rx);
    b = b.match(rx);
    while (a.length && b.length) {
      const a1 = a.shift();
      const b1 = b.shift();
      if (a1 !== b1) {
        if (rd.test(a1) && rd.test(b1)) {
          return a1.replace(rz, '.0') - b1.replace(rz, '.0');
        }
        return a1 > b1 ? 1 : -1;
      }
    }
    return a.length - b.length;
  };
  
  const sortAs = function(order) {
    const mapping = {};
  
    // sort lowercased keys similarly
    const l_mapping = {};
    for (const i in order) {
      const x = order[i];
      mapping[x] = i;
      if (typeof x === 'string') {
        l_mapping[x.toLowerCase()] = i;
      }
    }
    return function(a, b) {
      if (a in mapping && b in mapping) {
        return mapping[a] - mapping[b];
      } else if (a in mapping) {
        return -1;
      } else if (b in mapping) {
        return 1;
      } else if (a in l_mapping && b in l_mapping) {
        return l_mapping[a] - l_mapping[b];
      } else if (a in l_mapping) {
        return -1;
      } else if (b in l_mapping) {
        return 1;
      }
      return naturalSort(a, b);
    };
  };
  
  const getSort = function(sorters, attr) {
    if (sorters) {
      if (typeof sorters === 'function') {
        const sort = sorters(attr);
        if (typeof sort === 'function') {
          return sort;
        }
      } else if (attr in sorters) {
        return sorters[attr];
      }
    }
    return naturalSort;
  };
  
  // aggregator templates default to US number formatting but this is overrideable
  const usFmt = numberFormat();
  const usFmtInt = numberFormat({digitsAfterDecimal: 0});
  const usFmtPct = numberFormat({
    digitsAfterDecimal: 1,
    scaler: 100,
    suffix: '%',
  });
  
  const aggregatorTemplates = {
    count(formatter = usFmtInt) {
      return () =>
        function() {
          return {
            count: 0,
            push() {
              this.count++;
            },
            value() {
              return this.count;
            },
            format: formatter,
          };
        };
    },
  
    uniques(fn, formatter = usFmtInt) {
      return function([attr]) {
        return function() {
          return {
            uniq: [],
            push(record) {
              if (!Array.from(this.uniq).includes(record[attr])) {
                this.uniq.push(record[attr]);
              }
            },
            value() {
              return fn(this.uniq);
            },
            format: formatter,
            numInputs: typeof attr !== 'undefined' ? 0 : 1,
          };
        };
      };
    },
  
    sum(formatter = usFmt) {
      return function([attr]) {
        return function() {
          return {
            sum: 0,
            push(record) {
              if (!isNaN(parseFloat(record[attr]))) {
                this.sum += parseFloat(record[attr]);
              }
            },
            value() {
              return this.sum;
            },
            format: formatter,
            numInputs: typeof attr !== 'undefined' ? 0 : 1,
          };
        };
      };
    },
  
    extremes(mode, formatter = usFmt) {
      return function([attr]) {
        return function(data) {
          return {
            val: null,
            sorter: getSort(
              typeof data !== 'undefined' ? data.sorters : null,
              attr
            ),
            push(record) {
              let x = record[attr];
              if (['min', 'max'].includes(mode)) {
                x = parseFloat(x);
                if (!isNaN(x)) {
                  this.val = Math[mode](x, this.val !== null ? this.val : x);
                }
              }
              if (
                mode === 'first' &&
                this.sorter(x, this.val !== null ? this.val : x) <= 0
              ) {
                this.val = x;
              }
              if (
                mode === 'last' &&
                this.sorter(x, this.val !== null ? this.val : x) >= 0
              ) {
                this.val = x;
              }
            },
            value() {
              return this.val;
            },
            format(x) {
              if (isNaN(x)) {
                return x;
              }
              return formatter(x);
            },
            numInputs: typeof attr !== 'undefined' ? 0 : 1,
          };
        };
      };
    },
  
    quantile(q, formatter = usFmt) {
      return function([attr]) {
        return function() {
          return {
            vals: [],
            push(record) {
              const x = parseFloat(record[attr]);
              if (!isNaN(x)) {
                this.vals.push(x);
              }
            },
            value() {
              if (this.vals.length === 0) {
                return null;
              }
              this.vals.sort((a, b) => a - b);
              const i = (this.vals.length - 1) * q;
              return (this.vals[Math.floor(i)] + this.vals[Math.ceil(i)]) / 2.0;
            },
            format: formatter,
            numInputs: typeof attr !== 'undefined' ? 0 : 1,
          };
        };
      };
    },
  
    runningStat(mode = 'mean', ddof = 1, formatter = usFmt) {
      return function([attr]) {
        return function() {
          return {
            n: 0.0,
            m: 0.0,
            s: 0.0,
            push(record) {
              const x = parseFloat(record[attr]);
              if (isNaN(x)) {
                return;
              }
              this.n += 1.0;
              if (this.n === 1.0) {
                this.m = x;
              }
              const m_new = this.m + (x - this.m) / this.n;
              this.s = this.s + (x - this.m) * (x - m_new);
              this.m = m_new;
            },
            value() {
              if (mode === 'mean') {
                if (this.n === 0) {
                  return 0 / 0;
                }
                return this.m;
              }
              if (this.n <= ddof) {
                return 0;
              }
              switch (mode) {
                case 'var':
                  return this.s / (this.n - ddof);
                case 'stdev':
                  return Math.sqrt(this.s / (this.n - ddof));
                default:
                  throw new Error('unknown mode for runningStat');
              }
            },
            format: formatter,
            numInputs: typeof attr !== 'undefined' ? 0 : 1,
          };
        };
      };
    },
  
    sumOverSum(formatter = usFmt) {
      return function([num, denom]) {
        return function() {
          return {
            sumNum: 0,
            sumDenom: 0,
            push(record) {
              if (!isNaN(parseFloat(record[num]))) {
                this.sumNum += parseFloat(record[num]);
              }
              if (!isNaN(parseFloat(record[denom]))) {
                this.sumDenom += parseFloat(record[denom]);
              }
            },
            value() {
              return this.sumNum / this.sumDenom;
            },
            format: formatter,
            numInputs:
              typeof num !== 'undefined' && typeof denom !== 'undefined' ? 0 : 2,
          };
        };
      };
    },
  
    fractionOf(wrapped, type = 'total', formatter = usFmtPct) {
      return (...x) =>
        function(data, rowKey, colKey) {
          return {
            selector: {total: [[], []], row: [rowKey, []], col: [[], colKey]}[
              type
            ],
            inner: wrapped(...Array.from(x || []))(data, rowKey, colKey),
            push(record) {
              this.inner.push(record);
            },
            format: formatter,
            value() {
              return (
                this.inner.value() /
                data
                  .getAggregator(...Array.from(this.selector || []))
                  .inner.value()
              );
            },
            numInputs: wrapped(...Array.from(x || []))().numInputs,
          };
        };
    },
  };
  
  aggregatorTemplates.countUnique = f =>
    aggregatorTemplates.uniques(x => x.length, f);
  aggregatorTemplates.listUnique = s =>
    aggregatorTemplates.uniques(x => x.join(s), x => x);
  aggregatorTemplates.max = f => aggregatorTemplates.extremes('max', f);
  aggregatorTemplates.min = f => aggregatorTemplates.extremes('min', f);
  aggregatorTemplates.first = f => aggregatorTemplates.extremes('first', f);
  aggregatorTemplates.last = f => aggregatorTemplates.extremes('last', f);
  aggregatorTemplates.median = f => aggregatorTemplates.quantile(0.5, f);
  aggregatorTemplates.average = f =>
    aggregatorTemplates.runningStat('mean', 1, f);
  aggregatorTemplates.var = (ddof, f) =>
    aggregatorTemplates.runningStat('var', ddof, f);
  aggregatorTemplates.stdev = (ddof, f) =>
    aggregatorTemplates.runningStat('stdev', ddof, f);
  
  // default aggregators & renderers use US naming and number formatting
  const aggregators = (tpl => ({
    // Count: tpl.count(usFmtInt),
    // 'Count Unique Values': tpl.countUnique(usFmtInt),
    'List Unique Values': tpl.listUnique(', '),
    Sum: tpl.sum(usFmt),
    'Integer Sum': tpl.sum(usFmtInt),
    Average: tpl.average(usFmt),
    Median: tpl.median(usFmt),
    'Sample Variance': tpl.var(1, usFmt),
    'Sample Standard Deviation': tpl.stdev(1, usFmt),
    Minimum: tpl.min(usFmt),
    Maximum: tpl.max(usFmt),
    First: tpl.first(usFmt),
    Last: tpl.last(usFmt),
    'Sum over Sum': tpl.sumOverSum(usFmt),
    'Sum as Fraction of Total': tpl.fractionOf(tpl.sum(), 'total', usFmtPct),
    'Sum as Fraction of Rows': tpl.fractionOf(tpl.sum(), 'row', usFmtPct),
    'Sum as Fraction of Columns': tpl.fractionOf(tpl.sum(), 'col', usFmtPct),
    'Count as Fraction of Total': tpl.fractionOf(tpl.count(), 'total', usFmtPct),
    'Count as Fraction of Rows': tpl.fractionOf(tpl.count(), 'row', usFmtPct),
    'Count as Fraction of Columns': tpl.fractionOf(tpl.count(), 'col', usFmtPct),
  }))(aggregatorTemplates);
  
  const locales = {
    en: {
      aggregators,
      localeStrings: {
        renderError: 'An error occurred rendering the PivotTable results.',
        computeError: 'An error occurred computing the PivotTable results.',
        uiRenderError: 'An error occurred rendering the PivotTable UI.',
        selectAll: 'Select All',
        selectNone: 'Select None',
        tooMany: '(too many to list)',
        filterResults: 'Filter values',
        apply: 'Apply',
        cancel: 'Cancel',
        totals: 'Totals',
        vs: 'vs',
        by: 'by',
      },
    },
  };
  
  // dateFormat deriver l10n requires month and day names to be passed in directly
  const mthNamesEn = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
  ];
  const dayNamesEn = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  const zeroPad = number => `0${number}`.substr(-2, 2); // eslint-disable-line no-magic-numbers
  
  const derivers = {
    bin(col, binWidth) {
      return record => record[col] - record[col] % binWidth;
    },
    dateFormat(
      col,
      formatString,
      utcOutput = false,
      mthNames = mthNamesEn,
      dayNames = dayNamesEn
    ) {
      const utc = utcOutput ? 'UTC' : '';
      return function(record) {
        const date = new Date(Date.parse(record[col]));
        if (isNaN(date)) {
          return '';
        }
        return formatString.replace(/%(.)/g, function(m, p) {
          switch (p) {
            case 'y':
              return date[`get${utc}FullYear`]();
            case 'm':
              return zeroPad(date[`get${utc}Month`]() + 1);
            case 'n':
              return mthNames[date[`get${utc}Month`]()];
            case 'd':
              return zeroPad(date[`get${utc}Date`]());
            case 'w':
              return dayNames[date[`get${utc}Day`]()];
            case 'x':
              return date[`get${utc}Day`]();
            case 'H':
              return zeroPad(date[`get${utc}Hours`]());
            case 'M':
              return zeroPad(date[`get${utc}Minutes`]());
            case 'S':
              return zeroPad(date[`get${utc}Seconds`]());
            default:
              return `%${p}`;
          }
        });
      };
    },
  };
  
  /*
  Data Model class
  */
  
  class PivotData {
    constructor(inputProps = {}) {
      this.props = Object.assign({}, PivotData.defaultProps, inputProps);
      if (!this.props.renderAttributeKey) {
        this.props.renderAttributeKey = x => x
      }
      if (!this.props.renderAttributeValue) {
        this.props.renderAttributeValue = x => x
      }
  
      PropTypes.checkPropTypes(
        PivotData.propTypes,
        this.props,
        'prop',
        'PivotData'
      );
  
      this.aggregator = this.props.aggregators[this.props.aggregatorName](
        this.props.vals
      );
      this.tree = {};
      this.rowKeys = [];
      this.colKeys = [];
      this.rowTotals = {};
      this.colTotals = {};
      this.allTotal = this.aggregator(this, [], []);
      this.sorted = false;
  
      // iterate through input, accumulating data for cells
      PivotData.forEachRecord(
        this.props.data,
        this.props.derivedAttributes,
        record => {
          if (this.filter(record)) {
            this.processRecord(record);
          }
        }
      );
    }
  
    filter(record) {
      for (const k in this.props.valueFilter) {
        if (record[k] in this.props.valueFilter[k]) {
          return false;
        }
      }
      return true;
    }
  
    forEachMatchingRecord(criteria, callback) {
      return PivotData.forEachRecord(
        this.props.data,
        this.props.derivedAttributes,
        record => {
          if (!this.filter(record)) {
            return;
          }
          for (const k in criteria) {
            const v = criteria[k];
            if (v !== (k in record ? record[k] : 'null')) {
              return;
            }
          }
          callback(record);
        }
      );
    }
  
    arrSort(attrs) {
      let a;
      const sortersArr = (() => {
        const result = [];
        for (a of Array.from(attrs)) {
          result.push(getSort(this.props.sorters, a));
        }
        return result;
      })();
      return function(a, b) {
        for (const i of Object.keys(sortersArr || {})) {
          const sorter = sortersArr[i];
          const comparison = sorter(a[i], b[i]);
          if (comparison !== 0) {
            return comparison;
          }
        }
        return 0;
      };
    }
  
    sortKeys() {
      if (!this.sorted) {
        this.sorted = true;
        const v = (r, c) => this.getAggregator(r, c).value();
        switch (this.props.rowOrder) {
          case 'value_a_to_z':
            this.rowKeys.sort((a, b) => naturalSort(v(a, []), v(b, [])));
            break;
          case 'value_z_to_a':
            this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, [])));
            break;
          default:
            this.rowKeys.sort(this.arrSort(this.props.rows));
        }
        switch (this.props.colOrder) {
          case 'value_a_to_z':
            this.colKeys.sort((a, b) => naturalSort(v([], a), v([], b)));
            break;
          case 'value_z_to_a':
            this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b)));
            break;
          default:
            this.colKeys.sort(this.arrSort(this.props.cols));
        }
      }
    }
  
    getColKeys() {
      this.sortKeys();
      return this.colKeys;
    }
  
    getRowKeys() {
      this.sortKeys();
      return this.rowKeys;
    }
  
    processRecord(record) {
      // this code is called in a tight loop
      const colKey = [];
      const rowKey = [];
      for (const x of Array.from(this.props.cols)) {
        colKey.push(x in record ? record[x] : 'null');
      }
      for (const x of Array.from(this.props.rows)) {
        rowKey.push(x in record ? record[x] : 'null');
      }
      const flatRowKey = rowKey.join(String.fromCharCode(0));
      const flatColKey = colKey.join(String.fromCharCode(0));
  
      this.allTotal.push(record);
  
      if (rowKey.length !== 0) {
        if (!this.rowTotals[flatRowKey]) {
          this.rowKeys.push(rowKey);
          this.rowTotals[flatRowKey] = this.aggregator(this, rowKey, []);
        }
        this.rowTotals[flatRowKey].push(record);
      }
  
      if (colKey.length !== 0) {
        if (!this.colTotals[flatColKey]) {
          this.colKeys.push(colKey);
          this.colTotals[flatColKey] = this.aggregator(this, [], colKey);
        }
        this.colTotals[flatColKey].push(record);
      }
  
      if (colKey.length !== 0 && rowKey.length !== 0) {
        if (!this.tree[flatRowKey]) {
          this.tree[flatRowKey] = {};
        }
        if (!this.tree[flatRowKey][flatColKey]) {
          this.tree[flatRowKey][flatColKey] = this.aggregator(
            this,
            rowKey,
            colKey
          );
        }
        this.tree[flatRowKey][flatColKey].push(record);
      }
    }
  
    getAggregator(rowKey, colKey) {
      let agg;
      const flatRowKey = rowKey.join(String.fromCharCode(0));
      const flatColKey = colKey.join(String.fromCharCode(0));
      if (rowKey.length === 0 && colKey.length === 0) {
        agg = this.allTotal;
      } else if (rowKey.length === 0) {
        agg = this.colTotals[flatColKey];
      } else if (colKey.length === 0) {
        agg = this.rowTotals[flatRowKey];
      } else {
        agg = this.tree[flatRowKey][flatColKey];
      }
      return (
        agg || {
          value() {
            return null;
          },
          format() {
            return '';
          },
        }
      );
    }
  }
  
  // can handle arrays or jQuery selections of tables
  PivotData.forEachRecord = function(input, derivedAttributes, f) {
    let addRecord, record;
    if (Object.getOwnPropertyNames(derivedAttributes).length === 0) {
      addRecord = f;
    } else {
      addRecord = function(record) {
        for (const k in derivedAttributes) {
          const derived = derivedAttributes[k](record);
          if (derived !== null) {
            record[k] = derived;
          }
        }
        return f(record);
      };
    }
  
    // if it's a function, have it call us back
    if (typeof input === 'function') {
      return input(addRecord);
    } else if (Array.isArray(input)) {
      if (Array.isArray(input[0])) {
        // array of arrays
        return (() => {
          const result = [];
          for (const i of Object.keys(input || {})) {
            const compactRecord = input[i];
            if (i > 0) {
              record = {};
              for (const j of Object.keys(input[0] || {})) {
                const k = input[0][j];
                record[k] = compactRecord[j];
              }
              result.push(addRecord(record));
            }
          }
          return result;
        })();
      }
  
      // array of objects
      return (() => {
        const result1 = [];
        for (record of Array.from(input)) {
          result1.push(addRecord(record));
        }
        return result1;
      })();
    }
    throw new Error('unknown input format');
  };
  
  PivotData.defaultProps = {
    aggregators: aggregators,
    cols: [],
    rows: [],
    vals: [],
    aggregatorName: 'Sum',
    sorters: {},
    valueFilter: {},
    rowOrder: 'key_a_to_z',
    colOrder: 'key_a_to_z',
    derivedAttributes: {},
  };
  
  PivotData.propTypes = {
    data: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.func])
      .isRequired,
    aggregatorName: PropTypes.string,
    cols: PropTypes.arrayOf(PropTypes.string),
    rows: PropTypes.arrayOf(PropTypes.string),
    vals: PropTypes.arrayOf(PropTypes.string),
    valueFilter: PropTypes.objectOf(PropTypes.objectOf(PropTypes.bool)),
    sorters: PropTypes.oneOfType([
      PropTypes.func,
      PropTypes.objectOf(PropTypes.func),
    ]),
    derivedAttributes: PropTypes.objectOf(PropTypes.func),
    rowOrder: PropTypes.oneOf(['key_a_to_z', 'value_a_to_z', 'value_z_to_a']),
    colOrder: PropTypes.oneOf(['key_a_to_z', 'value_a_to_z', 'value_z_to_a']),
  };
  
  export {
    aggregatorTemplates,
    aggregators,
    derivers,
    locales,
    naturalSort,
    numberFormat,
    getSort,
    sortAs,
    PivotData,
  };
  