소스 검색

implement csv lib test

tripeur 4 년 전
부모
커밋
547ef8ce26
2개의 변경된 파일106개의 추가작업 그리고 10개의 파일을 삭제
  1. 72 0
      src/utils/__tests__/csv.test.ts
  2. 34 10
      src/utils/csv.ts

+ 72 - 0
src/utils/__tests__/csv.test.ts

@@ -0,0 +1,72 @@
+import csv from "../csv";
+
+test("CSV escape test", () => {
+  expect(csv.escapeCsvField("e,e")).toBe('"e,e"');
+  expect(csv.escapeCsvField("e\ne")).toBe('"e\ne"');
+  expect(csv.escapeCsvField("e\re")).toBe('"e\re"');
+  expect(csv.escapeCsvField('e"e')).toBe('"e""e"');
+});
+
+test("CSV escape test _ separator", () => {
+  const o = { separator: ";" };
+  expect(csv.escapeCsvField("e,e", o)).toBe("e,e");
+  expect(csv.escapeCsvField("e;e", o)).toBe('"e;e"');
+  expect(csv.escapeCsvField("e\ne", o)).toBe('"e\ne"');
+  expect(csv.escapeCsvField("e\re", o)).toBe('"e\re"');
+  expect(csv.escapeCsvField('e"e', o)).toBe('"e""e"');
+});
+
+test("CSV escape test _ delimiter", () => {
+  const o = { delimiter: "'" };
+  expect(csv.escapeCsvField("e,e", o)).toBe("'e,e'");
+  expect(csv.escapeCsvField("e;e", o)).toBe("e;e");
+  expect(csv.escapeCsvField("e\ne", o)).toBe("'e\ne'");
+  expect(csv.escapeCsvField("e\re", o)).toBe("'e\re'");
+  expect(csv.escapeCsvField('e"e', o)).toBe('e"e');
+  expect(csv.escapeCsvField("e'e", o)).toBe("'e''e'");
+});
+
+test("standard CSV parsing", () => {
+  const input = `1,2,3,4
+1,2,3,4
+`;
+  const output = [
+    ["1", "2", "3", "4"],
+    ["1", "2", "3", "4"],
+  ];
+  expect(csv.toArray(input)).toStrictEqual(output);
+});
+test("multiline CSV parsing", () => {
+  const output = csv.toArray(`1,2,3,"4\r\n 1",2,3,4
+`);
+  const expected = [["1", "2", "3", "4\r\n 1", "2", "3", "4"]];
+
+  expect(output).toStrictEqual(expected);
+});
+test("CSV using separator in string parsing", () => {
+  const output = csv.toArray(`1,2,3,"4,5"
+1,2,3,4
+`);
+  const expected = [
+    ["1", "2", "3", "4,5"],
+    ["1", "2", "3", "4"],
+  ];
+
+  expect(output).toStrictEqual(expected);
+});
+
+test("CSV without ending carrige return", () => {
+  const output = csv.toArray(`1,2,3`);
+  const expected = [["1", "2", "3"]];
+
+  expect(output).toStrictEqual(expected);
+
+  expect(csv.toArray(``)).toStrictEqual([]);
+});
+test("CSV with blanck last line", () => {
+  const output = csv.toArray(`1,2,3
+     `);
+  const expected = [["1", "2", "3"]];
+
+  expect(output).toStrictEqual(expected);
+});

+ 34 - 10
src/utils/csv.ts

@@ -1,9 +1,10 @@
 enum csvState {
   parseValue,
+  parseValueStart,
   parseDelimitedValue,
   parseEntry,
 }
-type csvOptions = {
+export type csvOptions = {
   delimiter: string;
   separator: string;
 };
@@ -11,28 +12,47 @@ const defaultoptions: csvOptions = {
   delimiter: '"',
   separator: ",",
 };
-const parseCSV = function (str: string, options: Partial<csvOptions> = defaultoptions) {
-  const o = options ? { ...options, ...defaultoptions } : defaultoptions;
+const escapeCsvField = function (
+  s: string | number,
+  options: Partial<csvOptions> = defaultoptions
+): string {
+  const o = options ? { ...defaultoptions, ...options } : defaultoptions;
+  const d = o.delimiter;
+  if (typeof s == "number") {
+    return s.toString();
+  } else {
+    return s.match(`[${o.separator + d}\r\n]`) ? d + s.replace(d, d + d) + d : s;
+  }
+};
+const parseCSV = function (
+  str: string,
+  options: Partial<csvOptions> = defaultoptions
+): Array<Array<string>> {
+  const o = options ? { ...defaultoptions, ...options } : defaultoptions;
   const delimiter = o.delimiter;
   const separator = o.separator;
   const output = [];
   let currentValue = "";
   let currentEntry = [];
-  let state: csvState = csvState.parseValue;
+  let state: csvState = csvState.parseValueStart;
   let i = 0;
   while (i < str.length) {
     const c = str[i];
     if (state == csvState.parseEntry) {
-      if (c == delimiter) {
-        state = csvState.parseDelimitedValue;
-        i++;
-      } else if (c == separator) {
+      if (c == separator) {
         i++;
-        state = csvState.parseValue;
+        state = csvState.parseValueStart;
       } else if (c == "\r" || c == "\n") {
         output.push(currentEntry);
         currentEntry = [];
         i += str[i + 1] == "\n" ? 2 : 1;
+      } else {
+        state = csvState.parseValueStart;
+      }
+    } else if (state == csvState.parseValueStart) {
+      if (c == delimiter) {
+        i++;
+        state = csvState.parseDelimitedValue;
       } else {
         state = csvState.parseValue;
       }
@@ -61,6 +81,10 @@ const parseCSV = function (str: string, options: Partial<csvOptions> = defaultop
       i++;
     }
   }
+  if (currentValue != "") currentEntry.push(currentValue);
+  const emptyEntry =
+    currentEntry.length == 0 || (currentEntry.length == 1 && currentEntry[0].trim() == "");
+  if (!emptyEntry) output.push(currentEntry);
   return output;
 };
 const toObject = function (str: string, options: Partial<csvOptions> = defaultoptions) {
@@ -79,4 +103,4 @@ const ArraytoObject = function (arr: Array<Array<string>>) {
   }
   return output;
 };
-export default { toObject, toArray: parseCSV, ArraytoObject };
+export default { escapeCsvField, toObject, toArray: parseCSV, ArraytoObject };