← back to Rentv Adintel

src/export/csv.js

82 lines

'use strict';
/**
 * Pure-Node CSV writer — RFC 4180 compliant quoting/escaping.
 *
 * - All fields containing commas, double-quotes, or newlines are quoted.
 * - Double-quotes inside a quoted field are escaped as "".
 * - Null/undefined rendered as empty string.
 * - Numbers and booleans serialized as strings.
 * - CRLF line endings (RFC 4180).
 *
 * @module src/export/csv
 */

/**
 * Serialize one field value to RFC 4180.
 * @param {*} value
 * @returns {string}
 */
function escapeField(value) {
  if (value === null || value === undefined) return '';
  let str = String(value);
  // Must quote if contains comma, double-quote, CR, LF, or leading/trailing space
  if (/[",\r\n]/.test(str) || str !== str.trim()) {
    str = '"' + str.replace(/"/g, '""') + '"';
  }
  return str;
}

/**
 * Serialize one row (array of values) to a CSV line with CRLF.
 * @param {Array} row
 * @returns {string}
 */
function rowToLine(row) {
  return row.map(escapeField).join(',') + '\r\n';
}

/**
 * Convert an array of objects to a CSV string.
 * Column order is determined by the keys of the first row (or the explicit
 * `columns` parameter).
 *
 * @param {Object[]} rows
 * @param {Object} [opts]
 * @param {string[]} [opts.columns]  - explicit column order; defaults to Object.keys(rows[0])
 * @param {boolean}  [opts.header]   - include header row (default true)
 * @returns {string} UTF-8 CSV string (BOM prefix for Excel compatibility)
 */
function toCsv(rows, opts = {}) {
  const { columns, header = true } = opts;

  if (!rows || rows.length === 0) {
    const cols = columns || [];
    const headerLine = header && cols.length ? rowToLine(cols) : '';
    return '' + headerLine; // BOM for Excel
  }

  const cols = columns || Object.keys(rows[0]);
  const lines = [];

  if (header) lines.push(rowToLine(cols));

  for (const row of rows) {
    lines.push(rowToLine(cols.map((c) => row[c])));
  }

  // BOM (U+FEFF) so Excel on Windows recognizes UTF-8 correctly
  return '' + lines.join('');
}

/**
 * Convert an array of objects to a Buffer (UTF-8 with BOM).
 * @param {Object[]} rows
 * @param {Object} [opts]
 * @returns {Buffer}
 */
function toCsvBuffer(rows, opts = {}) {
  return Buffer.from(toCsv(rows, opts), 'utf8');
}

module.exports = { toCsv, toCsvBuffer, escapeField, rowToLine };