<|start_filename|>test/verbs/impute-test.js<|end_filename|>
import tape from 'tape';
import tableEqual from '../table-equal';
import { op, table } from '../../src';

const na = undefined;

tape('impute imputes values for an ungrouped table', t => {
  const dt = table({ x: [1, null, NaN, undefined, 3] });

  tableEqual(t,
    dt.impute({ x: () => 2 }),
    { x: [1, 2, 2, 2, 3] },
    'impute data, constant'
  );

  tableEqual(t,
    dt.impute({ x: op.mean('x') }),
    { x: [1, 2, 2, 2, 3] },
    'impute data, mean'
  );

  t.end();
});

tape('impute imputes values for a grouped table', t => {
  const dt = table({
    k: [0, 0, 0, 0, 1, 1, 1, 1],
    x: [1, null, NaN, 3, 3, null, undefined, 5]
  }).groupby('k');

  const t1 = dt.impute({ x: () => 2 });
  tableEqual(t, t1, {
    k: [0, 0, 0, 0, 1, 1, 1, 1],
    x: [1, 2, 2, 3, 3, 2, 2, 5]
  }, 'impute data, constant');

  t.equal(dt.groups(), t1.groups(), 'groups');

  const t2 = dt.impute({ x: op.mean('x') });
  tableEqual(t, t2, {
    k: [0, 0, 0, 0, 1, 1, 1, 1],
    x: [1, 2, 2, 3, 3, 4, 4, 5]
  }, 'impute data, mean');

  t.equal(dt.groups(), t2.groups(), 'groups');

  t.end();
});

tape('impute imputes expanded rows for an ungrouped table', t => {
  const dt = table({
      x: ['a', 'b', 'c'],
      y: [1, 2, 3],
      z: ['x', 'x', 'x']
    })
    .impute(null, { expand: ['x', 'y'] })
    .orderby('x', 'y')
    .reify();

  tableEqual(t, dt, {
    x: ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c'],
    y: [ 1,   2,   3,   1,   2,   3,   1,   2,   3 ],
    z: ['x', na,  na,  na, 'x',  na,  na,  na,  'x']
  }, 'impute data');

  t.equal(dt.groups(), null, 'no groups');

  t.end();
});

tape('impute imputes expanded rows for a grouped table', t => {
  const dt = table({
      x: ['a', 'a', 'b', 'c'],
      y: [1, 1, 2, 3],
      z: ['x', 'x', 'y', 'z'],
      v: [0, 9, 8, 7]
    })
    .groupby('x', 'y')
    .impute(null, { expand: 'z' })
    .orderby('x', 'y', 'z')
    .reify();

  tableEqual(t, dt, {
    x: ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c'],
    y: [ 1,   1,   1,   1,   2,   2,   2,   3,   3,   3 ],
    z: ['x', 'x', 'y', 'z', 'x', 'y', 'z', 'x', 'y', 'z'],
    v: [ 0,   9,   na,  na,  na,  8,   na,  na,  na,  7 ]
  }, 'impute data');

  t.deepEqual(
    Array.from(dt.groups().keys),
    [0, 0, 0, 0, 1, 1, 1, 2, 2, 2],
    'group keys'
  );

  t.end();
});

tape('impute imputes values and rows for an ungrouped table', t => {
  const imp = 'imp';
  const dt = table({
      x: ['a', 'b', 'c'],
      y: [1, 2, 3],
      z: ['x', 'x', 'x']
    })
    .impute({ z: () => 'imp' }, { expand: ['x', 'y'] })
    .orderby('x', 'y')
    .reify();

  tableEqual(t, dt, {
    x: ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c'],
    y: [ 1,   2,   3,   1,   2,   3,   1,   2,   3 ],
    z: ['x', imp, imp, imp, 'x', imp, imp, imp, 'x']
  }, 'impute data');

  t.equal(dt.groups(), null, 'no groups');

  t.end();
});

tape('impute imputes expanded rows for a grouped table', t => {
  const dt = table({
      x: ['a', 'a', 'b', 'c'],
      y: [1, 1, 2, 3],
      z: ['x', 'x', 'y', 'z'],
      v: [0, 9, 8, 7]
    })
    .groupby('x', 'y')
    .impute({ v: op.max('v') }, { expand: 'z' })
    .orderby('x', 'y', 'z')
    .reify();

  tableEqual(t, dt, {
    x: ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c'],
    y: [ 1,   1,   1,   1,   2,   2,   2,   3,   3,   3 ],
    z: ['x', 'x', 'y', 'z', 'x', 'y', 'z', 'x', 'y', 'z'],
    v: [ 0,   9,   9,   9,   8,   8,   8,   7,   7,   7 ]
  }, 'impute data');

  t.deepEqual(
    Array.from(dt.groups().keys),
    [0, 0, 0, 0, 1, 1, 1, 2, 2, 2],
    'group keys'
  );

  t.end();
});

tape('impute imputes expanded rows given fixed values', t => {
  const dt = table({
      x: ['a', 'a', 'b', 'c'],
      y: [1, 1, 2, 3],
      z: ['x', 'x', 'y', 'z'],
      v: [0, 9, 8, 7]
    })
    .groupby('x', 'y')
    .impute({ v: op.max('v') }, { expand: { z: ['x', 'y', 'z'] } })
    .orderby('x', 'y', 'z')
    .reify();

  tableEqual(t, dt, {
    x: ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c'],
    y: [ 1,   1,   1,   1,   2,   2,   2,   3,   3,   3 ],
    z: ['x', 'x', 'y', 'z', 'x', 'y', 'z', 'x', 'y', 'z'],
    v: [ 0,   9,   9,   9,   8,   8,   8,   7,   7,   7 ]
  }, 'impute data');

  t.deepEqual(
    Array.from(dt.groups().keys),
    [0, 0, 0, 0, 1, 1, 1, 2, 2, 2],
    'group keys'
  );

  t.end();
});

tape('impute throws on non-existent values column', t => {
  const dt = table({ x: [1, null, NaN, undefined, 3] });

  t.throws(() => dt.impute({ z: () => 1 }));

  t.end();
});

<|start_filename|>src/index.js<|end_filename|>
// export internal class definitions
import Table from './table/table';
import { columnFactory } from './table/column';
import ColumnTable from './table/column-table';
import Transformable from './table/transformable';
import Reducer from './engine/reduce/reducer';
import parse from './expression/parse';
import walk_ast from './expression/ast/walk';
import Query from './query/query';
import { Verb, Verbs } from './query/verb';

export const internal = {
  Table,
  ColumnTable,
  Transformable,
  Query,
  Reducer,
  Verb,
  Verbs,
  columnFactory,
  parse,
  walk_ast
};

// export public API
export { version } from '../package.json';
export { seed } from './util/random';
export { default as Type } from './arrow/arrow-types';
export { default as fromArrow } from './format/from-arrow';
export { default as fromCSV } from './format/from-csv';
export { default as fromFixed } from './format/from-fixed';
export { default as fromJSON } from './format/from-json';
export { load, loadArrow, loadCSV, loadFixed, loadJSON } from './format/load-url';
export { default as toArrow } from './arrow/encode';
export { default as bin } from './helpers/bin';
export { default as escape } from './helpers/escape';
export { default as desc } from './helpers/desc';
export { default as field } from './helpers/field';
export { default as frac } from './helpers/frac';
export { default as names } from './helpers/names';
export { default as rolling } from './helpers/rolling';
export { all, endswith, matches, not, range, startswith } from './helpers/selection';
export { default as agg } from './verbs/helpers/agg';
export { default as op } from './op/op-api';
export { query, queryFrom } from './query/query';
export * from './register';
export * from './table';

<|start_filename|>test/helpers/agg-test.js<|end_filename|>
import tape from 'tape';
import { agg, op, table } from '../../src';

tape('agg computes aggregate values', t => {
  const dt = table({ a: [1, 2, 3, 4] });

  t.deepEqual(
    {
      sum: agg(dt, op.sum('a')),
      max: agg(dt, op.max('a')),
      ext: agg(dt, d => [op.min(d.a), op.max(d.a)])
    },
    { sum: 10, max: 4, ext: [1, 4] },
    'agg helper'
  );

  t.end();
});

<|start_filename|>perf/escape-perf.js<|end_filename|>
const tape = require('tape');
const time = require('./time');
const { floats, sample, strings } = require('./data-gen');
const aq = require('..');

function run(N, nulls, msg) {
  const off = 1;
  const dt = aq.table({
    k: sample(N, strings(10), nulls),
    c: sample(N, strings(100), nulls),
    a: floats(N, -10000, 10000, nulls),
    b: floats(N, -10000, 10000, nulls)
  }).params({ off });

  const opt = { s: 'd.a * d.b + off' };
  const esc = { s: aq.escape(d => d.a * d.b + off) };

  tape(`escape: ${msg}`, t => {
    console.table([ // eslint-disable-line
      { type: 'opt', flat: time(() => dt.derive(opt)) },
      { type: 'esc', flat: time(() => dt.derive(esc)) }
    ]);
    t.end();
  });
}

run(1e6, 0, '1M values');
run(1e6, 0.05, '1M values, 5% nulls');

<|start_filename|>test/format/from-csv-test.js<|end_filename|>
import tape from 'tape';
import tableEqual from '../table-equal';
import fromCSV from '../../src/format/from-csv';

function data() {
  return {
    str: ['a', 'b', 'c'],
    int: [1, 2, 3],
    num: [12.3, 45.6, 78.9],
    bool: [true, null, false],
    date: [new Date('2010-01-01'), new Date('2015-04-05'), new Date('2020-02-29')]
  };
}

const text = [
  'str,int,num,bool,date',
  'a,1,12.3,true,2010-01-01',
  'b,2,45.6,,2015-04-05',
  'c,3,78.9,false,2020-02-29'
];

const tabText = text.map(t => t.split(',').join('\t'));

tape('fromCSV parses delimited text', t => {
  const table = fromCSV(text.join('\n'));
  t.equal(table.numRows(), 3, 'num rows');
  t.equal(table.numCols(), 5, 'num cols');
  tableEqual(t, table, data(), 'csv parsed data');
  t.end();
});

tape('fromCSV infers types', t => {
  function check(msg, values, test) {
    const d = fromCSV('col\n' + values.join('\n')).array('col');
    t.ok(d.every(v => v == null || test(v)), msg);
  }

  check('boolean', [true, false, '', true], v => typeof v === 'boolean');
  check('number', [1, Math.PI, '', 'NaN'], v => typeof v === 'number');
  check('string', ['a', 1, '', 'c'], v => typeof v === 'string');
  check('date', [
    new Date().toISOString(), '',
    new Date(2000, 0, 1).toISOString(),
    new Date(1979, 3, 14, 3, 45).toISOString()
  ], v => v instanceof Date);
  t.end();
});

tape('fromCSV parses delimited text with delimiter', t => {
  const table = fromCSV(tabText.join('\n'), { delimiter: '\t' });
  t.equal(table.numRows(), 3, 'num rows');
  t.equal(table.numCols(), 5, 'num cols');
  tableEqual(t, table, data(), 'csv parsed data with delimiter');
  t.end();
});

tape('fromCSV parses delimited text with header option', t => {
  const table = fromCSV(text.slice(1).join('\n'), { header: false });
  const cols = data();
  const d = {
    col1: cols.str,
    col2: cols.int,
    col3: cols.num,
    col4: cols.bool,
    col5: cols.date
  };
  tableEqual(t, table, d, 'csv parsed data with no header');
  t.end();
});

tape('fromCSV parses delimited text with parse option', t => {
  const table = fromCSV(text.join('\n'), { parse: { str: d => d + d } });
  const d = { ...data(), str: ['aa', 'bb', 'cc'] };
  tableEqual(t, table, d, 'csv parsed data with custom parse');
  t.end();
});

tape('fromCSV parses delimited text with decimal option', t => {
  tableEqual(t,
    fromCSV('a;b\nu;-1,23\nv;4,56e5\nw;', { delimiter: ';', decimal: ',' }),
    { a: ['u', 'v', 'w'], b: [-1.23, 4.56e5, null] },
    'csv parsed data with decimal option'
  );
  t.end();
});

tape('fromCSV parses delimited text with skip options', t => {
  const text = '# line 1\n# line 2\na,b\n1,2\n3,4';
  const data = { a: [1, 3], b: [2, 4] };

  tableEqual(t, fromCSV(text, { skip: 2 }), data,
    'csv parsed data with skip option'
  );

  tableEqual(t, fromCSV(text, { comment: '#' }), data,
    'csv parsed data with comment option'
  );

  tableEqual(t, fromCSV(text, { skip: 1, comment: '#' }), data,
    'csv parsed data with skip and comment options'
  );

  t.end();
});

tape('fromCSV applies parsers regardless of autoType flag', t => {
  const text = 'a,b\r\n00152,01/01/2021\r\n30219,01/01/2021';
  const table = autoType => fromCSV(text, {
    autoType,
    parse: {
      a: v => v,
      b: v => v.split('/').reverse().join('-')
    }
  });
  const data = {
    a: ['00152', '30219'],
    b: ['2021-01-01', '2021-01-01']
  };

  tableEqual(t, table(true), data, 'csv parsed data with autoType true');
  tableEqual(t, table(false), data, 'csv parsed data with autoType false');

  t.end();
});

<|start_filename|>test/helpers/escape-test.js<|end_filename|>
import tape from 'tape';
import tableEqual from '../table-equal';
import { escape, op, query, table } from '../../src';

tape('derive supports escaped functions', t => {
  const dt = table({ a: [1, 2], b: [3, 4] });
  const sq = x => x * x;
  const off = 1;

  tableEqual(t,
    dt.derive({ z: escape(d => sq(d.a) + off) }),
    { a: [1, 2], b: [3, 4], z: [2, 5] },
    'derive data with escape'
  );

  tableEqual(t,
    dt.derive({ z: escape(d => d.a * -d.b + off) }),
    { a: [1, 2], b: [3, 4], z: [-2, -7] },
    'derive data with escape, two columns'
  );

  tableEqual(t,
    dt.params({ foo: 2 })
      .derive({ z: escape((d, $) => sq(d.a) + off + op.abs($.foo)) }),
    { a: [1, 2], b: [3, 4], z: [4, 7] },
    'derive data with escape, op, and params'
  );

  tableEqual(t,
    dt.derive({ z: escape(2) }),
    { a: [1, 2], b: [3, 4], z: [2, 2] },
    'derive data with escaped literal value'
  );

  t.end();
});

tape('filter supports escaped functions', t => {
  const thresh = 5;
  tableEqual(t,
    table({ a: [1, 4, 9], b: [1, 2, 3] }).filter(escape(d => d.a < thresh)),
    { a: [1, 4], b: [1, 2] },
    'filter data with escape'
  );

  t.end();
});

tape('spread supports escaped functions', t => {
  const pair = d => [d.v, d.v * d.v + 1];

  tableEqual(t,
    table({ v: [3, 2, 1] }).spread({ v: escape(pair) }, { as: ['a', 'b'] }),
    { a: [3, 2, 1], b: [10, 5, 2] },
    'spread data with escape'
  );

  t.end();
});

tape('groupby supports escaped functions', t => {
  tableEqual(t,
    table({ v: [3, 2, 1] }).groupby({ g: escape(d => -d.v) }).count(),
    { g: [-3, -2, -1], count: [1, 1, 1] },
    'groupby data with escape'
  );

  t.end();
});

tape('orderby supports escaped functions', t => {
  tableEqual(t,
    table({ v: [1, 2, 3] }).orderby(escape(d => -d.v)),
    { v: [3, 2, 1] },
    'orderby data with escape'
  );

  t.end();
});

tape('aggregate verbs throw for escaped functions', t => {
  t.throws(
    () => table({ v: [1, 2, 3] }).rollup({ v: escape(d => -d.v) }),
    'rollup throws on escaped function'
  );

  t.throws(
    () => table({ g: [1, 2], a: [3, 4] }).pivot('g', { v: escape(d => -d.a) }),
    'pivot throws on escaped function'
  );

  t.end();
});

tape('query serialization throws for escaped functions', t => {
  const sq = d => d.a * d.a;

  t.throws(
    () => query().derive({ z: escape(sq) }).toObject(),
    'query toObject throws on escaped function'
  );

  t.throws(
    () => query().derive({ z: escape(sq) }).toAST(),
    'query toAST throws on escape function'
  );

  t.end();
});

<|start_filename|>test/helpers/names-test.js<|end_filename|>
import tape from 'tape';
import { names, table } from '../../src';

tape('names produces a rename map', t => {
  const dt = table({ x: [1], y: [2], z: [3] });
  const entries = [ ['x', 'a'], ['y', 'b'], ['z', 'c'] ];

  t.deepEqual(
    [
      names('a', 'b', 'c')(dt),
      names(['a', 'b', 'c'])(dt),
      names(['a', 'b'], 'c')(dt),
      names('a', 'b')(dt),
      names('a', 'b', 'c', 'd')(dt)
    ],
    [
      new Map(entries),
      new Map(entries),
      new Map(entries),
      new Map(entries.slice(0, 2)),
      new Map(entries)
    ],
    'names helper'
  );

  t.end();
});

<|start_filename|>test/format/from-fixed-test.js<|end_filename|>
import tape from 'tape';
import tableEqual from '../table-equal';
import fromFixed from '../../src/format/from-fixed';

function data() {
  return {
    str: ['a', 'b', 'c'],
    int: [1, 2, 3],
    num: [12.3, 45.6, 78.9],
    bool: [true, null, false],
    date: [new Date('2010-01-01'), new Date('2015-04-05'), new Date('2020-02-29')]
  };
}

const names = [ 'str', 'int', 'num', 'bool', 'date' ];
const widths = [1, 1, 4, 5, 10];
const positions = [[0, 1], [1, 2], [2, 6], [6, 11], [11, 21]];
const text = [
  'a112.3true 2010-01-01',
  'b245.6     2015-04-05',
  'c378.9false2020-02-29'
];

tape('fromFixed parses fixed width files using positions', t => {
  const table = fromFixed(text.join('\n'), { names, positions });
  t.equal(table.numRows(), 3, 'num rows');
  t.equal(table.numCols(), 5, 'num cols');
  tableEqual(t, table, data(), 'fixed-width parsed data');
  t.end();
});

tape('fromFixed parses fixed width files using widths', t => {
  const table = fromFixed(text.join('\n'), { names, widths });
  t.equal(table.numRows(), 3, 'num rows');
  t.equal(table.numCols(), 5, 'num cols');
  tableEqual(t, table, data(), 'fixed-width parsed data');
  t.end();
});

tape('fromFixed infers types', t => {
  function check(msg, widths, values, test) {
    const d = fromFixed(values.join('\n'), { widths }).array('col1');
    t.ok(d.every(v => v == null || test(v)), msg);
  }

  check('boolean', [5],
    ['true ', 'false', '     ', 'true '],
    v => typeof v === 'boolean'
  );
  check('number', [3],
    ['1  ', '3.14', '   ', 'NaN'],
    v => typeof v === 'number'
  );
  check('string', [1],
    ['a', '1', ' ', 'c'],
    v => typeof v === 'string'
  );
  check('date', [24],
    [
      new Date().toISOString(),
      '                        ',
      new Date(2000, 0, 1).toISOString(),
      new Date(1979, 3, 14, 3, 45).toISOString()
    ],
    v => v instanceof Date
  );
  t.end();
});

tape('fromFixed parses text with parse option', t => {
  const table = fromFixed(text.join('\n'), { names, widths, parse: { str: d => d + d } });
  const d = { ...data(), str: ['aa', 'bb', 'cc'] };
  tableEqual(t, table, d, 'fixed-width parsed data with custom parse');
  t.end();
});

tape('fromFixed parses text with decimal option', t => {
  tableEqual(t,
    fromFixed(
      'u -1,23\nv4,56e5\nw',
      { decimal: ',', widths: [1, 6], names: ['a', 'b'] }
    ),
    { a: ['u', 'v', 'w'], b: [-1.23, 4.56e5, null] },
    'fixed-width parsed data with decimal option'
  );
  t.end();
});

tape('fromFixed parses text with skip options', t => {
  const text = '# line 1\n# line 2\n12\n34';
  const data = { a: [1, 3], b: [2, 4] };
  const names = ['a', 'b'];
  const widths = [1, 1];

  tableEqual(t, fromFixed(text, { names, widths, skip: 2 }), data,
    'fixed-width parsed data with skip option'
  );

  tableEqual(t, fromFixed(text, { names, widths, comment: '#' }), data,
    'fixed-width parsed data with comment option'
  );

  tableEqual(t, fromFixed(text, { names, widths, skip: 1, comment: '#' }), data,
    'fixed-width parsed data with skip and comment options'
  );

  t.end();
});

<|start_filename|>test/op/row-object-test.js<|end_filename|>
import tape from 'tape';
import { op, table } from '../../src';

tape('op.row_object generates objects with row data', t => {
  const dt = table({ a: [1, 2], b: [3, 4] });

  t.deepEqual(
    dt.derive({ row: op.row_object() }).array('row'),
    dt.objects(),
    'row objects, outside function context'
  );

  t.deepEqual(
    dt.derive({ row: () => op.row_object() }).array('row'),
    dt.objects(),
    'row objects, inside function context'
  );

  t.deepEqual(
    dt.derive({ row: op.row_object('a') }).array('row'),
    dt.objects({ columns: 'a' }),
    'row objects, column names outside function context'
  );

  t.deepEqual(
    dt.derive({ row: () => op.row_object('a' + '') }).array('row'),
    dt.objects({ columns: 'a' }),
    'row objects, column names inside function context'
  );

  t.deepEqual(
    dt.derive({ row: op.row_object(0) }).array('row'),
    dt.objects({ columns: 'a' }),
    'row objects, column indices outside function context'
  );

  t.deepEqual(
    dt.derive({ row: () => op.row_object(0 + 0) }).array('row'),
    dt.objects({ columns: 'a' }),
    'row objects, column indices inside function context'
  );

  t.end();
});


<|start_filename|>test/op/object-test.js<|end_filename|>
import tape from 'tape';
import { op } from '../../src';

tape('op.has checks if a object/map/set has a key', t => {
  t.deepEqual(
    [
      op.has({ a: 1}, 'a'),
      op.has({ a: 1}, 'b'),
      op.has(new Map([['a', 1]]), 'a'),
      op.has(new Map([['a', 1]]), 'b'),
      op.has(new Set(['a']), 'a'),
      op.has(new Set(['a']), 'b'),
      op.has(null, 'a'),
      op.has(undefined, 'a'),
      op.has(NaN, 'a')
    ],
    [
      true, false,
      true, false,
      true, false,
      false,
      false,
      false
    ],
    'has'
  );
  t.end();
});

tape('op.keys returns object/map keys', t => {
  t.deepEqual(
    [
      op.keys({ a: 1}),
      op.keys(new Map([['a', 1]])),
      op.keys(null),
      op.keys(undefined),
      op.keys(NaN)
    ],
    [ ['a'], ['a'], [], [], [] ],
    'keys'
  );
  t.end();
});

tape('op.values returns object/map/set values', t => {
  t.deepEqual(
    [
      op.values({ a: 1}),
      op.values(new Map([['a', 1]])),
      op.values(new Set(['a'])),
      op.values(null),
      op.values(undefined),
      op.values(NaN)
    ],
    [ [1], [1], ['a'], [], [], [] ],
    'values'
  );
  t.end();
});

tape('op.entries returns object/map/set entries', t => {
  t.deepEqual(
    [
      op.entries({ a: 1}),
      op.entries(new Map([['a', 1]])),
      op.entries(new Set(['a'])),
      op.entries(null),
      op.entries(undefined),
      op.entries(NaN)
    ],
    [ [['a', 1]], [['a', 1]], [['a', 'a']], [], [], [] ],
    'entries'
  );
  t.end();
});

tape('op.object constructs an object from iterable entries', t => {
  t.deepEqual(
    [
      op.object([['a', 1]]),
      op.object(new Map([['b', 2]])),
      op.object(null),
      op.object(undefined),
      op.object(NaN)
    ],
    [ {a: 1}, {b: 2}, undefined, undefined, undefined ],
    'object'
  );
  t.end();
});

<|start_filename|>src/format/to-html.js<|end_filename|>
import ColumnTable from '../table/column-table'; // eslint-disable-line no-unused-vars

import formatValue from './value';
import { columns, formats, scan } from './util';
import isFunction from '../util/is-function';
import mapObject from '../util/map-object';

/**
 * Null format function.
 * @callback NullFormat
 * @param {null|undefined} [value] The value to format.
 * @return {string} The formatted HTML string.
 */

/**
 * CSS style function.
 * @callback StyleFunction
 * @param {string} name The column name.
 * @param {number} row The table row index.
 * @return {string} A CSS style string.
 */

/**
 * CSS style options.
 * @typedef {Object.<string, string | StyleFunction>} StyleOptions
 */

/**
 * Options for HTML formatting.
 * @typedef {object} HTMLFormatOptions
 * @property {number} [limit=Infinity] The maximum number of rows to print.
 * @property {number} [offset=0] The row offset indicating how many initial rows to skip.
 * @property {import('./util').ColumnSelectOptions} [columns] Ordered list
 *  of column names to include. If function-valued, the function should
 *  accept a table as input and return an array of column name strings.
 * @property {import('./util').ColumnAlignOptions} [align] Object of column
 *  alignment options. The object keys should be column names. The object
 *  values should be aligment strings, one of 'l' (left), 'c' (center), or
 *  'r' (right). If specified, these override automatically inferred options.
 * @property {import('./util').ColumnFormatOptions} [format] Object of column
 *  format options. The object keys should be column names. The object values
 *  should be formatting functions or specification objects. If specified,
 *  these override automatically inferred options.
 * @property {NullFormat} [null] Format function for null or undefined values.
 *  If specified, this function will be invoked with the null or undefined
 *  value as the sole input, and the return value will be used as the HTML
 *  output for the value.
 * @property {StyleOptions} [style] CSS styles to include in HTML output.
 *  The object keys should be HTML table tag names: 'table', 'thead',
 *  'tbody', 'tr', 'th', or 'td'. The object values should be strings of
 *  valid CSS style directives (such as "font-weight: bold;") or functions
 *  that take a column name and row as inputs and return a CSS string.
 * @property {number} [maxdigits=6] The maximum number of fractional digits
 *  to include when formatting numbers. This option is passed to the format
 *  inference method and is overridden by any explicit format options.
 */

/**
 * Format a table as an HTML table string.
 * @param {ColumnTable} table The table to format.
 * @param {HTMLFormatOptions} options The formatting options.
 * @return {string} An HTML table string.
 */
export default function(table, options = {}) {
  const names = columns(table, options.columns);
  const { align, format } = formats(table, names, options);
  const style = styles(options);
  const nullish = options.null;

  const alignValue = a => a === 'c' ? 'center' : a === 'r' ? 'right' : 'left';
  const escape = s => s.replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
  const baseFormat = (value, opt) => escape(formatValue(value, opt));
  const formatter = nullish
    ? (value, opt) => value == null ? nullish(value) : baseFormat(value, opt)
    : baseFormat;

  let r = -1;
  let idx = -1;

  const tag = (tag, name, shouldAlign) => {
    const a = shouldAlign ? alignValue(align[name]) : '';
    const s = style[tag] ? (style[tag](name, idx, r) || '') : '';
    const css = (a ? (`text-align: ${a};` + (s ? ' ' : '')) : '') + s;
    return `<${tag}${css ? ` style="${css}"` : ''}>`;
  };

  let text = tag('table')
    + tag('thead')
    + tag('tr', r)
    + names.map(name => `${tag('th', name, 1)}${name}</th>`).join('')
    + '</tr></thead>'
    + tag('tbody');

  scan(table, names, options.limit, options.offset, {
    row(row) {
      r = row;
      text += (++idx ? '</tr>' : '') + tag('tr');
    },
    cell(value, name) {
      text += tag('td', name, 1)
        + formatter(value, format[name])
        + '</td>';
    }
  });

  return text + '</tr></tbody></table>';
}

function styles(options) {
  return mapObject(
    options.style,
    value => isFunction(value) ? value : () => value
  );
}

<|start_filename|>test/verbs/spread-test.js<|end_filename|>
import tape from 'tape';
import tableEqual from '../table-equal';
import { op, table } from '../../src';

tape('spread produces multiple columns from arrays', t => {
  const data = {
    text: ['foo bar bop', 'foo', 'bar baz', 'baz bop']
  };

  const dt = table(data).spread(
    { split: d => op.split(d.text, ' ') },
    { limit: 2 }
  );

  tableEqual(t, dt, {
    ...data,
    split_1: [ 'foo', 'foo', 'bar', 'baz' ],
    split_2: [ 'bar', undefined, 'baz', 'bop' ]
  }, 'spread data');
  t.end();
});

tape('spread supports column name argument', t => {
  const data = {
    list: [['foo', 'bar', 'bop'], ['foo'], ['bar', 'baz'], ['baz', 'bop']]
  };

  const dt = table(data).spread('list', { drop: false, limit: 2 });

  tableEqual(t, dt, {
    ...data,
    list_1: [ 'foo', 'foo', 'bar', 'baz' ],
    list_2: [ 'bar', undefined, 'baz', 'bop' ]
  }, 'spread data');
  t.end();
});

tape('spread supports column index argument', t => {
  const data = {
    list: [['foo', 'bar', 'bop'], ['foo'], ['bar', 'baz'], ['baz', 'bop']]
  };

  const dt = table(data).spread(0, { limit: 2 });

  tableEqual(t, dt, {
    list_1: [ 'foo', 'foo', 'bar', 'baz' ],
    list_2: [ 'bar', undefined, 'baz', 'bop' ]
  }, 'spread data');
  t.end();
});

tape('spread supports multiple input columns', t => {
  const data = {
    a: [['foo', 'bar', 'bop'], ['foo'], ['bar', 'baz'], ['baz', 'bop']],
    b: [['baz', 'bop'], ['bar', 'baz'], ['foo'], ['foo', 'bar', 'bop']]
  };

  const dt = table(data).spread(['a', 'b'], { limit: 2 });

  tableEqual(t, dt, {
    a_1: [ 'foo', 'foo', 'bar', 'baz' ],
    a_2: [ 'bar', undefined, 'baz', 'bop' ],
    b_1: [ 'baz', 'bar', 'foo', 'foo' ],
    b_2: [ 'bop', 'baz', undefined, 'bar' ]
  }, 'spread data');
  t.end();
});

tape('spread supports as option with single column input', t => {
  const data = {
    list: [['foo', 'bar', 'bop'], ['foo'], ['bar', 'baz'], ['baz', 'bop']]
  };

  const dt = table(data).spread('list', { as: ['bip', 'bop'] });

  tableEqual(t, dt, {
    bip: [ 'foo', 'foo', 'bar', 'baz' ],
    bop: [ 'bar', undefined, 'baz', 'bop' ]
  }, 'spread data with as');
  t.end();
});

tape('spread ignores as option with multi column input', t => {
  const data = {
    key: ['a', 'b', 'c', 'd'],
    a: [['foo', 'bar', 'bop'], ['foo'], ['bar', 'baz'], ['baz', 'bop']],
    b: [['baz', 'bop'], ['bar', 'baz'], ['foo'], ['foo', 'bar', 'bop']]
  };

  const dt = table(data).spread(['a', 'b'], { limit: 2, as: ['bip', 'bop'] });

  tableEqual(t, dt, {
    key: ['a', 'b', 'c', 'd'],
    a_1: [ 'foo', 'foo', 'bar', 'baz' ],
    a_2: [ 'bar', undefined, 'baz', 'bop' ],
    b_1: [ 'baz', 'bar', 'foo', 'foo' ],
    b_2: [ 'bop', 'baz', undefined, 'bar' ]
  }, 'spread data with as');
  t.end();
});

tape('spread handles arrays of varying length', t => {
  const data1 = {
    u: [
      ['A', 'B', 'C'],
      ['D', 'E']
    ]
  };
  const data2 = {
    u: data1.u.slice().reverse()
  };
  const obj = [
    { u_1: 'A', u_2: 'B', u_3: 'C' },
    { u_1: 'D', u_2: 'E', u_3: undefined }
  ];

  t.deepEqual(
    table(data1).spread('u').objects(),
    obj,
    'spread data, larger first'
  );

  t.deepEqual(
    table(data2).spread('u').objects(),
    obj.slice().reverse(),
    'spread data, smaller first'
  );

  t.end();
});
