WDataCsv.mjs

import { Readable } from 'stream'
import fs from 'fs'
import csvParse from 'csv-parser'
import stripBom from 'strip-bom-stream'
import get from 'lodash-es/get.js'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isstr from 'wsemi/src/isstr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import replace from 'wsemi/src/replace.mjs'
import cstr from 'wsemi/src/cstr.mjs'
import genPm from 'wsemi/src/genPm.mjs'
import ltdtkeysheads2mat from 'wsemi/src/ltdtkeysheads2mat.mjs'


/**
 * 解析CSV字串
 *
 * @param {String} c 輸入CSV字串
 * @return {Promise} 回傳Promise,resolve回傳ltdt(各數據列為物件陣列),reject回傳錯誤訊息
 * @example
 *
 * import wdc from './src/WDataCsv.mjs'
 *
 * let fp = './g-test-in.csv'
 *
 * let c = fs.readFileSync(fp, 'utf8')
 *
 * await wdc.parseCsv(c)
 *     .then((ltdt) => {
 *         console.log(ltdt)
 *         // => [ { NAME: 'Daffy Duck', AGE: '24' }, { NAME: 'Bugs 邦妮', AGE: '22' } ]
 *     })
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
async function parseCsv(inp) {
    let res = []

    let pm = genPm()

    //check
    if (!isestr(inp)) {
        return Promise.reject(`inp is not an effective string`)
    }

    Readable.from([inp])
        .pipe(csvParse())
        .on('data', (chunk) => {
            // console.log('data', chunk)
            res.push(chunk)
        })
        .on('end', () => {
            pm.resolve(res)
        })
        .on('error', (err) => {
            console.log(err)
            pm.reject(err)
        })

    return pm
}


/**
 * 讀取CSV檔,自動清除BOM
 *
 * @param {String} fp 輸入檔案位置字串
 * @return {Promise} 回傳Promise,resolve回傳ltdt(各數據列為物件陣列),reject回傳錯誤訊息
 * @example
 *
 * import wdc from './src/WDataCsv.mjs'
 *
 * let fp = './g-test-in.csv'
 *
 * wdc.readCsv(fp)
 *     .then((ltdt) => {
 *         console.log(ltdt)
 *         // => [ { NAME: 'Daffy Duck', AGE: '24' }, { NAME: 'Bugs 邦妮', AGE: '22' } ]
 *     })
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
async function readCsv(fp) {
    let res = []

    let pm = genPm()

    //check
    if (!fsIsFile(fp)) {
        return Promise.reject(`fp[${fp}] is not exist`)
    }

    fs.createReadStream(fp)
        .pipe(stripBom())
        .pipe(csvParse())
        .on('data', (chunk) => {
            // console.log('data', chunk)
            res.push(chunk)
        })
        .on('end', () => {
            pm.resolve(res)
        })
        .on('error', (err) => {
            console.log(err)
            pm.reject(err)
        })

    return pm
}


/**
 * 輸出數據至CSV檔案
 *
 * @param {String} fp 輸入檔案位置字串
 * @param {Array} data 輸入數據陣列,為mat或ltdt格式
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.mode='ltdt'] 輸入數據格式字串,可選ltdt或mat,預設ltdt
 * @param {Array} [opt.keys=[]] 輸入指定欲輸出鍵值陣列,預設[]
 * @param {Object} [opt.kphead={}] 輸入指定鍵值轉換物件,預設{}
 * @param {Boolean} [opt.bom=true] 輸入是否添加開頭BOM符號,預設true
 * @return {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
 * @example
 *
 * import wdc from './src/WDataCsv.mjs'
 *
 * let ltdt = [{ name: '大福 Duck', value: 2.4 }, { name: 'Bugs 邦妮', value: '2.2' }]
 *
 * let fp = './g-test-out.csv'
 *
 * wdc.writeCsv(fp, ltdt)
 *     .then((res) => {
 *         console.log(res)
 *         // => finish
 *     })
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
async function writeCsv(fp, data, opt = {}) {
    let mat

    //mode
    let mode = get(opt, 'mode')
    if (mode !== 'ltdt' && mode !== 'mat') {
        mode = 'ltdt'
    }

    //bom
    let bom = get(opt, 'bom', true)

    if (mode === 'mat') {

        //save
        mat = data

    }
    else if (mode === 'ltdt') {
        try {

            //save
            let ltdt = data

            //keys
            let keys = get(opt, 'keys')

            //kphead
            let kphead = get(opt, 'kphead')

            //ltdtkeysheads2mat
            mat = ltdtkeysheads2mat(ltdt, keys, kphead)

        }
        catch (err) {
            console.log(err)
            return Promise.reject(err.toString())
        }
    }

    //stream寫入: 逐row編碼後write, 撞backpressure時等drain,
    //避免在記憶體組出整段CSV字串撞V8 MAX_STRING_LENGTH (~512 MB)
    let pm = genPm()
    let ws = fs.createWriteStream(fp, { encoding: 'utf8' })
    ws.on('error', (err) => {
        console.log(err)
        pm.reject(err.toString())
    })
    ws.on('finish', () => {
        pm.resolve('finish')
    })

    let core = async () => {

        //BOM
        if (bom) {
            if (!ws.write('')) {
                await new Promise((r) => {
                    ws.once('drain', r)
                })
            }
        }

        //rows (escape邏輯與原getCsvStrFromData內getCsv一致, 維持byte-for-byte相容)
        for (let row of mat) {
            let cr = []
            for (let value of row) {
                if (isstr(value)) {
                    value = replace(value, '\r\n', '')
                    value = replace(value, '\r', '')
                    value = replace(value, '\n', '')
                    value = `"${value}"`
                }
                else if (isbol(value)) {
                    value = value ? 'true' : 'false'
                }
                else {
                    value = cstr(value)
                }
                cr.push(value)
            }
            let line = cr.join(',') + '\r\n'
            if (!ws.write(line)) {
                await new Promise((r) => {
                    ws.once('drain', r)
                })
            }
        }

        ws.end()
    }

    core()
        .catch((err) => {
            console.log(err)
            ws.destroy()
            pm.reject(err.toString())
        })

    return pm
}


/**
 * 讀寫CSV檔
 *
 * @return {Object} 回傳物件,其內有readCsv與writeCsv函式
 * @example
 *
 * import wdc from './src/WDataCsv.mjs'
 *
 * let fpIn = './g-test-in.csv'
 * wdc.readCsv(fpIn)
 *     .then((ltdtIn) => {
 *         console.log(ltdtIn)
 *         // => [ { NAME: 'Daffy Duck', AGE: '24' }, { NAME: 'Bugs 邦妮', AGE: '22' } ]
 *     })
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 * let ltdtOut = [{ name: '大福 Duck', value: 2.4 }, { name: 'Bugs 邦妮', value: '2.2' }]
 * let fpOut = './g-test-out.csv'
 * wdc.writeCsv(fpOut, ltdtOut)
 *     .then((res) => {
 *         console.log(res)
 *         // => finish
 *     })
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
let WDataCsv = {
    parseCsv,
    readCsv,
    writeCsv,
}


export default WDataCsv