diff --git a/src/CodeOfChaos.Lucide/package-lock.json b/src/CodeOfChaos.Lucide/package-lock.json index 05c69fb..59a83bb 100644 --- a/src/CodeOfChaos.Lucide/package-lock.json +++ b/src/CodeOfChaos.Lucide/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "lucide-static": "^0.454.0" + "lucide-static": "^0.468.0" } }, "node_modules/lucide-static": { - "version": "0.454.0", - "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-0.454.0.tgz", - "integrity": "sha512-Jr6Xbn/WOXUy5ULRp191s+JUEqoHxEBIG6tlqEZw7sHnRVIgfIKF8T5VsoOb63p/oODGdEA4BRZ+qtaVFXt/6A==", + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-0.468.0.tgz", + "integrity": "sha512-JvpWui2umxRyEVMoETfMzb+qKqibV/sdoqJbKmW1JLdkuhXluJKoO6NqCbvCK/vAbUuH5bTEFD4T6uECsrNcnA==", "license": "ISC" } } diff --git a/src/CodeOfChaos.Lucide/package.json b/src/CodeOfChaos.Lucide/package.json index 6e27e33..000886c 100644 --- a/src/CodeOfChaos.Lucide/package.json +++ b/src/CodeOfChaos.Lucide/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "lucide-static": "^0.454.0" + "lucide-static": "^0.468.0" } } diff --git a/src/CodeOfChaos.Parsers.Csv.Sample/Program.cs b/src/CodeOfChaos.Parsers.Csv.Sample/Program.cs index 6106c21..036b667 100644 --- a/src/CodeOfChaos.Parsers.Csv.Sample/Program.cs +++ b/src/CodeOfChaos.Parsers.Csv.Sample/Program.cs @@ -1,26 +1,24 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Parsers; - namespace CodeOfChaos.Parsers.Csv.Sample; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- public static class Program { public async static Task Main(string[] args) { - var reader = new CsvReader(cfg => { + CsvParser reader = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); - IAsyncEnumerable dataEnumerable = reader.FromCsvFileAsync("AvondSchool.csv"); + IAsyncEnumerable dataEnumerable = reader.ToEnumerableAsync("AvondSchool.csv"); await foreach (EveningClassData record in dataEnumerable) { Console.WriteLine(record.Geometry); } - var writer = new CsvDictionaryWriter(config => { + CsvParser parser = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -30,6 +28,6 @@ public async static Task Main(string[] args) { ["data"] = "something" }; - await writer.WriteToFileAsync("test.csv", [data]); + await parser.ParseToFileAsync("test.csv", [data]); } } diff --git a/src/CodeOfChaos.Parsers.Csv/CodeOfChaos.Parsers.Csv.csproj b/src/CodeOfChaos.Parsers.Csv/CodeOfChaos.Parsers.Csv.csproj index 5376d90..681d8a9 100644 --- a/src/CodeOfChaos.Parsers.Csv/CodeOfChaos.Parsers.Csv.csproj +++ b/src/CodeOfChaos.Parsers.Csv/CodeOfChaos.Parsers.Csv.csproj @@ -4,10 +4,29 @@ net9.0 enable enable + + true + CodeOfChaos.Parsers.Csv + 2.0.0 + Anna Sas + A small to parse CSV files + https://github.com/code-of-chaos/code_of_chaos-cs + parser csv + true + true + true + embedded + LICENSE + README.md + + + + + - + \ No newline at end of file diff --git a/src/CodeOfChaos.Parsers.Csv/Contracts/ICsvReader.cs b/src/CodeOfChaos.Parsers.Csv/Contracts/ICsvReader.cs index 3351746..4884534 100644 --- a/src/CodeOfChaos.Parsers.Csv/Contracts/ICsvReader.cs +++ b/src/CodeOfChaos.Parsers.Csv/Contracts/ICsvReader.cs @@ -8,6 +8,6 @@ namespace CodeOfChaos.Parsers.Csv.Contracts; public interface ICsvReader { public IEnumerable FromCsvFile(string filePath); public IEnumerable FromCsvString(string data); - public IAsyncEnumerable FromCsvFileAsync(string filePath); - public IAsyncEnumerable FromCsvStringAsync(string data); + public IAsyncEnumerable FromCsvFileAsync(string filePath, CancellationToken ct = default); + public IAsyncEnumerable FromCsvStringAsync(string data, CancellationToken ct = default); } diff --git a/src/CodeOfChaos.Parsers.Csv/CsvParser.cs b/src/CodeOfChaos.Parsers.Csv/CsvParser.cs index 5b5a29c..bb431ed 100644 --- a/src/CodeOfChaos.Parsers.Csv/CsvParser.cs +++ b/src/CodeOfChaos.Parsers.Csv/CsvParser.cs @@ -1,19 +1,571 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using CodeOfChaos.Extensions; +using CodeOfChaos.Parsers.Csv.Attributes; +using System.Reflection; +using System.Runtime.CompilerServices; + namespace CodeOfChaos.Parsers.Csv; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public abstract class CsvParser(Action configAction) { - public CsvParserConfig Config { get; } = FromConfig(configAction); +/// +/// Provides functionality to parse CSV files into various collection types. +/// +public class CsvParser(CsvParserConfig config) : ICsvParser { // ----------------------------------------------------------------------------------------------------------------- // Constructors // ----------------------------------------------------------------------------------------------------------------- - private static CsvParserConfig FromConfig(Action configAction) { + /// + /// Creates an instance of using the specified configuration action. + /// + /// + /// An action to configure the used by the parser. + /// The action allows the caller to specify options such as column delimiter, quote character, etc. + /// + /// + /// A configured instance of ready to parse CSV data according to + /// the specified configuration. + /// + public static CsvParser FromConfig(Action configAction) { var config = new CsvParserConfig(); configAction(config); - return config; + return new CsvParser(config); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Input Methods + // ----------------------------------------------------------------------------------------------------------------- + #region ToEnumerable + /// + public IEnumerable ToEnumerable(TReader reader) + where T : class, new() + where TReader : TextReader => FromTextReader(reader); + + /// + public IEnumerable ToEnumerable(string filePath) + where T : class, new() => FromTextReader(new StreamReader(filePath)); + + /// + public IAsyncEnumerable ToEnumerableAsync(TReader reader, CancellationToken ct = default) + where T : class, new() + where TReader : TextReader => FromTextReaderAsync(reader, ct); + + /// + public IAsyncEnumerable ToEnumerableAsync(string filePath, CancellationToken ct = default) + where T : class, new() => FromTextReaderAsync(new StreamReader(filePath), ct); + #endregion + #region ToArray + /// + public T[] ToArray(TReader reader) + where T : class, new() + where TReader : TextReader => FromTextReader(reader).ToArray(); + + /// + public T[] ToArray(string filePath) + where T : class, new() => FromTextReader(new StreamReader(filePath)).ToArray(); + + /// + public async ValueTask ToArrayAsync(TReader reader, CancellationToken ct = default) + where T : class, new() + where TReader : TextReader { + var results = new List(config.InitialCapacity); + + await foreach (T item in FromTextReaderAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results.ToArray(); + } + + /// + public async ValueTask ToArrayAsync(string filePath, CancellationToken ct = default) where T : class, new() { + using var reader = new StreamReader(filePath); + var results = new List(config.InitialCapacity); + + await foreach (T item in FromTextReaderAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results.ToArray(); + } + #endregion + #region ToList + /// + public List ToList(TReader reader) + where T : class, new() + where TReader : TextReader => FromTextReader(reader).ToList(); + + /// + public List ToList(string filePath) + where T : class, new() { + var reader = new StreamReader(filePath); + return FromTextReader(reader).ToList(); + } + + /// + public async ValueTask> ToListAsync(TReader reader, CancellationToken ct = default) + where T : class, new() + where TReader : TextReader { + var results = new List(config.InitialCapacity); + + await foreach (T item in FromTextReaderAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results.ToList(); + } + + /// + public async ValueTask> ToListAsync(string filePath, CancellationToken ct = default) where T : class, new() { + using var reader = new StringReader(filePath); + var results = new List(config.InitialCapacity); + + await foreach (T item in FromTextReaderAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results.ToList(); + } + #endregion + #region ToDictionaryEnumerable + /// + public IEnumerable> ToDictionaryEnumerable(TReader reader) + where TReader : TextReader => FromTextReaderToDictionary(reader); + + /// + public IEnumerable> ToDictionaryEnumerable(string filePath) + => FromTextReaderToDictionary(new StreamReader(filePath)); + + /// + public IAsyncEnumerable> ToDictionaryEnumerableAsync(TReader reader, CancellationToken ct = default) + where TReader : TextReader => FromTextReaderToDictionaryAsync(reader, ct); + + /// + public IAsyncEnumerable> ToDictionaryEnumerableAsync(string filePath, CancellationToken ct = default) + => FromTextReaderToDictionaryAsync(new StreamReader(filePath), ct); + #endregion + #region ToDictionaryArray + /// + public Dictionary[] ToDictionaryArray(TReader reader) + where TReader : TextReader => FromTextReaderToDictionary(reader).ToArray(); + + /// + public Dictionary[] ToDictionaryArray(string filePath) => FromTextReaderToDictionary(new StreamReader(filePath)).ToArray(); + + /// + public async ValueTask[]> ToDictionaryArrayAsync(TReader reader, CancellationToken ct = default) + where TReader : TextReader { + var results = new List>(config.InitialCapacity); + + await foreach (Dictionary item in FromTextReaderToDictionaryAsync(reader, ct)) { + results.Add(item); + } + return results.ToArray(); + } + + /// + public async ValueTask[]> ToDictionaryArrayAsync(string filePath, CancellationToken ct = default) { + using var reader = new StreamReader(filePath); + var results = new List>(config.InitialCapacity); + + await foreach (Dictionary item in FromTextReaderToDictionaryAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results.ToArray(); + } + #endregion + #region ToDictionaryList + /// + public List> ToDictionaryList(TReader reader) + where TReader : TextReader => FromTextReaderToDictionary(reader).ToList(); + + /// + public List> ToDictionaryList(string filePath) + => FromTextReaderToDictionary(new StreamReader(filePath)).ToList(); + + /// + public async ValueTask>> ToDictionaryListAsync(TReader reader, CancellationToken ct = default) + where TReader : TextReader { + var results = new List>(config.InitialCapacity); + + await foreach (Dictionary item in FromTextReaderToDictionaryAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results; + } + + /// + public async ValueTask>> ToDictionaryListAsync(string filePath, CancellationToken ct = default) { + using var reader = new StringReader(filePath); + var results = new List>(config.InitialCapacity); + await foreach (Dictionary item in FromTextReaderToDictionaryAsync(reader, ct)) { + results.Add(item); + } + + results.TrimExcess(); + return results; + } + #endregion + + // ----------------------------------------------------------------------------------------------------------------- + // Output + // ----------------------------------------------------------------------------------------------------------------- + #region ParseToString + /// + public string ParseToString(IEnumerable data) { + using var writer = new StringWriter(); + FromDataToTextWriter(writer, data); + return writer.ToString(); + } + + /// + public string ParseToString(IEnumerable> data) { + using var writer = new StringWriter(); + FromDictionaryToTextWriter(writer, data); + return writer.ToString(); + } + + /// + public async ValueTask ParseToStringAsync(IEnumerable data) { + await using var writer = new StringWriter(); + await FromDataToTextWriterAsync(writer, data); + return writer.ToString(); + } + + /// + public async ValueTask ParseToStringAsync(IEnumerable> data) { + await using var writer = new StringWriter(); + await FromDictionaryToTextWriterAsync(writer, data); + return writer.ToString(); + } + #endregion + #region ParseToFileAsync + /// + public void ParseToFile(string filePath, IEnumerable data) { + using var writer = new StreamWriter(filePath); + FromDataToTextWriter(writer, data); + } + + /// + public void ParseToFile(string filePath, IEnumerable> data) { + using var writer = new StreamWriter(filePath); + FromDictionaryToTextWriter(writer, data); + } + + /// + public async ValueTask ParseToFileAsync(string filePath, IEnumerable data) { + await using var writer = new StreamWriter(filePath); + await FromDataToTextWriterAsync(writer, data); + } + + /// + public async ValueTask ParseToFileAsync(string filePath, IEnumerable> data) { + await using var writer = new StreamWriter(filePath); + await FromDictionaryToTextWriterAsync(writer, data); + } + #endregion + #region ParseToWriter + /// + public void ParseToWriter(IEnumerable data, TWriter writer) + where TWriter : TextWriter => FromDataToTextWriter(writer, data); + + /// + public void ParseToWriter(IEnumerable> data, TWriter writer) + where TWriter : TextWriter => FromDictionaryToTextWriter(writer, data); + + /// + public async ValueTask ParseToWriterAsync(IEnumerable data, TWriter writer) + where TWriter : TextWriter => await FromDataToTextWriterAsync(writer, data); + + /// + public async ValueTask ParseToWriterAsync(IEnumerable> data, TWriter writer) + where TWriter : TextWriter => await FromDictionaryToTextWriterAsync(writer, data); + #endregion + + // ----------------------------------------------------------------------------------------------------------------- + // Actual Parsers + // ----------------------------------------------------------------------------------------------------------------- + #region Generic Type Parsing + private IEnumerable FromTextReader(TReader reader) + where T : class, new() + where TReader : TextReader { + string[] headerColumns = []; + int batchSize = config.BatchSize; + var batch = new List(config.BatchSize); + + if (reader.ReadLine() is {} lineFull) { + headerColumns = lineFull.Split(config.ColumnSplit); + } + + while (true) { + string? line = null; + for (int i = 0; i < batchSize && (line = reader.ReadLine()) != null; i++) { + string[] values = line.Split(config.ColumnSplit); + var obj = new T(); + SetPropertyFromCsvColumn(obj, headerColumns, values); + batch.Add(obj); + } + + foreach (T item in batch) { + yield return item; + } + + batch.Clear(); + if (line == null) break; + } + } + + private async IAsyncEnumerable FromTextReaderAsync(TReader reader, [EnumeratorCancellation] CancellationToken ct = default) + where T : class, new() + where TReader : TextReader { + string[] headerColumns = []; + int batchSize = config.BatchSize; + var batch = new List(config.BatchSize); + + if (await reader.ReadLineAsync(ct) is {} lineFull) { + headerColumns = lineFull.Split(config.ColumnSplit); + } + + while (!ct.IsCancellationRequested) { + string? line = null; + for (int i = 0; i < batchSize && (line = await reader.ReadLineAsync(ct)) != null; i++) { + string[] values = line.Split(config.ColumnSplit); + var obj = new T(); + + SetPropertyFromCsvColumn(obj, headerColumns, values); + batch.Add(obj); + } + + foreach (T item in batch) { + yield return item; + } + + batch.Clear(); + if (line == null) break; + } + } + + private void SetPropertyFromCsvColumn(T? value, string[] headerColumns, string[] values) where T : class, new() { + if (value is null) return; + + foreach (PropertyInfo prop in value.GetType().GetProperties()) { + int columnIndex = Attribute.GetCustomAttribute(prop, typeof(CsvColumnAttribute)) is CsvColumnAttribute attribute + ? Array.IndexOf(headerColumns, attribute.Name) + : Array.IndexOf(headerColumns, prop.Name); + + if (columnIndex == -1) continue; + + try { + object propertyValue = Convert.ChangeType(values[columnIndex], prop.PropertyType); + prop.SetValue(value, propertyValue); + } + catch (Exception) { + if (!config.LogErrors) return; + + throw; + } + } + } + #endregion + #region Dictionary Parsing + private IEnumerable> FromTextReaderToDictionary(TReader reader) + where TReader : TextReader { + string[] headerColumns = []; + int batchSize = config.BatchSize; + var batch = new List>(); + if (reader.ReadLine() is {} lineFull) { + headerColumns = lineFull.Split(config.ColumnSplit); + } + + while (true) { + string? line = null; + for (int i = 0; i < batchSize && (line = reader.ReadLine()) != null; i++) { + string[] values = line.Split(config.ColumnSplit); + + var dict = new Dictionary(); + for (int j = 0; j < headerColumns.Length; j++) { + string value = values[j]; + dict[headerColumns[j]] = value.IsNotNullOrEmpty() ? value : null; + } + batch.Add(dict); + } + + foreach (Dictionary item in batch) { + yield return item; + } + + batch.Clear(); + if (line == null) break; + } + } + + private async IAsyncEnumerable> FromTextReaderToDictionaryAsync(TReader reader, [EnumeratorCancellation] CancellationToken ct = default) + where TReader : TextReader { + string[] headerColumns = []; + int batchSize = config.BatchSize; + var batch = new List>(); + if (await reader.ReadLineAsync(ct) is {} lineFull) { + headerColumns = lineFull.Split(config.ColumnSplit); + } + + while (true) { + string? line = null; + for (int i = 0; i < batchSize && (line = await reader.ReadLineAsync(ct)) != null; i++) { + string[] values = line.Split(config.ColumnSplit); + + var dict = new Dictionary(); + for (int j = 0; j < headerColumns.Length; j++) { + string value = values[j]; + dict[headerColumns[j]] = value.IsNotNullOrEmpty() ? value : null; + } + batch.Add(dict); + } + + foreach (Dictionary item in batch) { + yield return item; + } + + batch.Clear(); + if (line == null) break; + } + } + #endregion + + #region Generic Type Writer + private void FromDataToTextWriter(TWriter writer, IEnumerable data) + where TWriter : TextWriter { + // Write header row + T[] array = data as T[] ?? data.ToArray(); + PropertyInfo[] propertyInfos = GetCsvProperties(array.FirstOrDefault());// Dirty but it will work + + if (config.IncludeHeader) { + string[] headers = GetCsvHeaders(propertyInfos).ToArray(); + for (int i = 0; i < headers.Length; i++) { + writer.Write(headers[i]); + if (i < headers.Length - 1) writer.Write(config.ColumnSplit); + } + + writer.Write(Environment.NewLine); + } + + // Write data rows + foreach (T? obj in array) { + string[] values = GetCsvValues(obj, propertyInfos).ToArray(); + for (int i = 0; i < values.Length; i++) { + writer.Write(values[i]); + if (i < values.Length - 1) writer.Write(config.ColumnSplit); + } + + writer.Write(Environment.NewLine); + } + } + + private async Task FromDataToTextWriterAsync(TWriter writer, IEnumerable data) + where TWriter : TextWriter { + // Write header row + T[] array = data as T[] ?? data.ToArray(); + PropertyInfo[] propertyInfos = GetCsvProperties(array.FirstOrDefault()); + + if (config.IncludeHeader) { + string[] headers = GetCsvHeaders(propertyInfos).ToArray(); + for (int i = 0; i < headers.Length; i++) { + await writer.WriteAsync(headers[i]); + if (i < headers.Length - 1) await writer.WriteAsync(config.ColumnSplit); + } + + await writer.WriteAsync(Environment.NewLine); + } + + // Write data rows + foreach (T? obj in array) { + string[] values = GetCsvValues(obj, propertyInfos).ToArray(); + for (int i = 0; i < values.Length; i++) { + await writer.WriteAsync(values[i]); + if (i < values.Length - 1) await writer.WriteAsync(config.ColumnSplit); + } + + await writer.WriteAsync(Environment.NewLine); + } + } + + + private static PropertyInfo[] GetCsvProperties(T? obj) => obj? + .GetType() + .GetProperties() + .ToArray() + ?? []; + + private IEnumerable GetCsvHeaders(PropertyInfo[] propertyInfos) { + return propertyInfos + .Select(p => { + if (p.GetCustomAttribute() is not {} attribute) + return config.UseLowerCaseHeaders ? p.Name.ToLowerInvariant() : p.Name; + + return config.UseLowerCaseHeaders + ? attribute.NameLowerInvariant + : attribute.Name; + }); + } + + private static IEnumerable GetCsvValues(T? obj, PropertyInfo[] propertyInfos) { + if (obj is null) return []; + + PropertyInfo[] properties = propertyInfos.Length != 0 + ? propertyInfos + : obj.GetType().GetProperties(); + + return properties + .Select(p => p.GetValue(obj)?.ToString() ?? string.Empty); + } + #endregion + #region Dictionary Writer + private void FromDictionaryToTextWriter(TWriter writer, IEnumerable> data) + where TWriter : TextWriter { + IDictionary[] records = data as IDictionary[] ?? data.ToArray>(); + if (records.Length == 0) return; + + // Write header row + if (config.IncludeHeader) { + IDictionary firstDictionary = records.First(); + IEnumerable headers = firstDictionary.Keys; + writer.WriteLine(string.Join(config.ColumnSplit, headers)); + } + + // Write data rows + foreach (IDictionary dictionary in records) { + IEnumerable values = dictionary.Values.Select(value => value?.ToString() ?? string.Empty); + writer.WriteLine(string.Join(config.ColumnSplit, values)); + } + } + + private async Task FromDictionaryToTextWriterAsync(TWriter writer, IEnumerable> data) + where TWriter : TextWriter { + IDictionary[] records = data as IDictionary[] ?? data.ToArray>(); + if (records.Length == 0) return; + + // Write header row + if (config.IncludeHeader) { + IDictionary firstDictionary = records.First(); + IEnumerable headers = firstDictionary.Keys; + await writer.WriteLineAsync(string.Join(config.ColumnSplit, headers)); + } + + // Write data rows + foreach (IDictionary dictionary in records) { + IEnumerable values = dictionary.Values.Select(value => value?.ToString() ?? string.Empty); + await writer.WriteLineAsync(string.Join(config.ColumnSplit, values)); + } } + #endregion } diff --git a/src/CodeOfChaos.Parsers.Csv/CsvParserConfig.cs b/src/CodeOfChaos.Parsers.Csv/CsvParserConfig.cs index 1fda863..77e9e66 100644 --- a/src/CodeOfChaos.Parsers.Csv/CsvParserConfig.cs +++ b/src/CodeOfChaos.Parsers.Csv/CsvParserConfig.cs @@ -10,4 +10,6 @@ public class CsvParserConfig { public bool IncludeHeader = true; public bool LogErrors = false; public bool UseLowerCaseHeaders = false; + public int BatchSize = 100; + public int InitialCapacity = 1000; } diff --git a/src/CodeOfChaos.Parsers.Csv/ICsvParser.cs b/src/CodeOfChaos.Parsers.Csv/ICsvParser.cs new file mode 100644 index 0000000..740513e --- /dev/null +++ b/src/CodeOfChaos.Parsers.Csv/ICsvParser.cs @@ -0,0 +1,442 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace CodeOfChaos.Parsers.Csv; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Provides an interface for parsing CSV data from various sources and outputting CSV data to different destinations. +/// +public interface ICsvParser { + /// + /// Converts data from a text reader into an enumerable collection of a specified type. + /// The method reads from the provided text reader and maps the data to objects of type . + /// + /// + /// The type of objects to be created from the CSV data. Must be a class with a parameterless + /// constructor. + /// + /// The type of text reader used to read the input. Must derive from . + /// The text reader from which to read the CSV data. + /// An enumerable collection of objects of type representing the parsed CSV records. + IEnumerable ToEnumerable(TReader reader) + where T : class, new() + where TReader : TextReader; + + /// + /// Converts the CSV file specified by the file path into an enumerable collection of type T. + /// + /// The type of objects to be created from the CSV. Must be a class with a parameterless constructor. + /// The path of the CSV file to be parsed. + /// An enumerable collection of type T containing the parsed CSV data. + IEnumerable ToEnumerable(string filePath) + where T : class, new(); + + /// + /// Asynchronously parses CSV data from a text reader into an enumerable collection of objects of type + /// . + /// + /// The type of objects to be created from the CSV data. + /// The type of text reader being used to read the CSV data. + /// The text reader containing the CSV data to process. + /// A cancellation token to observe while awaiting the asynchronous operation. + /// An asynchronous enumerable of objects of type parsed from the CSV data. + IAsyncEnumerable ToEnumerableAsync(TReader reader, CancellationToken ct = default) + where T : class, new() + where TReader : TextReader; + + /// + /// Asynchronously parses a CSV file specified by the file path into an enumerable sequence of objects of type + /// . + /// + /// + /// The type of objects to be created from the CSV data. The type must have a parameterless + /// constructor. + /// + /// The path to the CSV file to be parsed. + /// An optional cancellation token that can be used to cancel the operation. + /// An asynchronous enumerable of objects of type parsed from the CSV file. + IAsyncEnumerable ToEnumerableAsync(string filePath, CancellationToken ct = default) + where T : class, new(); + + /// + /// Converts the data read from a given TextReader into an array of a specified type. + /// + /// + /// The type of objects to create from the CSV data. Must be a reference type with a parameterless + /// constructor. + /// + /// The type of TextReader to read data from. + /// An instance of a TextReader from which to read the CSV data. + /// An array of objects of type T, representing the CSV data. + T[] ToArray(TReader reader) + where T : class, new() + where TReader : TextReader; + + /// + /// Reads data from a specified file path and converts it into an array of objects of type . + /// + /// + /// The type of objects to deserialize each line into. Must be a class with a parameterless + /// constructor. + /// + /// The path to the CSV file to be processed. + /// An array of objects of type representing the data from the CSV file. + T[] ToArray(string filePath) + where T : class, new(); + + /// + /// Asynchronously converts the contents of the provided TextReader to an array of objects of type T. + /// This operation can be cancelled using the provided CancellationToken. + /// + /// + /// The type of objects to be created from the CSV data. Must be a class with a parameterless + /// constructor. + /// + /// The type of TextReader used to read the CSV data. + /// The TextReader instance to read data from. + /// A CancellationToken used to cancel the asynchronous operation. + /// A ValueTask representing the asynchronous operation, containing an array of objects of type T. + ValueTask ToArrayAsync(TReader reader, CancellationToken ct = default) + where T : class, new() + where TReader : TextReader; + + /// + /// Asynchronously parses a CSV file and converts the data into an array of objects of type . + /// + /// + /// The type of objects to be created from the CSV data. Must be a class with a parameterless + /// constructor. + /// + /// The path to the CSV file to be parsed. + /// A CancellationToken that can be used to cancel the asynchronous operation. + /// + /// A task representing the asynchronous operation that contains an array of objects of type + /// parsed from the CSV file. + /// + ValueTask ToArrayAsync(string filePath, CancellationToken ct = default) + where T : class, new(); + + /// + /// Converts the CSV data read by the specified TextReader into a list of objects of type T. + /// + /// The type of objects to create from the CSV data. Must be a class with a parameterless constructor. + /// The type of the reader to use, which must inherit from TextReader. + /// The TextReader instance to read CSV data from. + /// A list of objects of type T created from the CSV data. + List ToList(TReader reader) + where T : class, new() + where TReader : TextReader; + + /// + /// Reads a CSV file and converts its content into a list of objects of type T. + /// + /// The type of the objects to create from each row of the CSV. + /// The path to the CSV file to read and parse. + /// A list of objects of type T, each representing a row in the CSV file. + List ToList(string filePath) + where T : class, new(); + + /// + /// Asynchronously processes a CSV file using the specified and converts the data into a + /// list of type . + /// + /// The type of objects to be created from the CSV data. + /// + /// The type of the reader used to parse the CSV data, which must be derived from + /// . + /// + /// The reader used to read the CSV data. + /// A CancellationToken to observe while waiting for the task to complete. + /// + /// A ValueTask representing the asynchronous operation, containing a list of objects of type + /// created from the CSV data. + /// + ValueTask> ToListAsync(TReader reader, CancellationToken ct = default) + where T : class, new() + where TReader : TextReader; + + /// + /// Asynchronously parses a CSV file and converts its contents into a list of objects of type + /// . + /// + /// The type of objects to create from CSV rows. Must be a class with a parameterless constructor. + /// The path to the CSV file to be parsed. + /// A token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous operation. The task result contains a list of objects of type + /// populated from the parsed CSV data. + /// + ValueTask> ToListAsync(string filePath, CancellationToken ct = default) + where T : class, new(); + + /// + /// Converts the CSV data from a text reader into an enumerable collection of dictionaries, + /// with each dictionary representing a row in the CSV file, where the keys are column headers + /// and the values are the corresponding row data. + /// + /// The type of the text reader. + /// + /// The text reader from which to read the CSV data. It should not be null and must be positioned at + /// the start of the data. + /// + /// + /// An enumerable of dictionaries, where each dictionary maps column names to their respective values for each row + /// in the CSV. + /// + IEnumerable> ToDictionaryEnumerable(TReader reader) + where TReader : TextReader; + + /// + /// Converts the contents of a CSV file at the specified file path into a sequence of dictionaries. + /// Each dictionary represents a row in the CSV, with keys being column headers and values being the cell contents. + /// + /// The file path to the CSV file to be parsed. + /// An IEnumerable of dictionaries, where each dictionary maps column headers to cell values. + IEnumerable> ToDictionaryEnumerable(string filePath); + + /// + /// Asynchronously parses a CSV data source provided by a + /// into an enumerable sequence of dictionaries. Each dictionary contains key-value + /// pairs where keys are column headers and values are the corresponding row values. + /// + /// The type of the text reader, constrained to . + /// An instance of the to read CSV data from. + /// A to observe while waiting for the task to complete. + /// An async enumerable sequence of dictionaries, each representing a row from the CSV. + IAsyncEnumerable> ToDictionaryEnumerableAsync(TReader reader, CancellationToken ct = default) + where TReader : TextReader; + + /// + /// Asynchronously parses a CSV file into an asynchronous enumerable of dictionaries, + /// where each dictionary represents a row and maps column headers to their respective values. + /// + /// The path to the CSV file to parse. + /// An optional cancellation token to observe while waiting for the task to complete. + /// An asynchronous enumerable of dictionaries, each representing a CSV row. + IAsyncEnumerable> ToDictionaryEnumerableAsync(string filePath, CancellationToken ct = default); + + /// + /// Converts the content from the provided text reader into an array of dictionaries, + /// where each dictionary represents a row in the CSV, mapping column names to their corresponding values. + /// + /// The text reader from which to read the CSV data. + /// The type of the text reader, which must inherit from TextReader. + /// + /// An array of dictionaries, with each dictionary containing mappings from column names to their respective cell + /// values. + /// + Dictionary[] ToDictionaryArray(TReader reader) + where TReader : TextReader; + + /// + /// Converts the contents of a CSV file specified by the file path into an array of dictionaries, + /// where each dictionary represents a row and maps column headers to row values. + /// + /// The file path of the CSV file to be parsed. + /// + /// An array of dictionaries, each dictionary containing key-value pairs + /// where the keys are column headers and the values are the corresponding row values. + /// + Dictionary[] ToDictionaryArray(string filePath); + + /// + /// Asynchronously parses a CSV input from a reader into an array of dictionaries, where each dictionary's + /// keys represent the column names and values represent the corresponding field values for a row. + /// + /// The type of the text reader used to read the CSV input. + /// An instance of that provides access to the CSV data. + /// A cancellation token that can be used to cancel the asynchronous operation. + /// + /// A task representing the asynchronous operation, containing an array of dictionaries. Each dictionary + /// entry corresponds to a row from the CSV, with keys as column names and values as field values. + /// + ValueTask[]> ToDictionaryArrayAsync(TReader reader, CancellationToken ct = default) + where TReader : TextReader; + + /// + /// Asynchronously reads a CSV file from the specified file path and converts its contents into an array of + /// dictionaries, + /// where each dictionary represents a row with column names as keys and cell values as values. + /// + /// The path to the CSV file to be read. + /// Optional. A CancellationToken to observe while waiting for the task to complete. + /// + /// A task representing the asynchronous operation. The task result contains an array of dictionaries, each + /// representing a CSV row. + /// + ValueTask[]> ToDictionaryArrayAsync(string filePath, CancellationToken ct = default); + + /// + /// Converts the input from a TextReader into a list of dictionaries where each dictionary + /// represents a row from the CSV file. Each dictionary's key is the column header and the + /// value is the corresponding cell data for that row and column. + /// + /// The TextReader to read the CSV data from. + /// The type of the TextReader. + /// A list of dictionaries, where each dictionary represents a row from the CSV data. + List> ToDictionaryList(TReader reader) + where TReader : TextReader; + + /// + /// Converts the contents of a CSV file specified by the file path into a list of dictionaries. + /// Each dictionary represents a row in the CSV, with the keys being the column headers and the values being the + /// corresponding field values. + /// + /// The path to the CSV file to be parsed. + /// + /// A list where each element is a dictionary. Each dictionary maps column headers to their corresponding values + /// in the CSV file. + /// + List> ToDictionaryList(string filePath); + + /// + /// Asynchronously parses the content read by the specified reader into a list of dictionaries, + /// where each dictionary represents a row in the CSV file and maps column headers to their corresponding cell values. + /// + /// The type of the reader which must inherit from TextReader. + /// The TextReader used to read the CSV content. + /// A CancellationToken to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous operation. The task result contains a list of dictionaries, + /// where each dictionary contains key-value pairs mapping column headers to their current row cell values. + /// + ValueTask>> ToDictionaryListAsync(TReader reader, CancellationToken ct = default) + where TReader : TextReader; + + /// + /// Asynchronously parses a CSV file into a list of dictionaries, where each dictionary represents a row in the CSV + /// file, + /// with column names as keys and cell values as corresponding values. + /// + /// The path to the CSV file to parse. + /// An optional CancellationToken to cancel the operation. + /// A ValueTask representing the asynchronous operation, containing the list of dictionaries from the CSV file. + ValueTask>> ToDictionaryListAsync(string filePath, CancellationToken ct = default); + + /// + /// Converts the given enumerable collection of data into a CSV formatted string. + /// + /// The type of elements in the data collection. + /// The collection of data to be parsed into a CSV string. + /// A string representing the CSV formatted version of the input data. + string ParseToString(IEnumerable data); + + /// + /// Converts an enumerable collection of dictionaries into a CSV formatted string. + /// + /// + /// The enumerable collection of dictionaries where each dictionary represents a row in the CSV and each + /// key/value pair represents a column and its corresponding value. + /// + /// A string representing the CSV formatted data derived from the input collection of dictionaries. + string ParseToString(IEnumerable> data); + + /// + /// Asynchronously parses a given collection of data into a CSV formatted string. + /// + /// The type of the elements in the data collection to be parsed. + /// The collection of data to be parsed into a CSV string. + /// A task representing the asynchronous operation, with a result of the CSV formatted string. + ValueTask ParseToStringAsync(IEnumerable data); + + /// + /// Asynchronously parses a collection of dictionaries into a CSV formatted string. + /// + /// + /// A collection of dictionaries where each dictionary represents a row of CSV data. + /// The keys in the dictionary represent the column headers, and the associated values + /// represent the corresponding data entries for that row. + /// + /// + /// A task representing the asynchronous operation, with a string result that represents + /// the CSV formatted data derived from the input collection. + /// + ValueTask ParseToStringAsync(IEnumerable> data); + + /// + /// Writes a collection of data to a CSV file specified by the file path. + /// + /// The type of objects contained in the data collection. + /// The path to the file where the data will be written. + /// The collection of data objects to be written to the file. + void ParseToFile(string filePath, IEnumerable data); + + /// + /// Writes the provided data to a file at the specified file path in CSV format. + /// + /// The path to the file where the CSV data will be written. + /// + /// An enumerable collection of dictionaries where each dictionary represents a row of data, + /// with keys representing column names and values representing the corresponding cell data. + /// + void ParseToFile(string filePath, IEnumerable> data); + + /// + /// Asynchronously writes the provided data to a CSV file at the specified file path. + /// + /// The type of the data elements to be written to the file. + /// The path to the file where the data should be written. + /// The collection of data elements to be written to the CSV file. + /// A task that represents the asynchronous write operation. + ValueTask ParseToFileAsync(string filePath, IEnumerable data); + + /// + /// Asynchronously writes a collection of data in dictionary format to a CSV file. + /// + /// The path of the file where the data will be written. + /// An enumerable collection of dictionaries representing the CSV data to be written. + /// A ValueTask that represents the asynchronous write operation. + ValueTask ParseToFileAsync(string filePath, IEnumerable> data); + + /// + /// Writes the provided enumerable data to the specified text writer in CSV format. + /// + /// The type of elements in the data collection. + /// The type of the writer, which must derive from TextWriter. + /// The enumerable collection of data elements to be written. + /// The text writer to which the CSV formatted data will be written. + void ParseToWriter(IEnumerable data, TWriter writer) + where TWriter : TextWriter; + + /// + /// Writes a collection of dictionaries to a text writer in CSV format. + /// Each dictionary represents a row in the CSV, where the keys are column headers + /// and the values are the corresponding entries for that row. + /// + /// The type of the writer, which must inherit from . + /// + /// The enumerable collection of dictionaries to be written. Each dictionary contains key-value pairs + /// representing a CSV row. + /// + /// The writer to which the CSV data will be written. + void ParseToWriter(IEnumerable> data, TWriter writer) + where TWriter : TextWriter; + + /// + /// Asynchronously parses a collection of data objects into a CSV format and writes it to the specified text writer. + /// + /// The type of data objects contained in the IEnumerable. + /// The type of the text writer to which the CSV data will be written. + /// The collection of data objects to be serialized into CSV format. + /// The text writer where the CSV formatted data will be written. + /// A task that represents the asynchronous write operation. + ValueTask ParseToWriterAsync(IEnumerable data, TWriter writer) + where TWriter : TextWriter; + + /// + /// Asynchronously parses the given enumerable of data dictionaries and writes it to the specified text writer in CSV + /// format. + /// + /// The type of the text writer which must inherit from TextWriter. + /// + /// An enumerable collection of dictionaries representing the data to be written to the writer. Each + /// dictionary entry corresponds to a CSV row. + /// + /// The text writer where the CSV content will be written. + /// A ValueTask that represents the asynchronous write operation. + ValueTask ParseToWriterAsync(IEnumerable> data, TWriter writer) + where TWriter : TextWriter; + +} diff --git a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvDictionaryReader.cs b/src/CodeOfChaos.Parsers.Csv/Parsers/CsvDictionaryReader.cs deleted file mode 100644 index 8f61208..0000000 --- a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvDictionaryReader.cs +++ /dev/null @@ -1,75 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Extensions; -using CodeOfChaos.Parsers.Csv.Contracts; - -namespace CodeOfChaos.Parsers.Csv.Parsers; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class CsvDictionaryReader(Action configAction) : CsvParser(configAction), ICsvReader> { - public IEnumerable> FromCsvFile(string filePath) { - var reader = new StreamReader(filePath); - return FromTextReader(reader); - } - - public IEnumerable> FromCsvString(string data) { - var reader = new StringReader(data); - return FromTextReader(reader); - } - - public IAsyncEnumerable> FromCsvFileAsync(string filePath) { - var reader = new StreamReader(filePath); - return FromTextReaderAsync(reader); - } - - public IAsyncEnumerable> FromCsvStringAsync(string data) { - var reader = new StringReader(data); - return FromTextReaderAsync(reader); - } - - #region Helper Methods - private IEnumerable> FromTextReader(TextReader reader) { - string[] headerColumns = []; - if (reader.ReadLine() is {} lineFull) { - headerColumns = lineFull.Split(Config.ColumnSplit); - } - - while (true) { - if (reader.ReadLine() is not {} line) break; - - string[] values = line.Split(Config.ColumnSplit); - - var dict = new Dictionary(); - for (int i = 0; i < headerColumns.Length; i++) { - string value = values[i]; - dict[headerColumns[i]] = value.IsNotNullOrEmpty() ? value : null; - } - - yield return dict; - } - } - - private async IAsyncEnumerable> FromTextReaderAsync(TextReader reader) { - string[] headerColumns = []; - if (await reader.ReadLineAsync() is {} lineFull) { - headerColumns = lineFull.Split(Config.ColumnSplit); - } - - while (true) { - if (await reader.ReadLineAsync() is not {} line) break; - - string[] values = line.Split(Config.ColumnSplit); - - var dict = new Dictionary(); - for (int i = 0; i < headerColumns.Length; i++) { - string value = values[i]; - dict[headerColumns[i]] = value.IsNotNullOrEmpty() ? value : null; - } - - yield return dict; - } - } - #endregion -} diff --git a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvDictionaryWriter.cs b/src/CodeOfChaos.Parsers.Csv/Parsers/CsvDictionaryWriter.cs deleted file mode 100644 index 0e759f5..0000000 --- a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvDictionaryWriter.cs +++ /dev/null @@ -1,69 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Contracts; - -namespace CodeOfChaos.Parsers.Csv.Parsers; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class CsvDictionaryWriter(Action configAction) : CsvParser(configAction), ICsvWriter> { - public string WriteToString(IEnumerable> data) { - using var writer = new StringWriter(); - DictionaryToCsv(writer, data); - return writer.ToString(); - } - - public async Task WriteToStringAsync(IEnumerable> data) { - await using var writer = new StringWriter(); - await DictionaryToCsvAsync(writer, data); - return writer.ToString(); - } - - public void WriteToFile(string filePath, IEnumerable> data) { - using var writer = new StreamWriter(filePath); - DictionaryToCsv(writer, data); - } - - public async Task WriteToFileAsync(string filePath, IEnumerable> data) { - await using var writer = new StreamWriter(filePath); - await DictionaryToCsvAsync(writer, data); - } - - #region Helper Methods - private void DictionaryToCsv(TextWriter writer, IEnumerable> data) { - IDictionary[] records = data as IDictionary[] ?? data.ToArray>(); - if (records.Length == 0) return; - - // Write header row - if (Config.IncludeHeader) { - IDictionary firstDictionary = records.First(); - IEnumerable headers = firstDictionary.Keys; - writer.WriteLine(string.Join(Config.ColumnSplit, headers)); - } - - // Write data rows - foreach (IDictionary dictionary in records) { - IEnumerable values = dictionary.Values.Select(value => value?.ToString() ?? string.Empty); - writer.WriteLine(string.Join(Config.ColumnSplit, values)); - } - } - private async Task DictionaryToCsvAsync(TextWriter writer, IEnumerable> data) { - IDictionary[] records = data as IDictionary[] ?? data.ToArray>(); - if (records.Length == 0) return; - - // Write header row - if (Config.IncludeHeader) { - IDictionary firstDictionary = records.First(); - IEnumerable headers = firstDictionary.Keys; - await writer.WriteLineAsync(string.Join(Config.ColumnSplit, headers)); - } - - // Write data rows - foreach (IDictionary dictionary in records) { - IEnumerable values = dictionary.Values.Select(value => value?.ToString() ?? string.Empty); - await writer.WriteLineAsync(string.Join(Config.ColumnSplit, values)); - } - } - #endregion -} diff --git a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvReader.cs b/src/CodeOfChaos.Parsers.Csv/Parsers/CsvReader.cs deleted file mode 100644 index cb9eae0..0000000 --- a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvReader.cs +++ /dev/null @@ -1,91 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Attributes; -using CodeOfChaos.Parsers.Csv.Contracts; -using System.Reflection; - -namespace CodeOfChaos.Parsers.Csv.Parsers; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class CsvReader(Action configAction) : CsvParser(configAction), ICsvReader - where T : class, new() { - public IEnumerable FromCsvFile(string filePath) { - var reader = new StreamReader(filePath); - return FromTextReader(reader); - } - public IEnumerable FromCsvString(string data) { - var reader = new StringReader(data); - return FromTextReader(reader); - } - - public IAsyncEnumerable FromCsvFileAsync(string filePath) { - var reader = new StreamReader(filePath); - return FromTextReaderAsync(reader); - } - public IAsyncEnumerable FromCsvStringAsync(string data) { - var reader = new StringReader(data); - return FromTextReaderAsync(reader); - } - - #region Helper Methods - private IEnumerable FromTextReader(TextReader reader) { - string[] headerColumns = []; - if (reader.ReadLine() is {} lineFull) { - headerColumns = lineFull.Split(Config.ColumnSplit); - } - - while (true) { - if (reader.ReadLine() is not {} line) break; - - string[] values = line.Split(Config.ColumnSplit); - - var obj = new T(); - SetPropertyFromCsvColumn(obj, headerColumns, values); - yield return obj; - } - } - - private async IAsyncEnumerable FromTextReaderAsync(TextReader reader) { - string[] headerColumns = []; - if (await reader.ReadLineAsync() is {} lineFull) { - headerColumns = lineFull.Split(Config.ColumnSplit); - } - - while (true) { - if (await reader.ReadLineAsync() is not {} line) break; - - string[] values = line.Split(Config.ColumnSplit); - var obj = new T(); - - SetPropertyFromCsvColumn(obj, headerColumns, values); - - yield return obj; - } - } - - private void SetPropertyFromCsvColumn(T? value, string[] headerColumns, string[] values) { - if (value is null) return; - - foreach (PropertyInfo prop in value.GetType().GetProperties()) { - int columnIndex = Attribute.GetCustomAttribute(prop, typeof(CsvColumnAttribute)) is CsvColumnAttribute attribute - ? Array.IndexOf(headerColumns, attribute.Name) - : Array.IndexOf(headerColumns, prop.Name); - - if (columnIndex == -1) continue; - - try { - object propertyValue = Convert.ChangeType(values[columnIndex], prop.PropertyType); - prop.SetValue(value, propertyValue); - } - catch (Exception e) { - if (!Config.LogErrors) return; - - // Todo allow for logger - Console.WriteLine($"Error setting property {prop.Name} on {value.GetType().Name}: {e.Message}"); - } - } - } - #endregion -} diff --git a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvWriter.cs b/src/CodeOfChaos.Parsers.Csv/Parsers/CsvWriter.cs deleted file mode 100644 index 536d4a2..0000000 --- a/src/CodeOfChaos.Parsers.Csv/Parsers/CsvWriter.cs +++ /dev/null @@ -1,120 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Attributes; -using System.Reflection; - -namespace CodeOfChaos.Parsers.Csv.Parsers; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class CsvWriter(Action configAction) : CsvParser(configAction) - where T : class, new() { - public string WriteToString(IEnumerable data) { - using var writer = new StringWriter(); - ToCsv(writer, data); - return writer.ToString(); - } - - public async Task WriteToStringAsync(IEnumerable data) { - await using var writer = new StringWriter(); - await ToCsvAsync(writer, data); - return writer.ToString(); - } - - public void WriteToFile(string filePath, IEnumerable data) { - using var writer = new StreamWriter(filePath); - ToCsv(writer, data); - } - - public async Task WriteToFileAsync(string filePath, IEnumerable data) { - await using var writer = new StreamWriter(filePath); - await ToCsvAsync(writer, data); - } - - #region Helper Methods - private void ToCsv(TextWriter writer, IEnumerable data) { - // Write header row - IEnumerable enumerable = data as T[] ?? data.ToArray(); - PropertyInfo[] propertyInfos = GetCsvProperties(enumerable.FirstOrDefault());// Dirty but it will work - - if (Config.IncludeHeader) { - string[] headers = GetCsvHeaders(propertyInfos).ToArray(); - for (int i = 0; i < headers.Length; i++) { - writer.Write(headers[i]); - if (i < headers.Length - 1) writer.Write(Config.ColumnSplit); - } - - writer.Write(Environment.NewLine); - } - - // Write data rows - foreach (T? obj in enumerable) { - string[] values = GetCsvValues(obj, propertyInfos).ToArray(); - for (int i = 0; i < values.Length; i++) { - writer.Write(values[i]); - if (i < values.Length - 1) writer.Write(Config.ColumnSplit); - } - - writer.Write(Environment.NewLine); - } - } - - private async Task ToCsvAsync(TextWriter writer, IEnumerable data) { - // Write header row - IEnumerable enumerable = data as T[] ?? data.ToArray(); - PropertyInfo[] propertyInfos = GetCsvProperties(enumerable.FirstOrDefault()); - - if (Config.IncludeHeader) { - string[] headers = GetCsvHeaders(propertyInfos).ToArray(); - for (int i = 0; i < headers.Length; i++) { - await writer.WriteAsync(headers[i]); - if (i < headers.Length - 1) await writer.WriteAsync(Config.ColumnSplit); - } - - await writer.WriteAsync(Environment.NewLine); - } - - // Write data rows - foreach (T? obj in enumerable) { - string[] values = GetCsvValues(obj, propertyInfos).ToArray(); - for (int i = 0; i < values.Length; i++) { - await writer.WriteAsync(values[i]); - if (i < values.Length - 1) await writer.WriteAsync(Config.ColumnSplit); - } - - await writer.WriteAsync(Environment.NewLine); - } - } - - - private static PropertyInfo[] GetCsvProperties(T? obj) => obj? - .GetType() - .GetProperties() - .ToArray() ?? []; - - private IEnumerable GetCsvHeaders(PropertyInfo[] propertyInfos) { - return propertyInfos - .Select(p => { - if (p.GetCustomAttribute() is not {} attribute) - return Config.UseLowerCaseHeaders ? p.Name.ToLowerInvariant() : p.Name; - - return Config.UseLowerCaseHeaders - ? attribute.NameLowerInvariant - : attribute.Name; - }); - } - - private static IEnumerable GetCsvValues(T? obj, PropertyInfo[] propertyInfos) { - if (obj is null) return []; - - PropertyInfo[] properties = propertyInfos.Length != 0 - ? propertyInfos - : obj.GetType().GetProperties(); - - return properties - .Select(p => p.GetValue(obj)?.ToString() ?? string.Empty); - } - #endregion - -} diff --git a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvDictionaryReaderTest.cs b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvDictionaryReaderTest.cs deleted file mode 100644 index 9814cd7..0000000 --- a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvDictionaryReaderTest.cs +++ /dev/null @@ -1,125 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Parsers; - -namespace CodeOfChaos.Parsers.Csv.Tests.Parsers; - -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class CsvDictionaryReaderTest { - private static CsvDictionaryReader CreateReader() { - return new CsvDictionaryReader(config => config.ColumnSplit = ";"); - } - - [Fact] - public void FromCsvFile_ShouldReturnCorrectData() - { - // Arrange - var reader = CreateReader(); - var filePath = "FromCsvFile_ShouldReturnCorrectData.csv"; - File.WriteAllText(filePath, """ - id;name - 1;John - 2;Jane - """); - - // Act - var result = reader.FromCsvFile(filePath); - - // Assert - var expected = new List> - { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - Assert.Equal(expected, result); - } - - [Fact] - public void FromCsvString_ShouldReturnCorrectData() - { - // Arrange - var reader = CreateReader(); - var data = """ - id;name - 1;John - 2;Jane - """; - - // Act - var result = reader.FromCsvString(data); - - // Assert - var expected = new List> - { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - Assert.Equal(expected, result); - } - - [Fact] - public async Task FromCsvFileAsync_ShouldReturnCorrectData() - { - // Arrange - var reader = CreateReader(); - var filePath = "FromCsvFileAsync_ShouldReturnCorrectData.csv"; - await File.WriteAllTextAsync(filePath, """ - id;name - 1;John - 2;Jane - """); - - // Act - var result = reader.FromCsvFileAsync(filePath); - - // Assert - var expected = new List> - { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - var actual = new List>(); - await foreach (var dict in result) - { - actual.Add(dict); - } - - Assert.Equal(expected, actual); - } - - [Fact] - public async Task FromCsvStringAsync_ShouldReturnCorrectData() - { - // Arrange - var reader = CreateReader(); - var data = """ - id;name - 1;John - 2;Jane - """; - - // Act - var result = reader.FromCsvStringAsync(data); - - // Assert - var expected = new List> - { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - var actual = new List>(); - await foreach (var dict in result) - { - actual.Add(dict); - } - - Assert.Equal(expected, actual); - } -} diff --git a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvDictionaryWriterTest.cs b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvDictionaryWriterTest.cs deleted file mode 100644 index 74e78a8..0000000 --- a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvDictionaryWriterTest.cs +++ /dev/null @@ -1,121 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Parsers; - -namespace CodeOfChaos.Parsers.Csv.Tests.Parsers; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class CsvDictionaryWriterTest { - private CsvDictionaryWriter CreateWriter() { - return new CsvDictionaryWriter(config => { - config.ColumnSplit = ";"; - config.IncludeHeader = true; - }); - } - - [Fact] - public void WriteToString_ShouldReturnCorrectCsv() { - // Arrange - CsvDictionaryWriter writer = CreateWriter(); - var data = new List> { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - // Act - string result = writer.WriteToString(data); - - // Assert - string expected = """ - id;name - 1;John - 2;Jane - """; - Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); - } - - [Fact] - public async Task WriteToStringAsync_ShouldReturnCorrectCsv() { - // Arrange - CsvDictionaryWriter writer = CreateWriter(); - var data = new List> { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - // Act - string result = await writer.WriteToStringAsync(data); - - // Assert - string expected = """ - id;name - 1;John - 2;Jane - """; - Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); - } - - [Fact] - public void WriteToFile_ShouldWriteCorrectCsvToFile() { - // Arrange - CsvDictionaryWriter writer = CreateWriter(); - string filePath = "WriteToFile_ShouldWriteCorrectCsvToFile.csv"; - var data = new List> { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - try { - // Act - writer.WriteToFile(filePath, data); - - // Assert - string result = File.ReadAllText(filePath); - string expected = """ - id;name - 1;John - 2;Jane - """; - Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); - } - finally { - // Clean up - if (File.Exists(filePath)) { - File.Delete(filePath); - } - } - } - - [Fact] - public async Task WriteToFileAsync_ShouldWriteCorrectCsvToFileAsync() { - // Arrange - CsvDictionaryWriter writer = CreateWriter(); - string filePath = "WriteToFileAsync_ShouldWriteCorrectCsvToFileAsync.csv"; - var data = new List> { - new() { { "id", "1" }, { "name", "John" } }, - new() { { "id", "2" }, { "name", "Jane" } } - }; - - try { - // Act - await writer.WriteToFileAsync(filePath, data); - - // Assert - string result = await File.ReadAllTextAsync(filePath); - string expected = """ - id;name - 1;John - 2;Jane - """; - Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); - } - finally { - // Clean up - if (File.Exists(filePath)) { - File.Delete(filePath); - } - } - } -} diff --git a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvReaderTests.cs b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvReaderTests.cs index db973d6..0cf63fc 100644 --- a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvReaderTests.cs +++ b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvReaderTests.cs @@ -1,9 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Parsers; - -namespace CodeOfChaos.Parsers.Csv.Tests; +namespace CodeOfChaos.Parsers.Csv.Tests.Parsers; // --------------------------------------------------------------------------------------------------------------------- // Code @@ -12,7 +10,7 @@ public class CsvReaderTests { [Fact] public void ReadFromCsv_ShouldReadCsvCorrectly() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); @@ -21,9 +19,10 @@ public void ReadFromCsv_ShouldReadCsvCorrectly() { John;30 Jane;25 """; + var stringReader = new StringReader(csv); // Act - List result = reader.FromCsvString(csv).ToList(); + List result = parser.ToList(stringReader); // Assert Assert.Equal(2, result.Count); @@ -36,7 +35,7 @@ public void ReadFromCsv_ShouldReadCsvCorrectly() { [Fact] public async Task ReadFromCsvAsync_ShouldReadCsvCorrectly() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); @@ -45,10 +44,11 @@ public async Task ReadFromCsvAsync_ShouldReadCsvCorrectly() { John;30 Jane;25 """; + var stringReader = new StringReader(csv); // Act List result = []; - await foreach (TestModelWithoutAttribute data in reader.FromCsvStringAsync(csv)) { + await foreach (TestModelWithoutAttribute data in parser.ToEnumerableAsync(stringReader)) { result.Add(data); } @@ -63,7 +63,7 @@ public async Task ReadFromCsvAsync_ShouldReadCsvCorrectly() { [Fact] public void ReadFromCsv_ShouldHandleMissingColumnsGracefully() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); @@ -72,9 +72,10 @@ public void ReadFromCsv_ShouldHandleMissingColumnsGracefully() { John;30 Jane; """; + var stringReader = new StringReader(csv); // Act - List result = reader.FromCsvString(csv).ToList(); + List result = parser.ToList(stringReader); // Assert Assert.Equal(2, result.Count); @@ -87,7 +88,7 @@ public void ReadFromCsv_ShouldHandleMissingColumnsGracefully() { [Fact] public void ReadFromCsv_ShouldRespectConfiguration() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ","; cfg.IncludeHeader = true; }); @@ -96,9 +97,10 @@ public void ReadFromCsv_ShouldRespectConfiguration() { John,30 Jane,25 """; + var stringReader = new StringReader(csv); // Act - List result = reader.FromCsvString(csv).ToList(); + List result = parser.ToList(stringReader); // Assert Assert.Equal(2, result.Count); @@ -111,7 +113,7 @@ public void ReadFromCsv_ShouldRespectConfiguration() { [Fact] public void ReadFromCsv_ShouldConvertToPropertyTypes() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); @@ -120,9 +122,10 @@ public void ReadFromCsv_ShouldConvertToPropertyTypes() { John;30 Jane;25 """; + var stringReader = new StringReader(csv); // Act - List result = reader.FromCsvString(csv).ToList(); + List result = parser.ToList(stringReader); // Assert Assert.Equal(2, result.Count); @@ -133,14 +136,14 @@ public void ReadFromCsv_ShouldConvertToPropertyTypes() { [Fact] public void ReadFromCsv_ShouldReadFileCorrectly() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); var path = "Data/TestData.csv"; // Act - List result = reader.FromCsvFile(path).ToList(); + List result = parser.ToList(path); // Assert Assert.Equal(2, result.Count); @@ -153,7 +156,7 @@ public void ReadFromCsv_ShouldReadFileCorrectly() { [Fact] public async Task ReadFromCsv_ShouldReadFileCorrectlyAsync() { // Arrange - var reader = new CsvReader(cfg => { + var parser = CsvParser.FromConfig(cfg => { cfg.ColumnSplit = ";"; cfg.IncludeHeader = true; }); @@ -161,7 +164,7 @@ public async Task ReadFromCsv_ShouldReadFileCorrectlyAsync() { // Act List result = []; - await foreach (TestModelWithoutAttribute data in reader.FromCsvFileAsync(path)) { + await foreach (TestModelWithoutAttribute data in parser.ToEnumerableAsync(path)) { result.Add(data); } diff --git a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvWriterTests.cs b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvWriterTests.cs index 5f628e1..3c28923 100644 --- a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvWriterTests.cs +++ b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/CsvWriterTests.cs @@ -1,9 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Parsers.Csv.Parsers; - -namespace CodeOfChaos.Parsers.Csv.Tests; +namespace CodeOfChaos.Parsers.Csv.Tests.Parsers; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- @@ -17,7 +15,7 @@ public void CsvWriter_WriteToCsv_ShouldGenerateExpectedOutput_NamelessObject() { new { Name = "Jane", Age = 25 } }; - var csvWriter = new CsvWriter(config => { + CsvParser csvWriter = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -29,7 +27,7 @@ public void CsvWriter_WriteToCsv_ShouldGenerateExpectedOutput_NamelessObject() { """; // Act - string csvContent = csvWriter.WriteToString(data); + string csvContent = csvWriter.ParseToString(data); // Assert Assert.Equal(expectedOutput, csvContent.Trim()); @@ -43,7 +41,7 @@ public async Task CsvWriter_WriteToCsvAsync_ShouldGenerateExpectedOutput_Nameles new { Name = "Jane", Age = 25 } }; - var csvWriter = new CsvWriter(config => { + CsvParser csvWriter = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -57,7 +55,7 @@ public async Task CsvWriter_WriteToCsvAsync_ShouldGenerateExpectedOutput_Nameles await using var stringWriter = new StringWriter(); // Act - string csvContent = await csvWriter.WriteToStringAsync(data); + string csvContent = await csvWriter.ParseToStringAsync(data); // Assert Assert.Equal(expectedOutput, csvContent.Trim()); @@ -73,7 +71,7 @@ public void CsvWriter_WriteToCsv_ShouldGenerateExpectedOutput_ClassWithoutAttrib new() { Name = "Jane", Age = 25 } ]; - var csvWriter = new CsvWriter(config => { + CsvParser csvWriter = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -85,7 +83,7 @@ public void CsvWriter_WriteToCsv_ShouldGenerateExpectedOutput_ClassWithoutAttrib """; // Act - string csvContent = csvWriter.WriteToString(data); + string csvContent = csvWriter.ParseToString(data); // Assert Assert.Equal(expectedOutput, csvContent.Trim()); @@ -99,7 +97,7 @@ public async Task CsvWriter_WriteToCsvAsync_ShouldGenerateExpectedOutput_ClassWi new() { Name = "Jane", Age = 25 } ]; - var csvWriter = new CsvWriter(config => { + var csvWriter = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -111,7 +109,7 @@ public async Task CsvWriter_WriteToCsvAsync_ShouldGenerateExpectedOutput_ClassWi """; // Act - string csvContent = await csvWriter.WriteToStringAsync(data); + string csvContent = await csvWriter.ParseToStringAsync(data); // Assert Assert.Equal(expectedOutput, csvContent.Trim()); @@ -127,7 +125,7 @@ public void CsvWriter_WriteToCsv_ShouldGenerateExpectedOutput_Class() { new() { UserName = "Jane", UserAge = 25 } ]; - var csvWriter = new CsvWriter(config => { + CsvParser csvWriter = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -139,7 +137,7 @@ public void CsvWriter_WriteToCsv_ShouldGenerateExpectedOutput_Class() { """; // Act - string csvContent = csvWriter.WriteToString(data); + string csvContent = csvWriter.ParseToString(data); // Assert Assert.Equal(expectedOutput, csvContent.Trim()); @@ -153,7 +151,7 @@ public async Task CsvWriter_WriteToCsvAsync_ShouldGenerateExpectedOutput_Class() new() { UserName = "Jane", UserAge = 25 } ]; - var csvWriter = new CsvWriter(config => { + var csvWriter = CsvParser.FromConfig(config => { config.ColumnSplit = ";"; config.UseLowerCaseHeaders = true; }); @@ -165,7 +163,7 @@ public async Task CsvWriter_WriteToCsvAsync_ShouldGenerateExpectedOutput_Class() """; // Act - string csvContent = await csvWriter.WriteToStringAsync(data); + string csvContent = await csvWriter.ParseToStringAsync(data); // Assert Assert.Equal(expectedOutput, csvContent.Trim()); diff --git a/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/DictionaryCsvParserTest.cs b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/DictionaryCsvParserTest.cs new file mode 100644 index 0000000..8ac229b --- /dev/null +++ b/tests/CodeOfChaos.Parsers.Csv.Tests/Parsers/DictionaryCsvParserTest.cs @@ -0,0 +1,218 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace CodeOfChaos.Parsers.Csv.Tests.Parsers; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class DictionaryCsvParserTest { + private static CsvParser CreateParser() { + return CsvParser.FromConfig(config => config.ColumnSplit = ";"); + } + + [Fact] + public void FromCsvFile_ShouldReturnCorrectData() { + // Arrange + CsvParser parser = CreateParser(); + const string filePath = "FromCsvFile_ShouldReturnCorrectData.csv"; + File.WriteAllText(filePath, """ + id;name + 1;John + 2;Jane + """); + + // Act + List> result = parser.ToDictionaryList(filePath); + + // Assert + var expected = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + Assert.Equal(expected, result); + } + + [Fact] + public void FromCsvString_ShouldReturnCorrectData() { + // Arrange + CsvParser parser = CreateParser(); + string data = """ + id;name + 1;John + 2;Jane + """; + var stringReader = new StringReader(data); + + // Act + List> result = parser.ToDictionaryList(stringReader); + + // Assert + var expected = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + Assert.Equal(expected, result); + } + + [Fact] + public async Task FromCsvFileAsync_ShouldReturnCorrectData() { + // Arrange + CsvParser parser = CreateParser(); + string filePath = "FromCsvFileAsync_ShouldReturnCorrectData.csv"; + await File.WriteAllTextAsync(filePath, """ + id;name + 1;John + 2;Jane + """); + + // Act + IAsyncEnumerable> result = parser.ToDictionaryEnumerableAsync(filePath); + + // Assert + var expected = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + var actual = new List>(); + await foreach (Dictionary dict in result) { + actual.Add(dict); + } + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task FromCsvStringAsync_ShouldReturnCorrectData() { + // Arrange + CsvParser parser = CreateParser(); + string data = """ + id;name + 1;John + 2;Jane + """; + var reader = new StringReader(data); + + // Act + IAsyncEnumerable> result = parser.ToDictionaryEnumerableAsync(reader); + + // Assert + var expected = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + var actual = new List>(); + await foreach (Dictionary dict in result) { + actual.Add(dict); + } + + Assert.Equal(expected, actual); + } + + [Fact] + public void WriteToString_ShouldReturnCorrectCsv() { + // Arrange + CsvParser parser = CreateParser(); + var data = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + // Act + string result = parser.ParseToString(data); + + // Assert + string expected = """ + id;name + 1;John + 2;Jane + """; + Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task WriteToStringAsync_ShouldReturnCorrectCsv() { + // Arrange + CsvParser parser = CreateParser(); + var data = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + // Act + string result = await parser.ParseToStringAsync(data); + + // Assert + string expected = """ + id;name + 1;John + 2;Jane + """; + Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); + } + + [Fact] + public void WriteToFile_ShouldWriteCorrectCsvToFile() { + // Arrange + CsvParser parser = CreateParser(); + string filePath = "WriteToFile_ShouldWriteCorrectCsvToFile.csv"; + var data = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + try { + // Act + parser.ParseToFile(filePath, data); + + // Assert + string result = File.ReadAllText(filePath); + string expected = """ + id;name + 1;John + 2;Jane + """; + Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); + } + finally { + // Clean up + if (File.Exists(filePath)) { + File.Delete(filePath); + } + } + } + + [Fact] + public async Task WriteToFileAsync_ShouldWriteCorrectCsvToFileAsync() { + // Arrange + CsvParser parser = CreateParser(); + string filePath = "WriteToFileAsync_ShouldWriteCorrectCsvToFileAsync.csv"; + var data = new List> { + new() { { "id", "1" }, { "name", "John" } }, + new() { { "id", "2" }, { "name", "Jane" } } + }; + + try { + // Act + await parser.ParseToFileAsync(filePath, data); + + // Assert + string result = await File.ReadAllTextAsync(filePath); + string expected = """ + id;name + 1;John + 2;Jane + """; + Assert.Equal(expected, result.Trim(), ignoreLineEndingDifferences: true); + } + finally { + // Clean up + if (File.Exists(filePath)) { + File.Delete(filePath); + } + } + } +}