Home Reference Source

src/index.js

// @flow
import jschardet from 'jschardet'
import iconv from 'iconv-lite'

// skip iconv warning
iconv.skipDecodeWarning = true

/**
 * @desc CSV
 * @export
 * @class CSV
 */
export default class CSV {
  /**
   * @desc schema
   * @type {Array<Schema>}
   * @memberof CSV
   */
  schema: Array<Schema>;
  /**
   * @desc data buffer
   * @type {?Buffer}
   * @memberof CSV
   */
  buffer: ?Buffer;
  /**
   * @desc encoding
   * @type {string}
   * @memberof CSV
   */
  encoding: string;
  /**
   * @desc csv data string
   * @type {?string}
   * @memberof CSV
   */
  data: string;
  /**
   * @desc generate csv with header
   * @type {boolean}
   * @memberof CSV
   */
  withHeader: boolean;
  /**
   * @desc Creates an instance of CSV.
   * @param {CSVOption} option option
   * @param {Array<Schema>} option.schema schema
   * @param {string} [option.encoding=utf8] encoding
   * @param {boolean} [option.widthHeader=true] with header
   * @memberof CSV
   */
  constructor (option: CSVOption) {
    const { schema, encoding = 'utf8', withHeader = true } = option
    this.checkSchema((schema))
    this.checkEncoding(encoding)
    this.schema = schema
    this.encoding = encoding
    this.data = ''
    this.buffer = null
    this.withHeader = !!withHeader
  }

  /**
   * @desc check schema
   * @throws {TypeError} Invalid schema presented
   * @memberof CSV
   */
  checkSchema = (schema: Array<Schema>) => {
    const validSchema = schema.every(item => item.key)
    if (!validSchema) {
      throw new TypeError('Invalid schema presented')
    }
  }

  /**
   * @desc check encoding
   * @param {string} encoding
   * @throws {TypeError} Encoding Not Support
   * @memberof CSV
   */
  checkEncoding = (encoding: string): void => {
    const validEncoding = iconv.encodingExists(encoding)
    if (!validEncoding) {
      throw new TypeError(`Encoding Error: ${encoding} is not supported`)
    }
  }

  /**
   * @desc find schema by key
   * @param {string} key
   * @return {Schema} schema
   * @memberof CSV
   */
  findSchemaByKey = (key: string): Schema => {
    return this.schema.find((item) => item.key === key) || {
      key,
      type: 'string',
      label: key
    }
  }

  /**
   * @desc format value
   * @param {any} value
   * @param {string} key
   * @return {string|number} formated value
   * @memberof CSV
   */
  format = (value: any, key: string): string | number => {
    const schema = this.findSchemaByKey(key)
    const { formatter = {}, type } = schema
    const { csv } = formatter
    if (csv && typeof csv === 'function') {
      return csv(value, key)
    } else {
      switch (type) {
        case 'number':
          return parseFloat(value)
        case 'date':
          return new Date(value).toDateString()
        case 'boolean':
          return (!!value).toString()
        case 'string':
        default:
          return value
      }
    }
  }

  /**
   * @desc encode str to buffer with specific encoding
   * @param {string} str
   * @param {string} encoding
   * @return {Buffer} encoded buffer
   * @memberof CSV
   */
  encode = (str: string): Buffer => {
    this.checkEncoding(this.encoding)
    return iconv.encode(str, this.encoding)
  }

  /**
   * @desc decode buffer or string to string with specific encoding
   * @param {Buffer|string} buf
   * @param {string} encoding
   * @return {string} decoded string
   * @memberof CSV
   */
  decode = (buf: Buffer | string): string => {
    this.checkEncoding(this.encoding)
    return iconv.decode(buf, this.encoding)
  }

  /**
   * @desc convert json to csv data string
   * @param {Array<Object>} items
   * @param {CustomOption} option
   * @param {string} option.encoding
   * @return {string} csv data string
   * @memberof CSV
   */
  convert = (items: Array<{[x: string]: any}>, option?: CustomOption = {}): string => {
    this.encoding = option.encoding || 'utf8'
    const columns = this.schema
    const csvArray = []
    const header = []
    const keys = []

    columns.forEach((column) => {
      keys.push(column.key)
      header.push('"' + (column.label || column.key) + '"')
    })

    if (this.withHeader) {
      csvArray.push(header)
    }

    items.forEach((item) => {
      csvArray.push(keys.map((key) => '"' + this.format(item[key], key) + '"').join(','))
    })

    const str = csvArray.join('\n')

    this.buffer = this.encode(str)
    this.data = this.decode(this.buffer)
    return this.data
  }

  /**
   * @desc parse buffer or string to csv data string
   * @param {Buffer|string} buf
   * @param {CustomOption} option
   * @param {string} option.encoding
   * @return {string} parsed csv data string
   * @memberof CSV
   */
  parse = (buf: Buffer, option?: CustomOption = {}): string => {
    if (Array.isArray(buf)) {
      this.data = this.convert(buf, option)
    } else {
      try {
        const res = jschardet.detect(buf)
        this.encoding = res.encoding
        this.data = this.decode(buf)
      } catch (error) {
        throw new Error('Parse failed, please check input data')
      }
    }
    return this.data
  }

  /**
   * @desc get file dataURL
   * @return {string} dataURL
   * @memberof CSV
   */
  getDataURL = (): string => {
    return this.buffer ? `data:text/csv;base64,${this.buffer.toString('base64')}` : ''
  }

  /**
   * @desc transform parsed data to json
   * @return {Array<Object>} json
   * @memberof CSV
   */
  toJSON = (): Array<{[x: string]: any}> => {
    const columns = this.schema
    if (!this.data) {
      return []
    } else {
      const data = this.data.replace(/\\r\\n|\\r/g, '\n').replace(/;|\\t|\|\^/g, ',')
      let temp = data.split('\n')
      if (this.withHeader) {
        temp.shift()
      }
      // remove empty line
      temp = temp.filter(temp => temp)
      temp = temp.map((str: string) => {
        const item = {}
        const arr = str.split(',')
        arr.map((s, index) => {
          s = s.substring(1, s.length - 1)
          const schema = columns[index]
          const { formatter = {}, type, key } = schema
          const { json } = formatter
          if (json && typeof json === 'function') {
            item[key] = json(s, key)
          } else {
            switch (type) {
              case 'number':
                item[key] = parseFloat(s)
                break
              case 'boolean':
                let val
                if (s === 'true') {
                  val = true
                } else if (s === 'false') {
                  val = false
                } else {
                  val = !!s
                }
                item[key] = val
                break
              case 'date':
                item[key] = new Date(s)
                break
              case 'string':
              default:
                item[key] = s
            }
          }
        })
        return item
      })
      return temp
    }
  }

  /**
   * @desc to string
   * @return {string} csv data string
   * @memberof CSV
   */
  toString = (): string => {
    return this.data
  }
}