w-dwdata-hub 1.0.0
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/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-dwdata-hub.umd.js +7 -0
- package/dist/w-dwdata-hub.umd.js.map +1 -0
- package/docs/WDwdataHub.mjs.html +136 -0
- package/docs/decodeEntities.mjs.html +129 -0
- package/docs/extractDivContent.mjs.html +147 -0
- package/docs/fetchAiNewsAggregator.mjs.html +155 -0
- package/docs/fetchAisixiang.mjs.html +1038 -0
- package/docs/fetchCnyes.mjs.html +260 -0
- package/docs/fetchGuancha.mjs.html +1007 -0
- package/docs/fetchHackerNews.mjs.html +189 -0
- package/docs/fetchMoneydj.mjs.html +251 -0
- package/docs/fetchMops.mjs.html +385 -0
- package/docs/fetchRSS.mjs.html +160 -0
- package/docs/fetchStatementdog.mjs.html +213 -0
- package/docs/fetchTaifex.mjs.html +513 -0
- package/docs/fetchTpex3insti.mjs.html +278 -0
- package/docs/fetchTpexMargin.mjs.html +248 -0
- package/docs/fetchTpexStock.mjs.html +199 -0
- package/docs/fetchTwDataHoliday.mjs.html +236 -0
- package/docs/fetchTwseMargin.mjs.html +253 -0
- package/docs/fetchTwseStock.mjs.html +188 -0
- package/docs/fetchTwseT86.mjs.html +208 -0
- package/docs/fetchWithRetry.mjs.html +376 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/getOptFetch.mjs.html +185 -0
- package/docs/global.html +22649 -0
- package/docs/htmlToMarkdown.mjs.html +168 -0
- package/docs/index.html +84 -0
- package/docs/isYmd.mjs.html +114 -0
- package/docs/parseIntComma.mjs.html +100 -0
- package/docs/safeFilename.mjs.html +102 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/docs/toDatetimeUTC8.mjs.html +143 -0
- package/docs/toRocDate.mjs.html +107 -0
- package/g.mjs +73 -0
- package/package.json +31 -0
- package/script.txt +17 -0
- package/src/WDwdataHub.mjs +64 -0
- package/src/decodeEntities.mjs +57 -0
- package/src/extractDivContent.mjs +75 -0
- package/src/fetchAiNewsAggregator.mjs +83 -0
- package/src/fetchAisixiang.mjs +966 -0
- package/src/fetchCnyes.mjs +188 -0
- package/src/fetchGuancha.mjs +935 -0
- package/src/fetchHackerNews.mjs +117 -0
- package/src/fetchMoneydj.mjs +179 -0
- package/src/fetchMops.mjs +313 -0
- package/src/fetchRSS.mjs +88 -0
- package/src/fetchStatementdog.mjs +141 -0
- package/src/fetchTaifex.mjs +441 -0
- package/src/fetchTpex3insti.mjs +206 -0
- package/src/fetchTpexMargin.mjs +176 -0
- package/src/fetchTpexStock.mjs +127 -0
- package/src/fetchTwDataHoliday.mjs +164 -0
- package/src/fetchTwseMargin.mjs +181 -0
- package/src/fetchTwseStock.mjs +116 -0
- package/src/fetchTwseT86.mjs +136 -0
- package/src/fetchWithRetry.mjs +304 -0
- package/src/getOptFetch.mjs +113 -0
- package/src/htmlToMarkdown.mjs +96 -0
- package/src/isYmd.mjs +42 -0
- package/src/parseIntComma.mjs +28 -0
- package/src/safeFilename.mjs +30 -0
- package/src/toDatetimeUTC8.mjs +71 -0
- package/src/toRocDate.mjs +35 -0
- package/test/WDwdataHub.test.mjs +63 -0
- package/test/decodeEntities.test.mjs +42 -0
- package/test/extractDivContent.test.mjs +48 -0
- package/test/fetchAiNewsAggregator.test.mjs +79 -0
- package/test/fetchAisixiang.test.mjs +225 -0
- package/test/fetchCnyes.test.mjs +61 -0
- package/test/fetchGuancha.test.mjs +190 -0
- package/test/fetchHackerNews.test.mjs +78 -0
- package/test/fetchMoneydj.test.mjs +82 -0
- package/test/fetchMops.test.mjs +106 -0
- package/test/fetchRSS.test.mjs +85 -0
- package/test/fetchStatementdog.test.mjs +69 -0
- package/test/fetchTaifex.test.mjs +99 -0
- package/test/fetchTpex3insti.test.mjs +90 -0
- package/test/fetchTpexMargin.test.mjs +94 -0
- package/test/fetchTpexStock.test.mjs +68 -0
- package/test/fetchTwDataHoliday.test.mjs +100 -0
- package/test/fetchTwseMargin.test.mjs +81 -0
- package/test/fetchTwseStock.test.mjs +80 -0
- package/test/fetchTwseT86.test.mjs +76 -0
- package/test/fetchWithRetry.test.mjs +174 -0
- package/test/htmlToMarkdown.test.mjs +71 -0
- package/test/isYmd.test.mjs +56 -0
- package/test/parseIntComma.test.mjs +38 -0
- package/test/safeFilename.test.mjs +35 -0
- package/test/toDatetimeUTC8.test.mjs +65 -0
- package/test/toRocDate.test.mjs +38 -0
- package/test/tools/encodeBig5.mjs +80 -0
- package/test/tools/fixtures.mjs +539 -0
- package/test/tools/hasChrome.mjs +39 -0
- package/test/tools/serverForTest.mjs +397 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +5 -0
- package/toolg/gDistRollup.mjs +49 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchWithRetry from '../src/fetchWithRetry.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchWithRetry', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
|
|
10
|
+
//optFast, 令重試等待縮至10毫秒, 避免測試耗時
|
|
11
|
+
let optFast = { baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
12
|
+
|
|
13
|
+
before(async function() {
|
|
14
|
+
svr = await serverForTest()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
after(async function() {
|
|
18
|
+
if (svr) {
|
|
19
|
+
await svr.close()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('非有效字串網址拋錯', async function() {
|
|
24
|
+
let r = []
|
|
25
|
+
for (let url of [null, undefined, '', 123, {}]) {
|
|
26
|
+
await fetchWithRetry(url)
|
|
27
|
+
.then(() => {
|
|
28
|
+
r.push('resolved')
|
|
29
|
+
})
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
r.push(err.message.startsWith('url須為有效字串'))
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
let rr = [true, true, true, true, true]
|
|
35
|
+
assert.strict.deepEqual(r, rr)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('responseType預設為json並回傳物件', async function() {
|
|
39
|
+
let r = await fetchWithRetry(svr.url('/json/ok'), optFast)
|
|
40
|
+
let rr = { ok: true, value: 1 }
|
|
41
|
+
assert.strict.deepEqual(r, rr)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('responseType為text時回傳原始字串', async function() {
|
|
45
|
+
let t = await fetchWithRetry(svr.url('/rss/feed.xml'), { ...optFast, responseType: 'text' })
|
|
46
|
+
let r = [typeof t, t.includes('<title>測試訂閱源</title>')]
|
|
47
|
+
let rr = ['string', true]
|
|
48
|
+
assert.strict.deepEqual(r, rr)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('responseType為text且指定encoding時以該編碼解碼', async function() {
|
|
52
|
+
let r = await fetchWithRetry(svr.url('/text/big5'), { ...optFast, responseType: 'text', encoding: 'big5' })
|
|
53
|
+
let rr = '中文測試,Big5編碼'
|
|
54
|
+
assert.strict.deepEqual(r, rr)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('responseType為text但未指定encoding時以utf-8解碼, Big5內容會解出亂碼', async function() {
|
|
58
|
+
let t = await fetchWithRetry(svr.url('/text/big5'), { ...optFast, responseType: 'text' })
|
|
59
|
+
let r = t === '中文測試,Big5編碼'
|
|
60
|
+
let rr = false
|
|
61
|
+
assert.strict.deepEqual(r, rr)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('responseType為arrayBuffer時回傳ArrayBuffer', async function() {
|
|
65
|
+
let t = await fetchWithRetry(svr.url('/text/big5'), { ...optFast, responseType: 'arrayBuffer' })
|
|
66
|
+
let r = [t instanceof ArrayBuffer, new TextDecoder('big5').decode(t)]
|
|
67
|
+
let rr = [true, '中文測試,Big5編碼']
|
|
68
|
+
assert.strict.deepEqual(r, rr)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('回傳非JSON時拋出可讀錯誤而非裸SyntaxError', async function() {
|
|
72
|
+
let r = null
|
|
73
|
+
await fetchWithRetry(svr.url('/json/bad'), optFast)
|
|
74
|
+
.then(() => {
|
|
75
|
+
r = 'resolved'
|
|
76
|
+
})
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
r = [err.message.startsWith('回傳非JSON格式'), err instanceof SyntaxError]
|
|
79
|
+
})
|
|
80
|
+
let rr = [true, false]
|
|
81
|
+
assert.strict.deepEqual(r, rr)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('HTTP 404不重試, 僅執行1次且錯誤帶statusCode', async function() {
|
|
85
|
+
let c0 = svr.getCount('/status/404')
|
|
86
|
+
let e = null
|
|
87
|
+
await fetchWithRetry(svr.url('/status/404'), { ...optFast, maxRetries: 5 })
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
e = err
|
|
90
|
+
})
|
|
91
|
+
let r = [e.statusCode, svr.getCount('/status/404') - c0]
|
|
92
|
+
let rr = [404, 1]
|
|
93
|
+
assert.strict.deepEqual(r, rr)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('HTTP 403預設不重試', async function() {
|
|
97
|
+
let c0 = svr.getCount('/status/403')
|
|
98
|
+
await fetchWithRetry(svr.url('/status/403'), { ...optFast, maxRetries: 3 })
|
|
99
|
+
.catch(() => {})
|
|
100
|
+
let r = svr.getCount('/status/403') - c0
|
|
101
|
+
let rr = 1
|
|
102
|
+
assert.strict.deepEqual(r, rr)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('HTTP 403於retryStatus指定時會重試, 含初始共執行maxRetries+1次', async function() {
|
|
106
|
+
let c0 = svr.getCount('/status/403')
|
|
107
|
+
await fetchWithRetry(svr.url('/status/403'), { ...optFast, maxRetries: 3, retryStatus: [403] })
|
|
108
|
+
.catch(() => {})
|
|
109
|
+
let r = svr.getCount('/status/403') - c0
|
|
110
|
+
let rr = 4
|
|
111
|
+
assert.strict.deepEqual(r, rr)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('HTTP 500重試至次數用盡, 含初始共執行maxRetries+1次', async function() {
|
|
115
|
+
let c0 = svr.getCount('/status/500')
|
|
116
|
+
let e = null
|
|
117
|
+
await fetchWithRetry(svr.url('/status/500'), { ...optFast, maxRetries: 2 })
|
|
118
|
+
.catch((err) => {
|
|
119
|
+
e = err
|
|
120
|
+
})
|
|
121
|
+
let r = [e.statusCode, svr.getCount('/status/500') - c0]
|
|
122
|
+
let rr = [500, 3]
|
|
123
|
+
assert.strict.deepEqual(r, rr)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('HTTP 429會重試', async function() {
|
|
127
|
+
let c0 = svr.getCount('/status/429')
|
|
128
|
+
await fetchWithRetry(svr.url('/status/429'), { ...optFast, maxRetries: 2 })
|
|
129
|
+
.catch(() => {})
|
|
130
|
+
let r = svr.getCount('/status/429') - c0
|
|
131
|
+
let rr = 3
|
|
132
|
+
assert.strict.deepEqual(r, rr)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('暫時性失敗於重試後成功', async function() {
|
|
136
|
+
await fetchWithRetry(svr.url('/flaky/reset'), optFast)
|
|
137
|
+
let t = await fetchWithRetry(svr.url('/flaky/503?n=2'), { ...optFast, maxRetries: 5 })
|
|
138
|
+
let r = [t.ok, t.failed]
|
|
139
|
+
let rr = [true, 2]
|
|
140
|
+
assert.strict.deepEqual(r, rr)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('連線不到之埠號視為網路層錯誤並重試', async function() {
|
|
144
|
+
let e = null
|
|
145
|
+
await fetchWithRetry('http://127.0.0.1:1/abc', { ...optFast, maxRetries: 1, timeout: 2000 })
|
|
146
|
+
.catch((err) => {
|
|
147
|
+
e = err
|
|
148
|
+
})
|
|
149
|
+
let r = e !== null
|
|
150
|
+
let rr = true
|
|
151
|
+
assert.strict.deepEqual(r, rr)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('逾時視為可重試錯誤, maxRetries為0時僅執行1次', async function() {
|
|
155
|
+
let c0 = svr.getCount('/slow')
|
|
156
|
+
let e = null
|
|
157
|
+
await fetchWithRetry(svr.url('/slow?ms=300'), { ...optFast, maxRetries: 0, timeout: 50 })
|
|
158
|
+
.catch((err) => {
|
|
159
|
+
e = err
|
|
160
|
+
})
|
|
161
|
+
let r = [e !== null, svr.getCount('/slow') - c0]
|
|
162
|
+
let rr = [true, 1]
|
|
163
|
+
assert.strict.deepEqual(r, rr)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('未指定User-Agent時自動補預設值, 指定時以指定者為準', async function() {
|
|
167
|
+
let t1 = await fetchWithRetry(svr.url('/echo'), optFast)
|
|
168
|
+
let t2 = await fetchWithRetry(svr.url('/echo'), { ...optFast, headers: { 'User-Agent': 'my-ua' } })
|
|
169
|
+
let r = [t1.headers['user-agent'].includes('Mozilla/5.0'), t2.headers['user-agent']]
|
|
170
|
+
let rr = [true, 'my-ua']
|
|
171
|
+
assert.strict.deepEqual(r, rr)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import htmlToMarkdown from '../src/htmlToMarkdown.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('htmlToMarkdown', function() {
|
|
6
|
+
|
|
7
|
+
it('段落轉為空行分隔', function() {
|
|
8
|
+
let r = htmlToMarkdown('<p>第一段</p><p>第二段</p>')
|
|
9
|
+
let rr = '第一段\n\n第二段'
|
|
10
|
+
assert.strict.deepEqual(r, rr)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('粗體與斜體轉為Markdown標記', function() {
|
|
14
|
+
let r = htmlToMarkdown('<p>a<b>粗</b>b<strong>體</strong>c<i>斜</i>d<em>體</em></p>')
|
|
15
|
+
let rr = 'a**粗**b**體**c*斜*d*體*'
|
|
16
|
+
assert.strict.deepEqual(r, rr)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('標題轉為粗體段落', function() {
|
|
20
|
+
let r = htmlToMarkdown('<h1>標題</h1><p>內文</p>')
|
|
21
|
+
let rr = '**標題**\n\n內文'
|
|
22
|
+
assert.strict.deepEqual(r, rr)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('連結轉為Markdown連結, 無文字之連結被移除', function() {
|
|
26
|
+
let r = [
|
|
27
|
+
htmlToMarkdown('<p><a href="https://a.example/">連結</a></p>'),
|
|
28
|
+
htmlToMarkdown('<p>x<a href="https://a.example/"></a>y</p>'),
|
|
29
|
+
]
|
|
30
|
+
let rr = ['[連結](https://a.example/)', 'xy']
|
|
31
|
+
assert.strict.deepEqual(r, rr)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('br轉為換行', function() {
|
|
35
|
+
let r = htmlToMarkdown('<p>一<br/>二<br>三</p>')
|
|
36
|
+
let rr = '一\n二\n三'
|
|
37
|
+
assert.strict.deepEqual(r, rr)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('移除script、style與註解', function() {
|
|
41
|
+
let r = htmlToMarkdown('<p>前</p><script>var a=1</script><style>.a{}</style><!--註解--><p>後</p>')
|
|
42
|
+
let rr = '前\n\n後'
|
|
43
|
+
assert.strict.deepEqual(r, rr)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('解碼HTML實體並收斂多重空行', function() {
|
|
47
|
+
let r = htmlToMarkdown('<p>a&b</p><p></p><p></p><p>c</p>')
|
|
48
|
+
let rr = 'a&b\n\nc'
|
|
49
|
+
assert.strict.deepEqual(r, rr)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('預設移除img, image為true時轉為Markdown圖片', function() {
|
|
53
|
+
let r = [
|
|
54
|
+
htmlToMarkdown('<p><img src="https://a.example/1.png" />文字</p>'),
|
|
55
|
+
htmlToMarkdown('<p><img src="https://a.example/1.png" />文字</p>', { image: true }),
|
|
56
|
+
]
|
|
57
|
+
let rr = ['文字', '文字']
|
|
58
|
+
assert.strict.deepEqual(r, rr)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('非字串回傳空字串', function() {
|
|
62
|
+
let r = [
|
|
63
|
+
htmlToMarkdown(null),
|
|
64
|
+
htmlToMarkdown(undefined),
|
|
65
|
+
htmlToMarkdown(123),
|
|
66
|
+
]
|
|
67
|
+
let rr = ['', '', '']
|
|
68
|
+
assert.strict.deepEqual(r, rr)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import isYmd from '../src/isYmd.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('isYmd', function() {
|
|
6
|
+
|
|
7
|
+
it('合法之YYYYMMDD字串為true', function() {
|
|
8
|
+
let r = [
|
|
9
|
+
isYmd('20260807'),
|
|
10
|
+
isYmd('20000229'),
|
|
11
|
+
isYmd('19110101'),
|
|
12
|
+
isYmd('20261231'),
|
|
13
|
+
]
|
|
14
|
+
let rr = [true, true, true, true]
|
|
15
|
+
assert.strict.deepEqual(r, rr)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('格式為8碼數字但日曆不存在之日期為false', function() {
|
|
19
|
+
let r = [
|
|
20
|
+
isYmd('20260230'),
|
|
21
|
+
isYmd('20260231'),
|
|
22
|
+
isYmd('20261301'),
|
|
23
|
+
isYmd('20260732'),
|
|
24
|
+
isYmd('20260000'),
|
|
25
|
+
isYmd('21000229'),
|
|
26
|
+
]
|
|
27
|
+
let rr = [false, false, false, false, false, false]
|
|
28
|
+
assert.strict.deepEqual(r, rr)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('非8碼數字格式為false', function() {
|
|
32
|
+
let r = [
|
|
33
|
+
isYmd('2026-08-07'),
|
|
34
|
+
isYmd('2026080'),
|
|
35
|
+
isYmd('202608071'),
|
|
36
|
+
isYmd('2026080a'),
|
|
37
|
+
isYmd(' 20260807'),
|
|
38
|
+
]
|
|
39
|
+
let rr = [false, false, false, false, false]
|
|
40
|
+
assert.strict.deepEqual(r, rr)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('非有效字串為false', function() {
|
|
44
|
+
let r = [
|
|
45
|
+
isYmd(''),
|
|
46
|
+
isYmd(null),
|
|
47
|
+
isYmd(undefined),
|
|
48
|
+
isYmd(20260807),
|
|
49
|
+
isYmd({}),
|
|
50
|
+
isYmd([]),
|
|
51
|
+
]
|
|
52
|
+
let rr = [false, false, false, false, false, false]
|
|
53
|
+
assert.strict.deepEqual(r, rr)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import parseIntComma from '../src/parseIntComma.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('parseIntComma', function() {
|
|
6
|
+
|
|
7
|
+
it('去除千分位逗號後轉整數', function() {
|
|
8
|
+
let r = [
|
|
9
|
+
parseIntComma('1,234,567'),
|
|
10
|
+
parseIntComma('1000'),
|
|
11
|
+
parseIntComma('-1,500'),
|
|
12
|
+
parseIntComma('0'),
|
|
13
|
+
]
|
|
14
|
+
let rr = [1234567, 1000, -1500, 0]
|
|
15
|
+
assert.strict.deepEqual(r, rr)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('證交所與櫃買中心之缺值表示法轉為0', function() {
|
|
19
|
+
let r = [
|
|
20
|
+
parseIntComma('-'),
|
|
21
|
+
parseIntComma(''),
|
|
22
|
+
parseIntComma('N/A'),
|
|
23
|
+
]
|
|
24
|
+
let rr = [0, 0, 0]
|
|
25
|
+
assert.strict.deepEqual(r, rr)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('非字串原樣回傳', function() {
|
|
29
|
+
let r = [
|
|
30
|
+
parseIntComma(12),
|
|
31
|
+
parseIntComma(null),
|
|
32
|
+
parseIntComma(undefined),
|
|
33
|
+
]
|
|
34
|
+
let rr = [12, null, undefined]
|
|
35
|
+
assert.strict.deepEqual(r, rr)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import safeFilename from '../src/safeFilename.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('safeFilename', function() {
|
|
6
|
+
|
|
7
|
+
it('替換Windows與POSIX不允許之字元為底線', function() {
|
|
8
|
+
let r = safeFilename('a\\b/c:d*e?f"g<h>i|j')
|
|
9
|
+
let rr = 'a_b_c_d_e_f_g_h_i_j'
|
|
10
|
+
assert.strict.deepEqual(r, rr)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('保留中文與一般字元', function() {
|
|
14
|
+
let r = safeFilename('葛兆光:禅宗与中国文化 (2026)')
|
|
15
|
+
let rr = '葛兆光:禅宗与中国文化 (2026)'
|
|
16
|
+
assert.strict.deepEqual(r, rr)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('截斷至200字元', function() {
|
|
20
|
+
let r = safeFilename('a'.repeat(300)).length
|
|
21
|
+
let rr = 200
|
|
22
|
+
assert.strict.deepEqual(r, rr)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('非字串轉為字串處理, null與undefined轉為空字串', function() {
|
|
26
|
+
let r = [
|
|
27
|
+
safeFilename(null),
|
|
28
|
+
safeFilename(undefined),
|
|
29
|
+
safeFilename(123),
|
|
30
|
+
]
|
|
31
|
+
let rr = ['', '', '123']
|
|
32
|
+
assert.strict.deepEqual(r, rr)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import toDatetimeUTC8 from '../src/toDatetimeUTC8.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('toDatetimeUTC8', function() {
|
|
6
|
+
|
|
7
|
+
it('ISO時間字串轉UTC+8', function() {
|
|
8
|
+
let r = [
|
|
9
|
+
toDatetimeUTC8('2026-08-07T01:02:03Z'),
|
|
10
|
+
toDatetimeUTC8('2026-08-06T16:00:00Z'),
|
|
11
|
+
toDatetimeUTC8('2026-12-31T20:00:00Z'),
|
|
12
|
+
]
|
|
13
|
+
let rr = ['2026-08-07 09:02:03', '2026-08-07 00:00:00', '2027-01-01 04:00:00']
|
|
14
|
+
assert.strict.deepEqual(r, rr)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('RFC822時間字串轉UTC+8', function() {
|
|
18
|
+
let r = [
|
|
19
|
+
toDatetimeUTC8('Fri, 07 Aug 2026 01:02:03 GMT'),
|
|
20
|
+
toDatetimeUTC8('Fri, 07 Aug 2026 02:03:04 GMT'),
|
|
21
|
+
]
|
|
22
|
+
let rr = ['2026-08-07 09:02:03', '2026-08-07 10:03:04']
|
|
23
|
+
assert.strict.deepEqual(r, rr)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('數值視為unix秒數轉UTC+8', function() {
|
|
27
|
+
let r = [
|
|
28
|
+
toDatetimeUTC8(1786000000),
|
|
29
|
+
toDatetimeUTC8(1786000060),
|
|
30
|
+
]
|
|
31
|
+
let rr = ['2026-08-06 15:06:40', '2026-08-06 15:07:40']
|
|
32
|
+
assert.strict.deepEqual(r, rr)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('Date物件轉UTC+8', function() {
|
|
36
|
+
let r = toDatetimeUTC8(new Date('2026-08-07T01:02:03Z'))
|
|
37
|
+
let rr = '2026-08-07 09:02:03'
|
|
38
|
+
assert.strict.deepEqual(r, rr)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('不受執行環境時區影響, 一律以UTC+8輸出', function() {
|
|
42
|
+
//以UTC+0與UTC+8之等價時刻做交叉驗證, 兩者應得相同結果
|
|
43
|
+
let r = [
|
|
44
|
+
toDatetimeUTC8('2026-08-07T01:02:03Z'),
|
|
45
|
+
toDatetimeUTC8('2026-08-07T09:02:03+08:00'),
|
|
46
|
+
]
|
|
47
|
+
let rr = ['2026-08-07 09:02:03', '2026-08-07 09:02:03']
|
|
48
|
+
assert.strict.deepEqual(r, rr)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('無法解析或為空時回傳空字串', function() {
|
|
52
|
+
let r = [
|
|
53
|
+
toDatetimeUTC8('not-a-date'),
|
|
54
|
+
toDatetimeUTC8(''),
|
|
55
|
+
toDatetimeUTC8(0),
|
|
56
|
+
toDatetimeUTC8(null),
|
|
57
|
+
toDatetimeUTC8(undefined),
|
|
58
|
+
toDatetimeUTC8({}),
|
|
59
|
+
toDatetimeUTC8([]),
|
|
60
|
+
]
|
|
61
|
+
let rr = ['', '', '', '', '', '', '']
|
|
62
|
+
assert.strict.deepEqual(r, rr)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import toRocDate from '../src/toRocDate.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('toRocDate', function() {
|
|
6
|
+
|
|
7
|
+
it('西元轉民國且月日保留原始2碼', function() {
|
|
8
|
+
let r = [
|
|
9
|
+
toRocDate('20260807'),
|
|
10
|
+
toRocDate('20260101'),
|
|
11
|
+
toRocDate('20261231'),
|
|
12
|
+
]
|
|
13
|
+
let rr = ['115/08/07', '115/01/01', '115/12/31']
|
|
14
|
+
assert.strict.deepEqual(r, rr)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('民國元年與元年前之邊界', function() {
|
|
18
|
+
let r = [
|
|
19
|
+
toRocDate('19120101'),
|
|
20
|
+
toRocDate('19110101'),
|
|
21
|
+
toRocDate('19100101'),
|
|
22
|
+
]
|
|
23
|
+
let rr = ['1/01/01', '0/01/01', '-1/01/01']
|
|
24
|
+
assert.strict.deepEqual(r, rr)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('非有效字串回傳空字串', function() {
|
|
28
|
+
let r = [
|
|
29
|
+
toRocDate(''),
|
|
30
|
+
toRocDate(null),
|
|
31
|
+
toRocDate(undefined),
|
|
32
|
+
toRocDate(20260807),
|
|
33
|
+
]
|
|
34
|
+
let rr = ['', '', '', '']
|
|
35
|
+
assert.strict.deepEqual(r, rr)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//由TextDecoder('big5')反推之編碼表, 首次呼叫時建立後快取
|
|
2
|
+
let mapChar2Bytes = null
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 建立Big5編碼表
|
|
7
|
+
*
|
|
8
|
+
* node內建TextDecoder可解Big5但TextEncoder僅支援utf-8, 故以TextDecoder逐一解碼合法雙位元組序列反推編碼表,
|
|
9
|
+
* 令測試不需額外相依即可產生Big5位元組(期交所CSV為Big5編碼)
|
|
10
|
+
*
|
|
11
|
+
* @returns {Map} 回傳字元對應位元組陣列之Map
|
|
12
|
+
*/
|
|
13
|
+
function buildMap() {
|
|
14
|
+
|
|
15
|
+
let m = new Map()
|
|
16
|
+
let dec = new TextDecoder('big5')
|
|
17
|
+
|
|
18
|
+
//單位元組(ASCII)
|
|
19
|
+
for (let b = 0; b < 0x80; b++) {
|
|
20
|
+
m.set(String.fromCharCode(b), [b])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//雙位元組, lead為0x81-0xFE, trail為0x40-0x7E與0xA1-0xFE
|
|
24
|
+
for (let lead = 0x81; lead <= 0xFE; lead++) {
|
|
25
|
+
for (let trail = 0x40; trail <= 0xFE; trail++) {
|
|
26
|
+
if (trail > 0x7E && trail < 0xA1) {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
let s = dec.decode(new Uint8Array([lead, trail]))
|
|
30
|
+
if (s.length !== 1 || s === '�') {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if (!m.has(s)) {
|
|
34
|
+
m.set(s, [lead, trail])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return m
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 字串編碼為Big5位元組
|
|
45
|
+
*
|
|
46
|
+
* 供測試伺服器產生期交所風格之Big5 CSV, 無對應碼位之字元以'?'替代
|
|
47
|
+
*
|
|
48
|
+
* @param {String} str 輸入字串
|
|
49
|
+
* @returns {Buffer} 回傳Big5編碼之Buffer
|
|
50
|
+
* @example
|
|
51
|
+
*
|
|
52
|
+
* import encodeBig5 from './tools/encodeBig5.mjs'
|
|
53
|
+
*
|
|
54
|
+
* let buf = encodeBig5('契約')
|
|
55
|
+
* console.log(new TextDecoder('big5').decode(buf))
|
|
56
|
+
* // => '契約'
|
|
57
|
+
*
|
|
58
|
+
*/
|
|
59
|
+
function encodeBig5(str) {
|
|
60
|
+
|
|
61
|
+
if (mapChar2Bytes === null) {
|
|
62
|
+
mapChar2Bytes = buildMap()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let bytes = []
|
|
66
|
+
for (let ch of String(str)) {
|
|
67
|
+
let bs = mapChar2Bytes.get(ch)
|
|
68
|
+
if (bs === undefined) {
|
|
69
|
+
bytes.push(0x3F) //'?'
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
bytes.push(...bs)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Buffer.from(bytes)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
export default encodeBig5
|