[JavaScript] CSVテキストを配列に分割する。「"*,*"」「"*改行*"」対応

javascriptでsplitを使ってコンマで分割すると、""で囲まれた内部のコンマまで分割されてしまう件 - Qiita
https://qiita.com/hatorijobs/items/dd0c730e6faba0c84203

【javascript】「,」でsplitする際、「"～,～"」の「,」での分割を回避（追記コード2個） - Qiita
https://qiita.com/noenture/items/02b2e1711e22182c6a97

これは楽しそうな話題です！自分も作ってみました。

こういう場合の動作確認には、テストコードを書くとコード品質があがります。

splitCSV 関数と、それを逆変換する joinCSV を作りました。
逆変換して正しく動作していることを確認しています。


```js
// 自作の Parts.js を使っています。
// https://github.com/standard-software/partsjs

const { loop } = parts.syntax;
const { isFirst, isLast, replaceAll, subLength } = parts.string;
const { checkEqual } = parts.test;

const splitCSV = (textData) => {
  const result = [];
  let line = [];
  const items = textData.split(',')
  for (let i = 0; i < items.length; i += 1) {
    let item = items[i];
    if (isFirst(item, '"')) {
      while (true){
        if (
          isLast(replaceAll(subLength(item, 1), '""', ''), '"')
          || (i === items.length - 1)
        ) {
          line.push(item)
          break;
        }
        i += 1;
        item += ',' + items[i];
      }
    } else {
      const addItems = item.split('\n');
      line.push(addItems[0]);
      for (let i = 1; i < addItems.length; i += 1) {
        result.push(line);
        line = [addItems[i]]
      }
    }
  }
  result.push(line);
  return result;
};

const joinCSV = (csvArray) => {
  const result = [];
  loop(csvArray)(lineArray => {
    line = lineArray.join(',');
    result.push(line);
  });
  return result.join('\n');
};

const checkCSV = (text) => {
  csvArray = splitCSV(text);
  console.log(csvArray);
  checkEqual(text, joinCSV(csvArray));
};

checkCSV(
  'a1,b1,"c1,c1",d1\n' +
  'a2,b2,"c2,c2,c2",d2\n' +
  'a3,b3,"c3\nc3\nc3",d3\n' +
  'a4,b4\n\n\na5,\na6,b6\n\na8,b8'
);
checkCSV('');
checkCSV(' ');
checkCSV('  ');
checkCSV('   ');
checkCSV('\n');
checkCSV('\n\n');
checkCSV('\n\n');
checkCSV(' \n');
checkCSV('\n ');
checkCSV('\n \n');
checkCSV('\n\n \n\n');
checkCSV(' \n\n \n\n ');

checkCSV('');
checkCSV('a1');
checkCSV('a1,b1');
checkCSV('a1\na2');
checkCSV('a1,b1\na2');
checkCSV('a1,b1\na2,b2');
checkCSV('a1,b1\na2,b2');
checkCSV('a1,b1\na2,b2\n\na4');
checkCSV('a1,b1\na2,b2\n\na4\n');

checkCSV(
  '"""aa"",aa","""bb,""bb","cc"",cc","dd,""dd","""ee,""ee"""'
);

var text = 'テスト1,"""テスト2""","テスト3,テスト3","""テスト4"",""テスト4"""';
checkCSV(text);
checkEqual(
  splitCSV(text),
  [
    [ 'テスト1', '"""テスト2"""', '"テスト3,テスト3"', '"""テスト4"",""テスト4"""']
  ]
)

var text = 'テスト1,"""テスト2""","テスト3,テスト3","""テスト4""",""テスト4"""';
checkCSV(text);
checkEqual(
  splitCSV(text),
  [
    [ 'テスト1', '"""テスト2"""', '"テスト3,テスト3"', '"""テスト4"""', '""テスト4"""']
  ]
)

checkEqual(
  splitCSV('"""aa"",aa","""bb,""bb","cc"",cc","dd,""dd","""ee,""ee"""'),
  [
    [ '"""aa"",aa"', '"""bb,""bb"', '"cc"",cc"', '"dd,""dd"', '"""ee,""ee"""' ]
  ]
)

```
https://jsbin.com/zaceguq/edit?html,js,console

出力結果は次の通り
それぞれうまく分割されているようです。

```
[
  [ 'a1', 'b1', '"c1,c1"', 'd1' ],
  [ 'a2', 'b2', '"c2,c2,c2"', 'd2' ],
  [ 'a3', 'b3', '"c3\nc3\nc3"', 'd3' ],
  [ 'a4', 'b4' ],
  [ '' ],
  [ '' ],
  [ 'a5', '' ],
  [ 'a6', 'b6' ],
  [ '' ],
  [ 'a8', 'b8' ]
]
[ [ '' ] ]
[ [ ' ' ] ]
[ [ '  ' ] ]
[ [ '   ' ] ]
[ [ '' ], [ '' ] ]
[ [ '' ], [ '' ], [ '' ] ]
[ [ '' ], [ '' ], [ '' ] ]
[ [ ' ' ], [ '' ] ]
[ [ '' ], [ ' ' ] ]
[ [ '' ], [ ' ' ], [ '' ] ]
[ [ '' ], [ '' ], [ ' ' ], [ '' ], [ '' ] ]
[ [ ' ' ], [ '' ], [ ' ' ], [ '' ], [ ' ' ] ]
[ [ '' ] ]
[ [ 'a1' ] ]
[ [ 'a1', 'b1' ] ]
[ [ 'a1' ], [ 'a2' ] ]
[ [ 'a1', 'b1' ], [ 'a2' ] ]
[ [ 'a1', 'b1' ], [ 'a2', 'b2' ] ]
[ [ 'a1', 'b1' ], [ 'a2', 'b2' ] ]
[ [ 'a1', 'b1' ], [ 'a2', 'b2' ], [ '' ], [ 'a4' ] ]
[ [ 'a1', 'b1' ], [ 'a2', 'b2' ], [ '' ], [ 'a4' ], [ '' ] ]
[
  [
    '"""aa"",aa"',
    '"""bb,""bb"',
    '"cc"",cc"',
    '"dd,""dd"',
    '"""ee,""ee"""'
  ]
]
[ [ 'テスト1', '"""テスト2"""', '"テスト3,テスト3"', '"""テスト4"",""テスト4"""' ] ]
[ [ 'テスト1', '"""テスト2"""', '"テスト3,テスト3"', '"""テスト4"""', '""テスト4"""' ] ]
```

改行はLFしか考慮していませんが、CRLFでも行末尾要素にLFが入るはずなので大丈夫でしょう。

セル内の2連続ダブルクウォートを1つのダブルクウォートにするという変換は逆変換のときに誤動作になりかねないので行いませんでした。

こういうタイプなら誤動作するよ、というCSVテキストの例ありましたら、コメント欄で教えていただけると助かります。
よろしくお願いします。

# 追記

JavaScriptでは、基本的な部品が相当不足しているので、だいたい誰もが lodash を使ったりして基本的な関数群を補ったりしていると思います。
自分の場合は、そのような基本的な関数群を自作で Parts.js としてまとめています。
Parts.js では、なるべく品質の高い部品を作った上で、それを組合わせて少し大きめな部品を作るということをよくやります。
この splitCSV も、Parts.js に組み込み予定です。

今回のコードではすでに昔作っていた次のような非常に単純な関数をつかい、より大きめの機能を品質高めに作成しました。

```js
const { loop } = parts.syntax;
// 通常 for で記載するループを少し拡張して便利にしたもの

const { isFirst, isLast, replaceAll, subLength } = parts.string;
// 文字列の先頭一致、終端一致、文字列置換、文字列一部切り出し

const { checkEqual } = parts.test;
// 単純比較だけじゃなくて、内部比較(deepEqual)して不一致なら報告する関数
```

このようにシンプルな機能を積み重ねて、より複雑な機能を実装していく、という開発を繰り返していくと、積み重ねによってより高度なものが様々作り出せるようになります。

今回の splitCSV を作るために、それぞれの細かい機能を作り出すとなるとめんどくさくなって雑なコードになってしまいます。例えばcheckEqualとか、内部では相当ややこしいことをしています。ですが、これを昔作っていたのでこのような場面ですぐに再利用できるために短い時間で開発できます。

一度作った部品は二度と作りたくなく、また、一度解決した課題は次も頭を悩ませたくないので、テストコード含めて作っておいてその実装を自分で保持して記録しておくと、同じ問題がきたときに素早く対処できますし、似たような課題が来たときも応用がすぐにできます。

開発が経験を積み重ねることで雪だるま式に楽になっていくようになるので、開発手法としてもおすすめです。

テストコードを書かないプログラマーはなぜ書かないのでしょうか？ - Quora
https://qr.ae/pNydzS

このあたりでも、リンクさせてもらいました。