| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- using System.Text;
- namespace SharedKernel.Helpers;
- /// <summary>
- /// RFC 4180 호환 CSV 직렬화/역직렬화. 외부 라이브러리 미사용 (antooza 코드 컨벤션).
- /// 직렬화는 UTF-8 BOM 포함 (Excel 호환).
- /// </summary>
- public static class CsvHelper
- {
- /// <summary>
- /// 행 시퀀스를 RFC 4180 CSV byte[] 로 직렬화. UTF-8 BOM 포함.
- /// </summary>
- public static byte[] WriteUtf8Bom(IEnumerable<string[]> rows)
- {
- var sb = new StringBuilder();
- foreach (var row in rows)
- {
- for (var i = 0; i < row.Length; i++)
- {
- if (i > 0)
- {
- sb.Append(',');
- }
- sb.Append(EscapeCell(row[i]));
- }
- sb.Append("\r\n");
- }
- var data = Encoding.UTF8.GetBytes(sb.ToString());
- var bom = new byte[] { 0xEF, 0xBB, 0xBF };
- var result = new byte[bom.Length + data.Length];
- Buffer.BlockCopy(bom, 0, result, 0, bom.Length);
- Buffer.BlockCopy(data, 0, result, bom.Length, data.Length);
- return result;
- }
- /// <summary>RFC 4180 escape: 쉼표/따옴표/개행 포함 시 "..." 로 감싸고 내부 따옴표는 "" 로.</summary>
- public static string EscapeCell(string? cell)
- {
- cell ??= "";
- if (cell.Contains(',') || cell.Contains('"') || cell.Contains('\n') || cell.Contains('\r'))
- {
- return "\"" + cell.Replace("\"", "\"\"") + "\"";
- }
- return cell;
- }
- /// <summary>
- /// TextReader 에서 RFC 4180 CSV 행을 lazy 하게 읽는다.
- /// UTF-8 BOM 이 앞에 있으면 자동으로 건너뜀 (StreamReader 가 처리해 주거나, 여기서 fallback).
- /// </summary>
- public static IEnumerable<string[]> Read(TextReader reader)
- {
- var currentRow = new List<string>();
- var currentField = new StringBuilder();
- var inQuotes = false;
- var hasContent = false;
- var atStart = true;
- int ch;
- while ((ch = reader.Read()) >= 0)
- {
- var c = (char)ch;
- // 파일 첫머리 BOM (U+FEFF) 건너뜀 (StreamReader 가 못 처리한 경우 대비)
- if (atStart)
- {
- atStart = false;
- if (c == '')
- {
- continue;
- }
- }
- if (inQuotes)
- {
- if (c == '"')
- {
- var next = reader.Peek();
- if (next == '"')
- {
- currentField.Append('"');
- reader.Read();
- }
- else
- {
- inQuotes = false;
- }
- }
- else
- {
- currentField.Append(c);
- }
- hasContent = true;
- }
- else
- {
- if (c == '"')
- {
- inQuotes = true;
- hasContent = true;
- }
- else if (c == ',')
- {
- currentRow.Add(currentField.ToString());
- currentField.Clear();
- hasContent = true;
- }
- else if (c == '\r')
- {
- // \r\n 정상 처리: \n 이 뒤에 오면 그때 행 종료
- }
- else if (c == '\n')
- {
- if (hasContent || currentRow.Count > 0)
- {
- currentRow.Add(currentField.ToString());
- currentField.Clear();
- yield return currentRow.ToArray();
- currentRow = new List<string>();
- hasContent = false;
- }
- }
- else
- {
- currentField.Append(c);
- hasContent = true;
- }
- }
- }
- // 마지막 행: 개행 없이 끝난 경우
- if (hasContent || currentRow.Count > 0)
- {
- currentRow.Add(currentField.ToString());
- yield return currentRow.ToArray();
- }
- }
- }
|