w-data-csv 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 07 2026 23:52:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 15 2026 17:02:41 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "w-data-csv",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "main": "dist/w-data-csv.umd.js",
5
5
  "dependencies": {
6
6
  "csv-parser": "^3.2.1",
7
7
  "strip-bom-stream": "^5.0.0",
8
- "wsemi": "^1.8.68"
8
+ "wsemi": "^1.8.70"
9
9
  },
10
10
  "devDependencies": {
11
11
  "w-package-tools": "^1.1.12"
package/src/WDataCsv.mjs CHANGED
@@ -1,9 +1,11 @@
1
- import { Readable } from 'stream'
1
+ import { Readable, Writable } from 'stream'
2
+ import { pipeline } from 'stream/promises'
2
3
  import fs from 'fs'
3
4
  import csvParse from 'csv-parser'
4
5
  import stripBom from 'strip-bom-stream'
5
6
  import get from 'lodash-es/get.js'
6
7
  import fsIsFile from 'wsemi/src/fsIsFile.mjs'
8
+ import haskey from 'wsemi/src/haskey.mjs'
7
9
  import isestr from 'wsemi/src/isestr.mjs'
8
10
  import isstr from 'wsemi/src/isstr.mjs'
9
11
  import isbol from 'wsemi/src/isbol.mjs'
@@ -13,11 +15,89 @@ import genPm from 'wsemi/src/genPm.mjs'
13
15
  import ltdtkeysheads2mat from 'wsemi/src/ltdtkeysheads2mat.mjs'
14
16
 
15
17
 
18
+ //csv-parser可透傳之選項
19
+ //排除raw(其值回傳Buffer, 破壞ltdt與mat之字串契約)與outputByteOffset(其改變回傳形狀為{row,byteOffset})
20
+ let ksOptCsvParse = [
21
+ 'headers', 'skipLines', 'skipComments', 'separator', 'quote',
22
+ 'escape', 'newline', 'strict', 'mapHeaders', 'mapValues', 'maxRowBytes',
23
+ ]
24
+
25
+
26
+ //genOptCsvParse, 自opt取出csv-parser可用選項, 未給者不傳入以維持csv-parser之預設
27
+ let genOptCsvParse = (opt) => {
28
+
29
+ //mode
30
+ let mode = get(opt, 'mode')
31
+ if (mode !== 'ltdt' && mode !== 'mat') {
32
+ mode = 'ltdt'
33
+ }
34
+
35
+ //透傳
36
+ let o = {}
37
+ for (let k of ksOptCsvParse) {
38
+ if (haskey(opt, k)) {
39
+ o[k] = opt[k]
40
+ }
41
+ }
42
+
43
+ //headers給true會使csv-parser之headers成為布林值, 各列鍵將全為_0,_1...之溢出鍵而成壞資料, 故一律視為未給
44
+ if (o.headers === true) {
45
+ delete o.headers
46
+ }
47
+
48
+ //mat模式固定關閉標頭, 否則第1列會被取為標頭
49
+ //且可避免headers給陣列時, 超出標頭數之欄位另生_N溢出鍵
50
+ if (mode === 'mat') {
51
+ o.headers = false
52
+ }
53
+
54
+ return { mode, optCsvParse: o }
55
+ }
56
+
57
+
58
+ //getRows, 以pipeline串接各段stream, 使來源與中段stream之錯誤皆能被捕獲
59
+ let getRows = async (rs, opt) => {
60
+
61
+ let { mode, optCsvParse } = genOptCsvParse(opt)
62
+
63
+ let res = []
64
+
65
+ let ws = new Writable({
66
+ objectMode: true,
67
+ write(row, enc, cb) {
68
+
69
+ //mat模式時headers必為false, csv-parser依各列實際欄數自生0,1,2...之整數字串鍵,
70
+ //物件之整數字串鍵本就依數值升序排列, 故直接取值即為正確順序
71
+ res.push(mode === 'mat' ? Object.values(row) : row)
72
+
73
+ cb()
74
+ },
75
+ })
76
+
77
+ await pipeline(rs, stripBom(), csvParse(optCsvParse), ws)
78
+
79
+ return res
80
+ }
81
+
82
+
16
83
  /**
17
84
  * 解析CSV字串
18
85
  *
19
- * @param {String} c 輸入CSV字串
20
- * @return {Promise} 回傳Promise,resolve回傳ltdt(各數據列為物件陣列),reject回傳錯誤訊息
86
+ * @param {String} inp 輸入CSV字串
87
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
88
+ * @param {String} [opt.mode='ltdt'] 輸入回傳數據格式字串,可選ltdt(各數據列為物件)或mat(各數據列為陣列),預設ltdt
89
+ * @param {Boolean|Array} [opt.headers] 輸入標頭設定,給false代表無標頭列,給陣列代表無標頭列且以此陣列為欄名,不給則取第1列為標頭。因csv-parser於給true時會使各列成為_0,_1等溢出鍵之壞數據,故給true時視為不給。另mode為mat時本設定固定為false
90
+ * @param {Number} [opt.skipLines] 輸入解析前跳過列數整數。因csv-parser之註解列不計入行號,故與skipComments併用時計數基準會位移,兩者不建議同時使用
91
+ * @param {Boolean|String} [opt.skipComments] 輸入忽略註解列設定,給true代表跳過#開頭之列,給字串代表以該字元為註解符
92
+ * @param {String} [opt.separator] 輸入欄位分隔字元,因csv-parser僅取首個byte,故須為單字元
93
+ * @param {String} [opt.quote] 輸入引號字元,因csv-parser僅取首個byte,故須為單字元
94
+ * @param {String} [opt.escape] 輸入逃逸字元,因csv-parser僅取首個byte,故須為單字元
95
+ * @param {String} [opt.newline] 輸入行尾字元,因csv-parser僅取首個byte,故須為單字元
96
+ * @param {Boolean} [opt.strict] 輸入各列欄數是否須與標頭數相符布林值。因csv-parser於headers為false時會強制關閉本設定,故mode為mat時本設定無效
97
+ * @param {Function} [opt.mapHeaders] 輸入標頭轉換函式。因csv-parser僅於解析標頭列時呼叫,故mode為mat時不會被呼叫。另回傳null時該欄將被整欄丟棄
98
+ * @param {Function} [opt.mapValues] 輸入欄值轉換函式。mode為mat時其header參數恆為undefined
99
+ * @param {Number} [opt.maxRowBytes] 輸入單列位元組數上限整數
100
+ * @return {Promise} 回傳Promise,resolve回傳ltdt(各數據列為物件陣列)或mat(二維陣列),reject回傳錯誤訊息
21
101
  * @example
22
102
  *
23
103
  * import wdc from './src/WDataCsv.mjs'
@@ -35,32 +115,28 @@ import ltdtkeysheads2mat from 'wsemi/src/ltdtkeysheads2mat.mjs'
35
115
  * console.log(err)
36
116
  * })
37
117
  *
118
+ * await wdc.parseCsv(c, { mode: 'mat' })
119
+ * .then((mat) => {
120
+ * console.log(mat)
121
+ * // => [ [ 'NAME', 'AGE' ], [ 'Daffy Duck', '24' ], [ 'Bugs 邦妮', '22' ] ]
122
+ * })
123
+ * .catch((err) => {
124
+ * console.log(err)
125
+ * })
126
+ *
38
127
  */
39
- async function parseCsv(inp) {
40
- let res = []
41
-
42
- let pm = genPm()
128
+ async function parseCsv(inp, opt = {}) {
43
129
 
44
130
  //check
45
131
  if (!isestr(inp)) {
46
132
  return Promise.reject(`inp is not an effective string`)
47
133
  }
48
134
 
49
- Readable.from([inp])
50
- .pipe(csvParse())
51
- .on('data', (chunk) => {
52
- // console.log('data', chunk)
53
- res.push(chunk)
54
- })
55
- .on('end', () => {
56
- pm.resolve(res)
57
- })
58
- .on('error', (err) => {
135
+ return getRows(Readable.from([inp]), opt)
136
+ .catch((err) => {
59
137
  console.log(err)
60
- pm.reject(err)
138
+ return Promise.reject(err)
61
139
  })
62
-
63
- return pm
64
140
  }
65
141
 
66
142
 
@@ -68,7 +144,20 @@ async function parseCsv(inp) {
68
144
  * 讀取CSV檔,自動清除BOM
69
145
  *
70
146
  * @param {String} fp 輸入檔案位置字串
71
- * @return {Promise} 回傳Promise,resolve回傳ltdt(各數據列為物件陣列),reject回傳錯誤訊息
147
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
148
+ * @param {String} [opt.mode='ltdt'] 輸入回傳數據格式字串,可選ltdt(各數據列為物件)或mat(各數據列為陣列),預設ltdt
149
+ * @param {Boolean|Array} [opt.headers] 輸入標頭設定,給false代表無標頭列,給陣列代表無標頭列且以此陣列為欄名,不給則取第1列為標頭。因csv-parser於給true時會使各列成為_0,_1等溢出鍵之壞數據,故給true時視為不給。另mode為mat時本設定固定為false
150
+ * @param {Number} [opt.skipLines] 輸入解析前跳過列數整數。因csv-parser之註解列不計入行號,故與skipComments併用時計數基準會位移,兩者不建議同時使用
151
+ * @param {Boolean|String} [opt.skipComments] 輸入忽略註解列設定,給true代表跳過#開頭之列,給字串代表以該字元為註解符
152
+ * @param {String} [opt.separator] 輸入欄位分隔字元,因csv-parser僅取首個byte,故須為單字元
153
+ * @param {String} [opt.quote] 輸入引號字元,因csv-parser僅取首個byte,故須為單字元
154
+ * @param {String} [opt.escape] 輸入逃逸字元,因csv-parser僅取首個byte,故須為單字元
155
+ * @param {String} [opt.newline] 輸入行尾字元,因csv-parser僅取首個byte,故須為單字元
156
+ * @param {Boolean} [opt.strict] 輸入各列欄數是否須與標頭數相符布林值。因csv-parser於headers為false時會強制關閉本設定,故mode為mat時本設定無效
157
+ * @param {Function} [opt.mapHeaders] 輸入標頭轉換函式。因csv-parser僅於解析標頭列時呼叫,故mode為mat時不會被呼叫。另回傳null時該欄將被整欄丟棄
158
+ * @param {Function} [opt.mapValues] 輸入欄值轉換函式。mode為mat時其header參數恆為undefined
159
+ * @param {Number} [opt.maxRowBytes] 輸入單列位元組數上限整數
160
+ * @return {Promise} 回傳Promise,resolve回傳ltdt(各數據列為物件陣列)或mat(二維陣列),reject回傳錯誤訊息
72
161
  * @example
73
162
  *
74
163
  * import wdc from './src/WDataCsv.mjs'
@@ -84,33 +173,28 @@ async function parseCsv(inp) {
84
173
  * console.log(err)
85
174
  * })
86
175
  *
176
+ * wdc.readCsv(fp, { mode: 'mat' })
177
+ * .then((mat) => {
178
+ * console.log(mat)
179
+ * // => [ [ 'NAME', 'AGE' ], [ 'Daffy Duck', '24' ], [ 'Bugs 邦妮', '22' ] ]
180
+ * })
181
+ * .catch((err) => {
182
+ * console.log(err)
183
+ * })
184
+ *
87
185
  */
88
- async function readCsv(fp) {
89
- let res = []
90
-
91
- let pm = genPm()
186
+ async function readCsv(fp, opt = {}) {
92
187
 
93
188
  //check
94
189
  if (!fsIsFile(fp)) {
95
190
  return Promise.reject(`fp[${fp}] is not exist`)
96
191
  }
97
192
 
98
- fs.createReadStream(fp)
99
- .pipe(stripBom())
100
- .pipe(csvParse())
101
- .on('data', (chunk) => {
102
- // console.log('data', chunk)
103
- res.push(chunk)
104
- })
105
- .on('end', () => {
106
- pm.resolve(res)
107
- })
108
- .on('error', (err) => {
193
+ return getRows(fs.createReadStream(fp), opt)
194
+ .catch((err) => {
109
195
  console.log(err)
110
- pm.reject(err)
196
+ return Promise.reject(err)
111
197
  })
112
-
113
- return pm
114
198
  }
115
199
 
116
200
 
@@ -0,0 +1,143 @@
1
+ import fs from 'fs'
2
+ import assert from 'assert'
3
+ import wdc from '../src/WDataCsv.mjs'
4
+
5
+
6
+ //監測站原始CSV之結構: 前12列為儀器metadata, 第13列為#標記, 第14列為欄位名稱, 第15列起為量測數據
7
+ //另含短列, 空白列與長列, 用於驗證mat模式反映各列實際欄數
8
+ let csvSite = [
9
+ '1,v22.08.22,,,,,',
10
+ '2025ST1,,,,,,',
11
+ '1000,1000,,,,,',
12
+ 'Chiayi,,,,,,',
13
+ '1,,,,,,',
14
+ '1,,,,,,',
15
+ '1,,,,,,',
16
+ '0.408,0,Piezo1,kg/cm2,,,',
17
+ '0.408,0,Piezo2,kg/cm2,,,',
18
+ '0.408,0,EXT,kg/cm2,,,',
19
+ '0.408,0,5V,kg/cm2,,,',
20
+ 'x,,,,,,',
21
+ '#,,,,,,',
22
+ 'X(gal),Y(gal),Z(gal),Piezo1,Piezo2,EXT,5V',
23
+ '0.214,0.143,-0.158,1.187,2.642,2.356,4.928',
24
+ '0.1,0.2,0.3',
25
+ '',
26
+ '"a,b",c,d,e,f,g,h,i',
27
+ ].join('\r\n')
28
+
29
+
30
+ describe('readOpt', function() {
31
+
32
+ it('test opt undefined, 回歸至無opt之行為', async function() {
33
+ let rout = [{ NAME: 'Daffy Duck', AGE: '24' }, { NAME: 'Bugs 邦妮', AGE: '22' }]
34
+
35
+ let rRead = await wdc.readCsv('./g-test-in.csv')
36
+ assert.strict.deepEqual(rRead, rout)
37
+
38
+ let c = fs.readFileSync('./g-test-in.csv', 'utf8')
39
+ let rParse = await wdc.parseCsv(c)
40
+ assert.strict.deepEqual(rParse, rout)
41
+ })
42
+
43
+ it('test mode mat with skipLines, 跳過儀器metadata後自欄位名稱列起回傳二維陣列', async function() {
44
+ let fp = './g-test-site.csv'
45
+ fs.writeFileSync(fp, csvSite, 'utf8')
46
+
47
+ let mat = await wdc.readCsv(fp, { mode: 'mat', skipLines: 13 })
48
+
49
+ //第1列為欄位名稱
50
+ assert.strict.deepEqual(mat[0], ['X(gal)', 'Y(gal)', 'Z(gal)', 'Piezo1', 'Piezo2', 'EXT', '5V'])
51
+
52
+ //第2列為7欄量測數據
53
+ assert.strict.deepEqual(mat[1], ['0.214', '0.143', '-0.158', '1.187', '2.642', '2.356', '4.928'])
54
+
55
+ //短列保留其實際欄數, 不補空
56
+ assert.strict.deepEqual(mat[2], ['0.1', '0.2', '0.3'])
57
+
58
+ //空白列為空陣列
59
+ assert.strict.deepEqual(mat[3], [])
60
+
61
+ //長列保留其實際欄數不截斷, 且引號內之逗號不被視為分隔字元
62
+ assert.strict.deepEqual(mat[4], ['a,b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'])
63
+
64
+ //各列皆為陣列, 無_N溢出鍵
65
+ assert.strict.deepEqual(mat.length, 5)
66
+ assert.strict.deepEqual(mat.every((v) => Array.isArray(v)), true)
67
+
68
+ fs.unlinkSync(fp)
69
+ })
70
+
71
+ it('test headers true, 視為未給而非產生_N溢出鍵之壞數據', async function() {
72
+ let r = await wdc.parseCsv('a,b\n1,2', { headers: true })
73
+ assert.strict.deepEqual(r, [{ a: '1', b: '2' }])
74
+ })
75
+
76
+ it('test mode mat with headers, headers固定為false故給陣列時不生_N溢出鍵', async function() {
77
+ let r = await wdc.parseCsv('1,2,3,4,5', { mode: 'mat', headers: ['h0', 'h1', 'h2'] })
78
+ assert.strict.deepEqual(r, [['1', '2', '3', '4', '5']])
79
+ })
80
+
81
+ it('test mode ltdt with headers, 給陣列代表無標頭列故第1列為數據', async function() {
82
+ let r = await wdc.parseCsv('a,b\n1,2', { headers: ['h0', 'h1'] })
83
+ assert.strict.deepEqual(r, [{ h0: 'a', h1: 'b' }, { h0: '1', h1: '2' }])
84
+ })
85
+
86
+ it('test raw and outputByteOffset, 不透傳以維持字串契約與回傳形狀', async function() {
87
+ let rRaw = await wdc.parseCsv('a,b\n1,2', { mode: 'mat', raw: true })
88
+ assert.strict.deepEqual(typeof rRaw[0][0], 'string')
89
+
90
+ let rOff = await wdc.parseCsv('a,b\n1,2', { mode: 'mat', outputByteOffset: true })
91
+ assert.strict.deepEqual(rOff[0], ['a', 'b'])
92
+ })
93
+
94
+ it('test skipComments with skipLines, 註解列不計入行號故兩者併用時計數基準位移', async function() {
95
+ let c = 'm1,m2\nm3,m4\nm5,m6\n#,x\nA,B\n1,2'
96
+
97
+ let r1 = await wdc.parseCsv(c, { mode: 'mat', skipLines: 4 })
98
+ assert.strict.deepEqual(r1, [['A', 'B'], ['1', '2']])
99
+
100
+ let r2 = await wdc.parseCsv(c, { mode: 'mat', skipLines: 4, skipComments: true })
101
+ assert.strict.deepEqual(r2, [['1', '2']])
102
+ })
103
+
104
+ it('test bom, parseCsv與readCsv對含BOM內容之結果一致', async function() {
105
+ let c = 'NAME,AGE\r\nDaffy Duck,24\r\n'
106
+ let fp = './g-test-bom.csv'
107
+ fs.writeFileSync(fp, c, 'utf8')
108
+
109
+ let rParse = await wdc.parseCsv(c)
110
+ let rRead = await wdc.readCsv(fp)
111
+
112
+ assert.strict.deepEqual(rParse, rRead)
113
+ assert.strict.deepEqual(rParse, [{ NAME: 'Daffy Duck', AGE: '24' }])
114
+
115
+ fs.unlinkSync(fp)
116
+ })
117
+
118
+ it('test error, stream錯誤轉為reject且不拋出未捕獲例外', async function() {
119
+ let err = null
120
+ await wdc.readCsv('./g-test-in.csv', { maxRowBytes: 1 })
121
+ .catch((e) => {
122
+ err = e
123
+ })
124
+ assert.strict.deepEqual(err !== null, true)
125
+ })
126
+
127
+ it('test reject payload, 檢核失敗時仍reject字串', async function() {
128
+ let e1 = null
129
+ await wdc.parseCsv('')
130
+ .catch((e) => {
131
+ e1 = e
132
+ })
133
+ assert.strict.deepEqual(e1, 'inp is not an effective string')
134
+
135
+ let e2 = null
136
+ await wdc.readCsv('./no-such-file.csv')
137
+ .catch((e) => {
138
+ e2 = e
139
+ })
140
+ assert.strict.deepEqual(e2, 'fp[./no-such-file.csv] is not exist')
141
+ })
142
+
143
+ })