using System.Text;
namespace SharedKernel.Helpers;
///
/// RFC 4180 호환 CSV 직렬화/역직렬화. 외부 라이브러리 미사용 (antooza 코드 컨벤션).
/// 직렬화는 UTF-8 BOM 포함 (Excel 호환).
///
public static class CsvHelper
{
///
/// 행 시퀀스를 RFC 4180 CSV byte[] 로 직렬화. UTF-8 BOM 포함.
///
public static byte[] WriteUtf8Bom(IEnumerable 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;
}
/// RFC 4180 escape: 쉼표/따옴표/개행 포함 시 "..." 로 감싸고 내부 따옴표는 "" 로.
public static string EscapeCell(string? cell)
{
cell ??= "";
if (cell.Contains(',') || cell.Contains('"') || cell.Contains('\n') || cell.Contains('\r'))
{
return "\"" + cell.Replace("\"", "\"\"") + "\"";
}
return cell;
}
///
/// TextReader 에서 RFC 4180 CSV 행을 lazy 하게 읽는다.
/// UTF-8 BOM 이 앞에 있으면 자동으로 건너뜀 (StreamReader 가 처리해 주거나, 여기서 fallback).
///
public static IEnumerable Read(TextReader reader)
{
var currentRow = new List();
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();
hasContent = false;
}
}
else
{
currentField.Append(c);
hasContent = true;
}
}
}
// 마지막 행: 개행 없이 끝난 경우
if (hasContent || currentRow.Count > 0)
{
currentRow.Add(currentField.ToString());
yield return currentRow.ToArray();
}
}
}