NaturalCycles/nodejs-lib

View on GitHub
src/csv/csvWriter.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
// Inspired by: https://github.com/ryu1kn/csv-writer/

import { _assert, AnyObject } from '@naturalcycles/js-lib'

export interface CSVWriterConfig {
  /**
   * Default: comma
   */
  delimiter?: string

  /**
   * Array of columns
   */
  columns?: string[]

  /**
   * Default: true
   */
  includeHeader?: boolean
}

export class CSVWriter {
  constructor(cfg: CSVWriterConfig) {
    this.cfg = {
      delimiter: ',',
      includeHeader: true,
      ...cfg,
    }
  }

  cfg: CSVWriterConfig & { delimiter: string }

  writeRows(rows: AnyObject[]): string {
    let s = ''

    // Detect columns based on content, if not defined upfront
    this.cfg.columns ||= arrayToCSVColumns(rows)

    if (this.cfg.includeHeader && rows.length) {
      s += this.writeHeader() + '\n'
    }
    return s + rows.map(row => this.writeRow(row)).join('\n')
  }

  writeHeader(): string {
    _assert(this.cfg.columns, 'CSVWriter cannot writeHeader, because columns were not provided')
    return this.cfg.columns.map(col => this.quoteIfNeeded(col)).join(this.cfg.delimiter)
  }

  writeRow(row: AnyObject): string {
    _assert(this.cfg.columns, 'CSVWriter cannot writeRow, because columns were not provided')
    return this.cfg.columns
      .map(col => this.quoteIfNeeded(String(row[col] ?? '')))
      .join(this.cfg.delimiter)
  }

  private quoteIfNeeded(s: string): string {
    return this.shouldQuote(s) ? this.quote(s) : s
  }

  private quote(s: string): string {
    return `"${s.replaceAll('"', '""')}"`
  }

  private shouldQuote(s: string): boolean {
    return s.includes(this.cfg.delimiter) || s.includes('"') || s.includes('\n') || s.includes('\r')
  }
}

export function arrayToCSVString(arr: AnyObject[], cfg: CSVWriterConfig = {}): string {
  const writer = new CSVWriter(cfg)
  return writer.writeRows(arr)
}

/**
 * Iterates over the whole array and notes all possible columns.
 */
export function arrayToCSVColumns(arr: AnyObject[]): string[] {
  const cols = new Set<string>()
  arr.forEach(row => Object.keys(row).forEach(col => cols.add(col)))
  return [...cols]
}