◆ 思ってたよりもかなり短く書けた

前回の CSV の記事を書いたときに作ったパーサは

for (const char of str) {
//
}

形式で一文字ずつ見ていって その時の状態に応じて分岐するものでした
これでやると 今の状態がどういうもので この状態でこういう文字が来たら……とか考えるのが面倒です
分岐ですべてのパターンを考えてるので 考慮漏れによるミスは少ないですが 実装が疲れますし 50 行は超えてきます

正規表現を使う方法だともっとシンプルできそうな気がしたので こっちもやってみました
普段は正規表現だけで色々やりすぎるのは怖いので 最小限のマッチング部分だけにして 結果を JavaScript 側で色々と追加で処理することが多いのですが正規表現にできる限り頼るようにしてみました
結果は思ったよりうまくいってシンプルになりました

ただ正規表現力があんまりないので例外的な入力でうまく動かないのがありそうなのがちょっと怖いところです
とりあえずソースコードはこうなりました

const quoted = `"((?:[^"]|"")*)"`
const unquoted = `([^,\r\n]*)`
const cell = `(?:${quoted}|${unquoted})`
const re_row = new RegExp(`^${cell}(?:,${cell})*$`, "gm")
const re_cell = new RegExp(`(?:^|,)${cell}`, "g")

const parse = (text) => {
const rows = []
for (const row of text.trim().match(re_row)) {
if (!row) continue
const cols = []
for (const [, quoted, unquoted] of row.matchAll(re_cell)) {
cols.push(quoted?.replaceAll('""', '"') || unquoted)
}
rows.push(cols)
}
return rows
}

使用例です

const parsed = parse(`
1,2,"a""b"
3
4,"5
6"
0,,1,,
`)
console.log(JSON.stringify(parsed, null, " "))
[
[
"1",
"2",
"a\"b"
],
[
"3"
],
[
"4",
"5\n6"
],
[
"0",
"",
"1",
"",
""
]
]

列数がバラバラだったり
クオートがあったり
クオート内にエスケープ表現があったり
クオート内に改行があったり
「,」続きだったり
「,」終わりだったり
しても大丈夫なのでとりあえずは問題なさそうに見えます