using BytecodeApi.Extensions; using Microsoft.VisualBasic.FileIO; using System.Reflection; using System.Text; namespace BytecodeApi.CsvParser; /// /// Represents a database read from a CSV files with lines separated by a delimiter. /// public class CsvFile { /// /// Gets or sets the delimiter detector to be used when reading a CSV file and the delimiter parameter is . /// This property is global for the class. /// Default values are: /// = ",", ";", "\t", "|" /// = 2 /// = 10 /// public static CsvDelimiterDetector DelimiterDetector { get; set; } /// /// Gets or sets a [] that contains the headers of this CSV file or , if the hasHeaderRow parameter was set to . /// public string[]? Headers { get; set; } /// /// Gets or sets the delimiter for this CSV file. The initial value is set to the specified or detected delimiter when loading the CSV file, or , if the constructor was used to create an empty . This property is used by the method. /// public string? Delimiter { get; set; } /// /// Gets a collection of objects that represents the content of this CSV file. /// public CsvRowCollection Rows { get; } /// /// Gets a value indicating whether this CSV file contains rows that could not be parsed. This property does not change once the file is loaded. /// public bool HasErrors { get; private set; } /// /// Gets a value indicating whether all rows in this CSV file have the same amount of columns, excluding the property and error lines. If zero rows or only error rows were loaded, this property returns . This property does not change once the file is loaded. /// public bool IsColumnCountConsistent { get; private set; } static CsvFile() { DelimiterDetector = CsvDelimiterDetector.CreateDefault(); } /// /// Initializes a new instance of the class. /// public CsvFile() { Rows = []; IsColumnCountConsistent = true; } /// /// Creates a object from the specified file. /// /// A representing the path to a CSV file. /// /// The this method creates. /// public static CsvFile FromFile(string path) { return FromFile(path, false); } /// /// Creates a object from the specified file. /// /// A representing the path to a CSV file. /// to treat the first row as a header row and load its contents into the property. /// /// The this method creates. /// public static CsvFile FromFile(string path, bool hasHeaderRow) { return FromFile(path, hasHeaderRow, null); } /// /// Creates a object from the specified file. /// /// A representing the path to a CSV file. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// /// The this method creates. /// public static CsvFile FromFile(string path, bool hasHeaderRow, string? delimiter) { return FromFile(path, hasHeaderRow, delimiter, false); } /// /// Creates a object from the specified file. /// /// A representing the path to a CSV file. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// /// The this method creates. /// public static CsvFile FromFile(string path, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines) { return FromFile(path, hasHeaderRow, delimiter, ignoreEmptyLines, null); } /// /// Creates a object from the specified file. /// /// A representing the path to a CSV file. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// The encoding to use if encoding is not determined from the file. Specify to detect encoding automatically or provide a value to explicitly parse with a specific . /// /// The this method creates. /// public static CsvFile FromFile(string path, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines, Encoding? encoding) { Check.ArgumentNull(path); Check.FileNotFound(path); Check.ArgumentEx.StringNotEmpty(delimiter); using FileStream file = File.OpenRead(path); return FromStream(file, hasHeaderRow, delimiter, ignoreEmptyLines, encoding); } /// /// Creates a object from the specified [] that represents a CSV file. /// /// The [] that represents a CSV file to read from. /// /// The this method creates. /// public static CsvFile FromBinary(byte[] file) { return FromBinary(file, false); } /// /// Creates a object from the specified [] that represents a CSV file. /// /// The [] that represents a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// /// The this method creates. /// public static CsvFile FromBinary(byte[] file, bool hasHeaderRow) { return FromBinary(file, hasHeaderRow, null); } /// /// Creates a object from the specified [] that represents a CSV file. /// /// The [] that represents a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// /// The this method creates. /// public static CsvFile FromBinary(byte[] file, bool hasHeaderRow, string? delimiter) { return FromBinary(file, hasHeaderRow, delimiter, false); } /// /// Creates a object from the specified [] that represents a CSV file. /// /// The [] that represents a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// /// The this method creates. /// public static CsvFile FromBinary(byte[] file, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines) { return FromBinary(file, hasHeaderRow, delimiter, ignoreEmptyLines, null); } /// /// Creates a object from the specified [] that represents a CSV file. /// /// The [] that represents a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// The encoding to use if encoding is not determined from the file. Specify to detect encoding automatically or provide a value to explicitly parse with a specific . /// /// The this method creates. /// public static CsvFile FromBinary(byte[] file, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines, Encoding? encoding) { Check.ArgumentNull(file); Check.ArgumentEx.StringNotEmpty(delimiter); using MemoryStream memoryStream = new(file); return FromStream(memoryStream, hasHeaderRow, delimiter, ignoreEmptyLines, encoding); } /// /// Creates a object from the specified that represents the contents of a CSV file. /// /// The that represents the contents of a CSV file to read from. /// /// The this method creates. /// public static CsvFile FromString(string csv) { return FromString(csv, false); } /// /// Creates a object from the specified that represents the contents of a CSV file. /// /// The that represents the contents of a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// /// The this method creates. /// public static CsvFile FromString(string csv, bool hasHeaderRow) { return FromString(csv, hasHeaderRow, null); } /// /// Creates a object from the specified that represents the contents of a CSV file. /// /// The that represents the contents of a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// /// The this method creates. /// public static CsvFile FromString(string csv, bool hasHeaderRow, string? delimiter) { return FromString(csv, hasHeaderRow, delimiter, false); } /// /// Creates a object from the specified that represents the contents of a CSV file. /// /// The that represents the contents of a CSV file to read from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// /// The this method creates. /// public static CsvFile FromString(string csv, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines) { return FromBinary(csv.ToUTF8Bytes(), hasHeaderRow, delimiter, ignoreEmptyLines, Encoding.UTF8); } /// /// Creates a object from the specified . /// /// The from which to read the CSV file from. /// /// The this method creates. /// public static CsvFile FromStream(Stream stream) { return FromStream(stream, false); } /// /// Creates a object from the specified . /// /// The from which to read the CSV file from. /// to treat the first row as a header row and load its contents into the property. /// /// The this method creates. /// public static CsvFile FromStream(Stream stream, bool hasHeaderRow) { return FromStream(stream, hasHeaderRow, null); } /// /// Creates a object from the specified . /// /// The from which to read the CSV file from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// /// The this method creates. /// public static CsvFile FromStream(Stream stream, bool hasHeaderRow, string? delimiter) { return FromStream(stream, hasHeaderRow, delimiter, false); } /// /// Creates a object from the specified . /// /// The from which to read the CSV file from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// /// The this method creates. /// public static CsvFile FromStream(Stream stream, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines) { return FromStream(stream, hasHeaderRow, delimiter, ignoreEmptyLines, null); } /// /// Creates a object from the specified . /// /// The from which to read the CSV file from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// The encoding to use if encoding is not determined from the file. Specify to detect encoding automatically or provide a value to explicitly parse with a specific . /// /// The this method creates. /// public static CsvFile FromStream(Stream stream, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines, Encoding? encoding) { return FromStream(stream, hasHeaderRow, delimiter, ignoreEmptyLines, encoding, false); } /// /// Creates a object from the specified . /// /// The from which to read the CSV file from. /// to treat the first row as a header row and load its contents into the property. /// A specifying the delimiter to be used during CSV parsing. If is provided, the delimiter is automatically detected. If automatic detection fails, an exception is thrown. /// to ignore empty lines and lines where all columns are empty. /// The encoding to use if encoding is not determined from the file. Specify to detect encoding automatically or provide a value to explicitly parse with a specific . /// A value indicating whether to leave open. /// /// The this method creates. /// public static CsvFile FromStream(Stream stream, bool hasHeaderRow, string? delimiter, bool ignoreEmptyLines, Encoding? encoding, bool leaveOpen) { Check.ArgumentNull(stream); Check.ArgumentEx.StringNotEmpty(delimiter); CsvHelper.AutoDetectDelimiter(DelimiterDetector, stream, encoding, ref delimiter); using TextFieldParser parser = CsvHelper.CreateTextFieldParser(stream, delimiter, encoding, leaveOpen, out FieldInfo lineNumberField); CsvFile csv = new() { Delimiter = delimiter }; if (hasHeaderRow) { try { csv.Headers = parser.ReadFields(); } catch (MalformedLineException) { csv.HasErrors = true; } } int columnCount = -1; foreach (CsvRow row in CsvHelper.EnumerateTextFieldParser(parser, ignoreEmptyLines)) { row.LineNumber = lineNumberField.GetValue(parser) is long lineNumber ? lineNumber - 1 : throw Throw.InvalidOperation("Error retrieving line number."); if (row.ErrorLine != null) { csv.HasErrors = true; } if (row.ErrorLine == null && csv.IsColumnCountConsistent) { if (columnCount == -1) { columnCount = row.Count; } else if (columnCount != row.Count) { csv.IsColumnCountConsistent = false; } } csv.Rows.Add(row); } return csv; } /// /// Checks whether the column count of all rows is equal to , excluding the property and error rows. /// /// A value specifying the expected column count for all rows. /// /// , if the column count of all rows is equal to , or it no rows were imported; /// otherwise, . /// public bool CheckColumnCount(int columnCount) { if (IsColumnCountConsistent) { return Rows.Count == 0 || Rows.First().Count == columnCount; } else { return Rows.All(row => row.Count == columnCount); } } /// /// Tries to find the column index of a specified case sensitive column header name. Returns -1, if the column header was not found. /// /// A specifying the case sensitive name of the column header to search for. /// /// The zero-based index of the column header, or -1, if the column header was not found. /// public int GetColumnIndex(string header) { return GetColumnIndex(header, false); } /// /// Tries to find the column index of a specified case header name. Returns -1, if the column header was not found. /// /// A specifying the case sensitive name of the column header to search for. /// to ignore character casing during column name comparison. /// /// The zero-based index of the column header, or -1, if the column header was not found. /// public int GetColumnIndex(string header, bool ignoreCase) { return Headers?.IndexOf(h => h.Equals(header, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) ?? -1; } /// /// Writes the contents of this CSV to a file. If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// A specifying the path to a file to which this CSV file is written to. public void Save(string path) { Save(path, false); } /// /// Writes the contents of this CSV to a file. If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// A specifying the path to a file to which this CSV file is written to. /// to wrap all cells with quotes; to only use quotes when needed. public void Save(string path, bool alwaysQuote) { Save(path, alwaysQuote, null); } /// /// Writes the contents of this CSV to a file. If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// A specifying the path to a file to which this CSV file is written to. /// to wrap all cells with quotes; to only use quotes when needed. /// The encoding to use to write to the file. public void Save(string path, bool alwaysQuote, Encoding? encoding) { Check.ArgumentNull(path); Check.ArgumentNull(Delimiter); Check.ArgumentEx.StringNotEmpty(Delimiter); using FileStream stream = File.Create(path); Save(stream, alwaysQuote, encoding, false); } /// /// Writes the contents of this CSV to a . If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// The to which this CSV is written to. public void Save(Stream stream) { Save(stream, false); } /// /// Writes the contents of this CSV to a . If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// The to which this CSV is written to. /// to wrap all cells with quotes; to only use quotes when needed. public void Save(Stream stream, bool alwaysQuote) { Save(stream, alwaysQuote, null); } /// /// Writes the contents of this CSV to a . If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// The to which this CSV is written to. /// to wrap all cells with quotes; to only use quotes when needed. /// The encoding to use to write to the file. public void Save(Stream stream, bool alwaysQuote, Encoding? encoding) { Save(stream, alwaysQuote, encoding, false); } /// /// Writes the contents of this CSV to a . If is not , the header row is included. The property specifies the delimiter to use when writing. /// /// The to which this CSV is written to. /// to wrap all cells with quotes; to only use quotes when needed. /// The encoding to use to write to the file. /// A value indicating whether to leave open. public void Save(Stream stream, bool alwaysQuote, Encoding? encoding, bool leaveOpen) { Check.ArgumentNull(stream); Check.ArgumentNull(Delimiter); Check.ArgumentEx.StringNotEmpty(Delimiter); if (Headers?.Any() == true) { CsvIterator.ToStream(stream, [new CsvRow(Headers)], Delimiter, alwaysQuote, encoding, true); } CsvIterator.ToStream(stream, Rows, Delimiter, alwaysQuote, encoding, leaveOpen); } /// /// Converts this to its [] representation. /// /// /// A new [] representing this . /// public byte[] Save() { return Save(false); } /// /// Converts this to its [] representation. /// /// to wrap all cells with quotes; to only use quotes when needed. /// /// A new [] representing this . /// public byte[] Save(bool alwaysQuote) { return Save(alwaysQuote, null); } /// /// Converts this to its [] representation. /// /// to wrap all cells with quotes; to only use quotes when needed. /// The encoding to use to write to the file. /// /// A new [] representing this . /// public byte[] Save(bool alwaysQuote, Encoding? encoding) { using MemoryStream memoryStream = new(); Save(memoryStream, alwaysQuote, encoding, false); return memoryStream.ToArray(); } }