src/NTCsv.js
/**
* CSV形式のテキストなどを扱うユーティリティクラス
*
* 主な特徴:
* - 区切り文字のカスタマイズ(デフォルトは `,`)
* - ダブルクォートで囲まれたフィールドや改行を含むフィールドのパースに対応
* - 2次元配列 ⇄ CSVテキスト、CSV配列 ⇄ JSON配列 の相互変換
*
* @class NTCsv
* @module NTCsv
* @author natade (https://github.com/natade-jp)
* @license MIT
*/
export default class NTCsv {
/**
* CSVテキストから2次元配列を作成します。
*
* - 改行コードは `\r\n?|\n` を `\n` に正規化してから解析します。
* - ダブルクォートで囲まれたフィールド内では、`""` は文字としての `"` として扱います。
* - ダブルクォートで囲まれたフィールドは複数行にまたがっても1フィールドとして扱います。
*
* @param {string} text - CSV形式のテキスト
* @param {string} [separator=","] - 区切り文字(例: `","`、`"\t"`、`";"` など)
* @returns {Array<Array<string>>} CSVの各行を配列として持つ2次元配列
*
* @example
* // 基本: カンマ区切り
* const csvText = "name,age\nAlice,30\nBob,25";
* const rows = NTCsv.parse(csvText);
* // rows => [["name","age"],["Alice","30"],["Bob","25"]]
*
* @example
* // セミコロン区切り
* const text = "id;memo\n1;hello;world"; // ← 'hello;world' を一つのセルにしたい場合はクォートが必要
* const rows = NTCsv.parse("id;memo\n1;\"hello;world\"", ";");
* // rows => [["id","memo"],["1","hello;world"]]
*
* @example
* // 改行を含むフィールド(ダブルクォートで囲む)
* const t = "id,comment\n1,\"line1\nline2\"";
* const r = NTCsv.parse(t);
* // r => [["id","comment"],["1","line1\nline2"]]
*/
static parse(text, separator) {
const iseparator = separator === undefined ? "," : separator;
// 改行コードの正規化
const itext = text.replace(/\r\n?|\n/g, "\n");
const CODE_SEPARATOR = iseparator.charCodeAt(0);
const CODE_CR = 0x0d;
const CODE_LF = 0x0a;
const CODE_DOUBLEQUOTES = 0x22;
const out = [];
const length = itext.length;
let element = "";
let count_rows = 0;
let count_columns = 0;
let isnextelement = false;
let isnextline = false;
for (let i = 0; i < length; i++) {
let code = itext.charCodeAt(i);
// 複数行なら一気に全て読み込んでしまう(1文字目がダブルクォーテーションかどうか)
if (code === CODE_DOUBLEQUOTES && element.length === 0) {
i++;
for (; i < length; i++) {
code = itext.charCodeAt(i);
if (code === CODE_DOUBLEQUOTES) {
// フィールドの終了か?
// 文字としてのダブルクォーテーションなのか
if (i + 1 !== length - 1) {
if (itext.charCodeAt(i + 1) === CODE_DOUBLEQUOTES) {
i++;
element += '"';
} else {
break;
}
} else {
break;
}
} else {
element += itext.charAt(i);
}
}
}
// 複数行以外なら1文字ずつ解析
else {
switch (code) {
case CODE_SEPARATOR:
isnextelement = true;
break;
case CODE_CR:
case CODE_LF:
isnextline = true;
break;
default:
break;
}
if (isnextelement) {
isnextelement = false;
if (out[count_rows] === undefined) {
out[count_rows] = [];
}
out[count_rows][count_columns] = element;
element = "";
count_columns += 1;
} else if (isnextline) {
isnextline = false;
//文字があったり、改行がある場合は処理
//例えば CR+LF や 最後のフィールド で改行しているだけなどは無視できる
if (element !== "" || count_columns !== 0) {
if (out[count_rows] === undefined) {
out[count_rows] = [];
}
out[count_rows][count_columns] = element;
element = "";
count_rows += 1;
count_columns = 0;
}
} else {
element += itext.charAt(i);
}
}
// 最終行に改行がない場合
if (i === length - 1) {
if (count_columns !== 0) {
out[count_rows][count_columns] = element;
}
}
}
return out;
}
/**
* 2次元配列からCSVテキストを作成します。
*
* - フィールド内に `"` または 改行(`\r`/`\n`) または 区切り(`,` デフォルト)またはタブが含まれる場合、
* 当該フィールドをダブルクォートで囲み、内部の `"` は `""` にエスケープします。
* - 改行コードは `newline` 引数で指定できます(デフォルトは `"\r\n"`)。
*
* @param {Array<Array<string>>} csv_array - CSVの各行を要素に持つ2次元配列
* @param {string} [separator=","] - 区切り文字
* @param {string} [newline="\r\n"] - 出力時の改行コード
* @returns {string} CSVテキスト
*
* @example
* const rows = [["name","note"],["Alice","He said \"Hello\""]];
* const csv = NTCsv.create(rows);
* // csv => "name,note\r\n\"Alice\",\"He said \"\"Hello\"\"\"\r\n"
*
* @example
* // セミコロン区切り & LF改行
* const rows = [["id","memo"],["1","hello;world"]];
* const csv = NTCsv.create(rows, ";", "\n");
* // csv => "id;memo\n\"1\";\"hello;world\"\n" (必要箇所がクォートされる)
*/
static create(csv_array, separator, newline) {
const iseparator = separator === undefined ? "," : separator;
const inewline = newline === undefined ? "\r\n" : newline;
let out = "";
const escape = /["\r\n,\t]/;
if (csv_array !== undefined) {
for (let i = 0; i < csv_array.length; i++) {
if (csv_array[i] !== undefined) {
for (let j = 0; j < csv_array[i].length; j++) {
let element = csv_array[i][j];
if (escape.test(element)) {
element = element.replace(/"/g, '""');
element = '"' + element + '"';
}
out += element;
if (j !== csv_array[i].length - 1) {
out += iseparator;
}
}
}
out += inewline;
}
}
return out;
}
/**
* 1行目に列名があるCSV配列を、オブジェクトの配列(JSON配列)に変換します。
*
* - `csv_array[0]` をキー配列として使用します。
* - 2行目以降は、対応するキー名を持つオブジェクトにマッピングされます。
* - 列数が合わない場合は、存在するキーに対応する範囲でプロパティが作成されます(不足分は `undefined`)。
*
* @param {Array<Array<string>>} csv_array - 先頭行がヘッダーのCSV配列
* @returns {Array<Object<string, string>>} キー:ヘッダー名、値:セル文字列のオブジェクト配列
*
* @example
* const rows = [["name","age"],["Alice","30"],["Bob","25"]];
* const json = NTCsv.toJSONArrayFromCSVArray(rows);
* // json => [{ name: "Alice", age: "30" }, { name: "Bob", age: "25" }]
*
* @example
* // 列が足りない行がある場合
* const rows2 = [["a","b","c"],["1","2"],["3","4","5"]];
* const json2 = NTCsv.toJSONArrayFromCSVArray(rows2);
* // json2 => [{a:"1", b:"2", c:undefined}, {a:"3", b:"4", c:"5"}]
*/
static toJSONArrayFromCSVArray(csv_array) {
const title_line = csv_array[0];
const key_name = [];
for (let i = 0; i < title_line.length; i++) {
key_name.push(title_line[i]);
}
const json_array = [];
for (let i = 1; i < csv_array.length; i++) {
const line = csv_array[i];
/**
* @type {Object<string, string>}
* @private
*/
const json_data = {};
for (let j = 0; j < line.length; j++) {
json_data[key_name[j]] = line[j];
}
json_array.push(json_data);
}
return json_array;
}
/**
* 共通のキー構造を持つJSON配列を、CSV配列へ変換します。
*
* - `title_array` を指定しない場合は、`json_array[0]` のキー列挙順をタイトルとします。
* - 各オブジェクトの値は `title_array`(または自動抽出したキーリスト)の順に並べます。
* - オブジェクトに存在しないキーの値は `undefined`(結果のセルは `undefined`)になります。
*
* @param {Array<Object<string, string>>} json_array - 同一キー構造のオブジェクト配列
* @param {Array<string>} [title_array] - ヘッダーに使用するキー名の配列(順序固定に便利)
* @returns {Array<Array<string>>} 先頭行がヘッダーのCSV配列
*
* @example
* const json = [{ name: "Alice", age: "30" }, { name: "Bob", age: "25" }];
* const rows = NTCsv.toCSVArrayFromJSONArray(json);
* // rows => [["name","age"],["Alice","30"],["Bob","25"]]
*
* @example
* // 列順を指定したい場合
* const json2 = [{ name: "Alice", age: "30" }, { name: "Bob", age: "25" }];
* const rows2 = NTCsv.toCSVArrayFromJSONArray(json2, ["age","name"]);
* // rows2 => [["age","name"],["30","Alice"],["25","Bob"]]
*/
static toCSVArrayFromJSONArray(json_array, title_array) {
const csv_array = [];
let title_list = null;
if (title_array === undefined) {
title_list = [];
for (const key in json_array[0]) {
title_list.push(key);
}
} else {
title_list = title_array;
}
csv_array.push(title_list);
for (let i = 0; i < json_array.length; i++) {
const line = json_array[i];
const data_list = [];
for (let j = 0; j < title_list.length; j++) {
data_list.push(line[title_list[j]]);
}
csv_array.push(data_list);
}
return csv_array;
}
}