diff --git a/Curiosity.Utils.sln b/Curiosity.Utils.sln index 242087b..238ff09 100644 --- a/Curiosity.Utils.sln +++ b/Curiosity.Utils.sln @@ -151,6 +151,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Curiosity.SMS.Iqsms", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Curiosity.SMS.Iqsms.Sample", "samples\Curiosity.SMS.Iqsms.Sample\Curiosity.SMS.Iqsms.Sample.csproj", "{4015E00D-EC76-4A73-A52F-4C4D71C6B8B5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Curiosity.FileDataReaderWriters", "src\FileData\Curiosity.FileDataReaderWriters\Curiosity.FileDataReaderWriters.csproj", "{D135ADFA-0A52-41C0-876F-691876A780E1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileData", "FileData", "{2B8CCF93-24F0-45F9-9961-85695351141E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileData", "FileData", "{973E81A0-574E-406A-AB95-01B1243A984C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Curiosity.FileDataReaderWriters.UnitTests", "tests\UnitTests\Curiosity.FileDataReaderWriters.UnitTests\Curiosity.FileDataReaderWriters.UnitTests.csproj", "{14444441-9159-44B1-AEE0-50B9C8F6E90C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Curiosity.FileDataReaderWriters.Npoi", "src\FileData\Curiosity.FileDataReaderWriters.Npoi\Curiosity.FileDataReaderWriters.Npoi.csproj", "{7BDDB1E2-79A6-4E0E-9EE4-C4B03E4D0864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Curiosity.FileDataReaderWriters.SylvanCsv", "src\FileData\Curiosity.FileDataReaderWriters.SylvanCsv\Curiosity.FileDataReaderWriters.SylvanCsv.csproj", "{BEF02973-7CBA-4AB3-9F91-D0F5BFB5E942}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -341,6 +353,22 @@ Global {4015E00D-EC76-4A73-A52F-4C4D71C6B8B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {4015E00D-EC76-4A73-A52F-4C4D71C6B8B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {4015E00D-EC76-4A73-A52F-4C4D71C6B8B5}.Release|Any CPU.Build.0 = Release|Any CPU + {D135ADFA-0A52-41C0-876F-691876A780E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D135ADFA-0A52-41C0-876F-691876A780E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D135ADFA-0A52-41C0-876F-691876A780E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D135ADFA-0A52-41C0-876F-691876A780E1}.Release|Any CPU.Build.0 = Release|Any CPU + {14444441-9159-44B1-AEE0-50B9C8F6E90C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14444441-9159-44B1-AEE0-50B9C8F6E90C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14444441-9159-44B1-AEE0-50B9C8F6E90C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14444441-9159-44B1-AEE0-50B9C8F6E90C}.Release|Any CPU.Build.0 = Release|Any CPU + {7BDDB1E2-79A6-4E0E-9EE4-C4B03E4D0864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BDDB1E2-79A6-4E0E-9EE4-C4B03E4D0864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BDDB1E2-79A6-4E0E-9EE4-C4B03E4D0864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BDDB1E2-79A6-4E0E-9EE4-C4B03E4D0864}.Release|Any CPU.Build.0 = Release|Any CPU + {BEF02973-7CBA-4AB3-9F91-D0F5BFB5E942}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEF02973-7CBA-4AB3-9F91-D0F5BFB5E942}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEF02973-7CBA-4AB3-9F91-D0F5BFB5E942}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEF02973-7CBA-4AB3-9F91-D0F5BFB5E942}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -414,6 +442,12 @@ Global {97ED36AE-E975-4CF8-BDDB-226DB0032531} = {7CFB0FEF-A83E-41F5-AAF7-61B84D18D243} {9CE5443D-D7D5-45DA-B32E-7776AAC4FFC5} = {EEB0E172-4687-434F-A91B-3E7D3C60C8F8} {4015E00D-EC76-4A73-A52F-4C4D71C6B8B5} = {A7A87B49-2E4C-4A0A-92F4-7C7849F36A84} + {2B8CCF93-24F0-45F9-9961-85695351141E} = {7C485CBA-A08F-4A0E-835A-4505CBBB7EA8} + {D135ADFA-0A52-41C0-876F-691876A780E1} = {2B8CCF93-24F0-45F9-9961-85695351141E} + {973E81A0-574E-406A-AB95-01B1243A984C} = {55CC9AE1-6978-4ECB-9A0A-0DCE1B66D492} + {14444441-9159-44B1-AEE0-50B9C8F6E90C} = {973E81A0-574E-406A-AB95-01B1243A984C} + {7BDDB1E2-79A6-4E0E-9EE4-C4B03E4D0864} = {2B8CCF93-24F0-45F9-9961-85695351141E} + {BEF02973-7CBA-4AB3-9F91-D0F5BFB5E942} = {2B8CCF93-24F0-45F9-9961-85695351141E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BA1A20A2-0D4E-492D-877C-0A36F18FBFF1} diff --git a/src/FileData/Curiosity.FileDataReaderWriters.Npoi/CHANGELOG.md b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/CHANGELOG.md new file mode 100644 index 0000000..46b34f7 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [1.0.0] - 2024-02-02 + +Package was released. diff --git a/src/FileData/Curiosity.FileDataReaderWriters.Npoi/Curiosity.FileDataReaderWriters.Npoi.csproj b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/Curiosity.FileDataReaderWriters.Npoi.csproj new file mode 100644 index 0000000..aae13a3 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/Curiosity.FileDataReaderWriters.Npoi.csproj @@ -0,0 +1,54 @@ + + + + netstandard2.1 + + Curiosity.FileDataReaderWriters.Npoi + Curiosity.FileDataReaderWriters.Npoi + Npoi realization for working with xls/xlsx/csv files + Single and multifile writing to xlsx file. Uses streaming version of workbook to reduce peak memory consumption. + Curiosity; FileDataReaderWriters; Npoi; siisltd + English + + 1.0.0 + 1.0.0 + 1.0.0 + + Max Markelow (@markeli), Andrei Vinogradov (@anri-vin), Andrey Ioch (@DevCorvette), Timur Sidoriuk (@shockthunder) + SIIS Ltd + SIIS Ltd, 2024 + + false + MIT + + https://github.com/siisltd/Curiosity.Utils + git + https://github.com/siisltd/Curiosity.Utils + https://github.com/siisltd/Curiosity.Utils/tree/master/src/FileData/Curiosity.FileDataReaderWriters.Npoi/CHANGELOG.md + + 10 + enable + true + + + + siisltd.png + + + + + + + + + + + + + + + + bin\Release\Curiosity.FileDataReaderWriters.Npoi.xml + + + diff --git a/src/FileData/Curiosity.FileDataReaderWriters.Npoi/NpoiXlsFileWriter.cs b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/NpoiXlsFileWriter.cs new file mode 100644 index 0000000..5eabfa1 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/NpoiXlsFileWriter.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Curiosity.FileDataReaderWriters.Style; +using Curiosity.FileDataReaderWriters.Writers; +using ICSharpCode.SharpZipLib.Zip; +using Microsoft.Extensions.Logging; +using NPOI.SS.UserModel; +using NPOI.XSSF.Streaming; + +namespace Curiosity.FileDataReaderWriters.Npoi; + +/// +/// Класс для записи данных в xlsx / xls формате для NPOI +/// +public class NpoiXlsFileWriter : IFileWriter +{ + private int _currentRow = 0; + private int _currentCell = 0; + private readonly string _outputFilePath; + private readonly ILogger _logger; + + private readonly SXSSFWorkbook _workbook; + private readonly ISheet _sheet; + private readonly ICellStyle _dataStyle; + private FileStream _fileStream; + + private Dictionary _cellStyles = new(); + + /// + /// Конструктор для записи эксель файла через NPOI + /// + /// Адрес итогового файла + /// Количество строк, находящихся в памяти писателя + public NpoiXlsFileWriter(string outputFilePath, ILogger logger, int rowAccessWindowSize = 100) + { + _outputFilePath = outputFilePath; + _logger = logger; + _workbook = new SXSSFWorkbook(rowAccessWindowSize); + _sheet = _workbook.CreateSheet(); + + //настройка формата даты для ячеек + var format = _workbook.CreateDataFormat(); + _dataStyle = _workbook.CreateCellStyle(); + _dataStyle.DataFormat = format.GetFormat("yyyy-MM-dd HH:mm:ss"); + } + + public int AddFormat(FormatSettings formatSettings) + { + var font = _workbook.CreateFont(); + font.IsBold = formatSettings.FontStyle.HasFlag(FontStyle.Bold); + font.IsItalic = formatSettings.FontStyle.HasFlag(FontStyle.Italic); + font.FontHeightInPoints = formatSettings.FontSize; + + var style = _workbook.CreateCellStyle(); + style.SetFont(font); + + style.Alignment = formatSettings.TextAlignment switch + { + TextAlignment.Left => HorizontalAlignment.Left, + TextAlignment.Center => HorizontalAlignment.Center, + TextAlignment.Right => HorizontalAlignment.Right, + TextAlignment.Justify => HorizontalAlignment.Justify, + _ => throw new ArgumentOutOfRangeException(nameof(formatSettings.TextAlignment)) + }; + + style.WrapText = formatSettings.WrapText; + + var format = _workbook.CreateDataFormat(); + style.DataFormat = format.GetFormat(formatSettings.DataFormat); + + var formatNumber = _cellStyles.Count; + _cellStyles.Add(formatNumber, style); + + return formatNumber; + } + + public int AddDefaultFormat() + { + var formatNumber = _cellStyles.Count; + _cellStyles.Add(formatNumber, _workbook.CreateCellStyle()); + + return formatNumber; + } + + public void AddHeaders(IReadOnlyList data) + { + if (data is null) + throw new ArgumentNullException(nameof(data)); + + for (var i = 0; i < data.Count; i++) + { + var datum = data[i]; + Append(datum.Value, datum.Format); + } + + EndLine(); + } + + public void AppendLine(IReadOnlyList data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + for (var i = 0; i < data.Count; i++) + { + var datum = data[i]; + Append(datum.Value, datum.Format); + } + + EndLine(); + } + + public void Append(object? value, int? format = null) + { + if (value is null || (value is string str && String.IsNullOrWhiteSpace(str))) + { + _currentCell++; + return; + } + + var row = _sheet.GetRow(_currentRow) ?? _sheet.CreateRow(_currentRow); + var cell = row.CreateCell(_currentCell); + + switch (value) + { + case byte val: + cell.SetCellValue(val); + break; + case short val: + cell.SetCellValue(val); + break; + case int val: + cell.SetCellValue(val); + break; + case long val: + cell.SetCellValue(val); + break; + case float val: + cell.SetCellValue(val); + break; + case double val: + cell.SetCellValue(val); + break; + case decimal val: + cell.SetCellValue(Convert.ToDouble(val)); + break; + case bool val: + cell.SetCellValue(val); + break; + case IRichTextString val: + cell.SetCellValue(val); + break; + case string val: + cell.SetCellValue(val); + break; + case DateTime val: + cell.SetCellValue(val); + cell.CellStyle = _dataStyle; + break; + default: + _logger.LogWarning("Используем обычный ToString() (Type={Type}, Value={Value})", value.GetType(), value); + cell.SetCellValue(value.ToString()); + break; + } + + if (format is not null) + cell.CellStyle = _cellStyles[(int)format]; + + _currentCell++; + } + + public void EndLine() + { + _currentRow++; + _currentCell = 0; + } + + public void Flush() + { + } + + /// + /// Записываем данные в файл и закрываем стрим. + /// + public void Dispose() + { + _fileStream = File.Open(_outputFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + _workbook.Write(_fileStream); + _fileStream.Close(); + } +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters.Npoi/NpoiXlsMultiFileWriter.cs b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/NpoiXlsMultiFileWriter.cs new file mode 100644 index 0000000..73a92e5 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.Npoi/NpoiXlsMultiFileWriter.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Curiosity.FileDataReaderWriters.Helpers; +using Curiosity.FileDataReaderWriters.Style; +using Curiosity.FileDataReaderWriters.Writers; +using Microsoft.Extensions.Logging; +using NPOI.SS.UserModel; +using NPOI.XSSF.Streaming; + +namespace Curiosity.FileDataReaderWriters.Npoi; + +/// +/// Класс для записи данных в xlsx / xls формате для NPOI с разбитием на несколько файлов +/// +public class NpoiXlsMultiFileWriter : IFileWriter +{ + private readonly ILogger _logger; + private IReadOnlyList? _headers; + private int _currentRow = 0; + private int _currentCell = 0; + private int _partNumber = 1; + + private readonly SXSSFWorkbook _workbook; + private readonly ICellStyle _dataStyle; + private ISheet _sheet; + private FileStream _fileStream; + + private Dictionary _cellStyles = new(); + + private readonly string _savePath; + private readonly string _fileName; + + /// + /// Конструктор для записи множества эксель файлов через NPOI + /// + /// + /// Название файла + /// Директория сохранения + /// Количество строк, находящихся в памяти писателя + public NpoiXlsMultiFileWriter(string savePath, string fileName, ILogger logger, int rowAccessWindowSize = 100) + { + if (String.IsNullOrWhiteSpace(savePath)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(savePath)); + if (String.IsNullOrWhiteSpace(fileName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName)); + + _savePath = savePath; + _logger = logger; + _fileName = fileName; + _workbook = new SXSSFWorkbook(rowAccessWindowSize); + _sheet = _workbook.CreateSheet(); + + //настройка формата даты для ячеек + var format = _workbook.CreateDataFormat(); + _dataStyle = _workbook.CreateCellStyle(); + _dataStyle.DataFormat = format.GetFormat("yyyy-MM-dd HH:mm:ss"); + } + + public int AddFormat(FormatSettings formatSettings) + { + var font = _workbook.CreateFont(); + font.IsBold = formatSettings.FontStyle.HasFlag(FontStyle.Bold); + font.IsItalic = formatSettings.FontStyle.HasFlag(FontStyle.Italic); + font.FontHeightInPoints = formatSettings.FontSize; + + var style = _workbook.CreateCellStyle(); + style.SetFont(font); + + style.Alignment = formatSettings.TextAlignment switch + { + TextAlignment.Left => HorizontalAlignment.Left, + TextAlignment.Center => HorizontalAlignment.Center, + TextAlignment.Right => HorizontalAlignment.Right, + TextAlignment.Justify => HorizontalAlignment.Justify, + _ => throw new ArgumentOutOfRangeException(nameof(formatSettings.TextAlignment)) + }; + + style.WrapText = formatSettings.WrapText; + + var format = _workbook.CreateDataFormat(); + style.DataFormat = format.GetFormat(formatSettings.DataFormat); + + var formatNumber = _cellStyles.Count; + _cellStyles.Add(formatNumber, style); + + return formatNumber; + } + + public int AddDefaultFormat() + { + var formatNumber = _cellStyles.Count; + _cellStyles.Add(formatNumber, _workbook.CreateCellStyle()); + + return formatNumber; + } + + public void AddHeaders(IReadOnlyList data) + { + _headers = data ?? throw new ArgumentNullException(nameof(data)); + + for (var i = 0; i < data.Count; i++) + { + var datum = data[i]; + Append(datum.Value, datum.Format); + } + + EndLine(); + } + + public void AppendLine(IReadOnlyList data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + for (var i = 0; i < data.Count; i++) + { + var datum = data[i]; + Append(datum.Value, datum.Format); + } + + EndLine(); + } + + public void Append(object? value, int? format = null) + { + if (value is null || (value is string str && String.IsNullOrWhiteSpace(str))) + { + _currentCell++; + return; + } + + var row = _sheet.GetRow(_currentRow) ?? _sheet.CreateRow(_currentRow); + var cell = row.CreateCell(_currentCell); + + switch (value) + { + case byte val: + cell.SetCellValue(val); + break; + case short val: + cell.SetCellValue(val); + break; + case int val: + cell.SetCellValue(val); + break; + case long val: + cell.SetCellValue(val); + break; + case float val: + cell.SetCellValue(val); + break; + case double val: + cell.SetCellValue(val); + break; + case decimal val: + cell.SetCellValue(Convert.ToDouble(val)); + break; + case bool val: + cell.SetCellValue(val); + break; + case IRichTextString val: + cell.SetCellValue(val); + break; + case string val: + cell.SetCellValue(val); + break; + case DateTime val: + cell.SetCellValue(val); + cell.CellStyle = _dataStyle; + break; + default: + _logger.LogWarning("Используем обычный ToString() (Type={Type}, Value={Value})", value.GetType(), value); + cell.SetCellValue(value.ToString()); + break; + } + + if (format is not null) + cell.CellStyle = _cellStyles[(int)format]; + + _currentCell++; + } + + public void EndLine() + { + _currentRow++; + _currentCell = 0; + + // если лимит - сбросим данные в файл + if (_currentRow >= ExcelConstants.XlsxRowsMax) + Flush(); + } + + public void Flush() + { + // если ничего нет + // или только хедеры и это 2ой файл - выходим + // (первый пустой файл с хедерами - сохраняем) + if (_currentRow == 0 || + (_headers != null && _currentRow == 1 && _partNumber > 1)) + return; + + // сгенерим имя + var fileName = _fileName; + if (_partNumber > 1) + { + fileName = + $"{Path.GetFileNameWithoutExtension(_fileName)}_part_{_partNumber}{Path.GetExtension(_fileName)}"; + } + + var outputFilePath = Path.Combine(_savePath, fileName); + + // сохраним + _logger.LogDebug($"Сохраняем файл \"{outputFilePath}\"..."); + _fileStream = File.Open(outputFilePath, FileMode.OpenOrCreate, FileAccess.Write); + _workbook.Write(_fileStream); + _fileStream.Close(); + _logger.LogInformation($"Файл \"{outputFilePath}\" успешно сохранён"); + + // обнулим состояние + _workbook.RemoveSheetAt(0); + _sheet = _workbook.CreateSheet(); + _partNumber++; + _currentRow = 0; + if (_headers != null) + AddHeaders(_headers); + } + + public void Dispose() + { + if (_fileStream is not null) + Flush(); + } +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CHANGELOG.md b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CHANGELOG.md new file mode 100644 index 0000000..46b34f7 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [1.0.0] - 2024-02-02 + +Package was released. diff --git a/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CsvFileDataReader.cs b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CsvFileDataReader.cs new file mode 100644 index 0000000..bb080e3 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CsvFileDataReader.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Curiosity.FileDataReaderWriters.Readers; +using Sylvan.Data.Csv; + +namespace Curiosity.FileDataReaderWriters.SylvanCsv; + +/// +/// Класс для построчного чтения CSV файлов. +/// +/// +public class CsvFileDataReader : IFileDataReader +{ + /// + /// Максимальное количество пустых строк подряд, после которых мы перестаем читать файл. + /// + private const int MaxSequentialEmptyRowsCount = 10; + + private readonly StreamReader _streamReader; + private readonly CsvDataReader _csvReader; + + private readonly Stream _fileStream; + private readonly bool _disposeFileStream; + + private int _rIdx; + private int _sequentialEmptyRowsCount; + private readonly int _startRowIdx; + + private IReadOnlyList? _currentRow; + private readonly List _rowStringBuffer; + private object[] _rowObjectBuffer; + + /// + public int MaxSupportedRowsCount => int.MaxValue; + + /// + /// + /// Уменьшаем на 1, потому что после чтения мы всегда увеличиваем после чтения очередной строки. + /// + public int CurrentRowIdx => _rIdx - 1; + + private CsvFileDataReader( + Stream fileStream, + bool disposeFileStream, + int? columnsCount, + Encoding encoding, + char delimiter, + int startRowIdx) + { + _fileStream = fileStream; + _disposeFileStream = disposeFileStream; + + _streamReader = new StreamReader(fileStream, encoding, false, 128 * 1024); + _csvReader = CsvDataReader.Create( + _streamReader, + new CsvDataReaderOptions + { + Delimiter = delimiter, + HasHeaders = false, // мы обычно сами парсим заголовки, поэтому нет смысла что-то там парсить + }); + + _rowStringBuffer = columnsCount.HasValue + ? new List(columnsCount.Value) + : new List(); + // если что, массив может быть пересоздан, если мы не знаем точное количество колонок + _rowObjectBuffer = new object[columnsCount ?? 256]; + + _rIdx = 1; + _startRowIdx = Math.Max(_rIdx, startRowIdx); + _sequentialEmptyRowsCount = 0; + } + + /// + /// Создает готовый к работе . + /// + /// Поток с данными файла. + /// Количество колонок в CSV файле. Если не указано, то количество колонок в каждой строке будет определяться автоматически. + /// Номер строки, начиная с которой надо читать файл. + /// Разделитель колонок. + /// Кодировка файлов. Если не указано используем UTF-8 + /// Нужно ли освобождать после завершения чтения. + public static CsvFileDataReader CreateReader( + Stream fileStream, + int? columnsCount = null, + int startRowIdx = 1, + char fieldDelimiter = ';', + Encoding? encoding = null, + bool disposeFileStream = false) + { + Guard.AssertNotNull(fileStream, nameof(fileStream)); + Guard.AssertNotNegative(columnsCount, nameof(columnsCount)); + Guard.AssertNotNegative(startRowIdx, nameof(startRowIdx)); + + return new CsvFileDataReader( + fileStream, + disposeFileStream, + columnsCount, + encoding ?? Encoding.UTF8, + fieldDelimiter, + startRowIdx); + } + + /// + public bool Read() + { + while (true) + { + if (_rIdx > MaxSupportedRowsCount) return false; + if (_sequentialEmptyRowsCount > MaxSequentialEmptyRowsCount) return false; + + _rowStringBuffer.Clear(); + var isEmptyRow = true; + if (_csvReader.Read()) + { + // пропускаем заголовки и прочее + if (_rIdx >= _startRowIdx) + { + // количество ячеек в строке может меняться + // до начала работ мы не можем знать, сколько их там, + // поэтому адаптируем размер буфера для чтения + var factColumnsCount = _csvReader.RowFieldCount; + if (factColumnsCount > _rowObjectBuffer.Length) + { + _rowObjectBuffer = new object[factColumnsCount]; + } + + var columnsCount = _csvReader.GetValues(_rowObjectBuffer); + for (var cIdx = 0; cIdx < columnsCount; cIdx++) + { + var cellValue = _rowObjectBuffer[cIdx].ToString()?.Trim(); + + if (String.IsNullOrWhiteSpace(cellValue)) + { + _rowStringBuffer.Add(null); + } + else + { + isEmptyRow = false; + _rowStringBuffer.Add(cellValue); + } + } + } + } + + _rIdx++; + + if (isEmptyRow) + { + _sequentialEmptyRowsCount++; + _currentRow = null; + + // если прочитали пустую строку, попробуем прочитать еще одну строку, + // чтобы упростить клиентский код и не добавлять в него парсинг пустых строк + + continue; + } + + // если какие-то данные были, сбросим счетчик пустых строк + _sequentialEmptyRowsCount = 0; + + // сохраним данные, чтобы вернуть по требованию + _currentRow = _rowStringBuffer; + + return true; + } + } + + /// + public IReadOnlyList? GetRow() => _currentRow; + + /// + public void Dispose() + { + _csvReader.Dispose(); + _streamReader.Dispose(); + + if (_disposeFileStream) + _fileStream.Dispose(); + } +} diff --git a/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CsvFileWriter.cs b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CsvFileWriter.cs new file mode 100644 index 0000000..dd4e504 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/CsvFileWriter.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using CsvHelper; +using CsvHelper.Configuration; +using Curiosity.FileDataReaderWriters.Style; +using Curiosity.FileDataReaderWriters.Writers; + +namespace Curiosity.FileDataReaderWriters.SylvanCsv; + +/// +/// Запись данных в .csv формате +/// +/// +/// Расходует мало памяти, потому что запись идёт через потоки с буферизацией +/// +public class CsvFileWriter : IFileWriter +{ + private readonly StreamWriter _streamWriter; + private readonly CsvWriter _csvWriter; + + public CsvFileWriter(string outputFilePath) + { + var configuration = new CsvConfiguration(CultureInfo.CurrentCulture) + { + Delimiter = ";", + Mode = CsvMode.RFC4180, + Encoding = Encoding.UTF8, + }; + + _streamWriter = new StreamWriter(outputFilePath); + _csvWriter = new CsvWriter(_streamWriter, configuration); + } + + public int AddFormat(FormatSettings formatSettings) => 0; + + public int AddDefaultFormat() => 0; + + public void AddHeaders(IReadOnlyList data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + AppendLine(data); + } + + public void AppendLine(IReadOnlyList data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + for (var i = 0; i < data.Count; i++) + { + Append(data[i].Value); + } + + EndLine(); + } + + public void Append(object? value, int? format = null) + { + _csvWriter.WriteField(value); + } + + public void EndLine() + { + // библиотека сама флашит строки периодически + _csvWriter.NextRecord(); + } + + public void Flush() + { + _csvWriter.Flush(); + _streamWriter.Flush(); + } + + public void Dispose() + { + _csvWriter.Dispose(); + _streamWriter.Dispose(); + } +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/Curiosity.FileDataReaderWriters.SylvanCsv.csproj b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/Curiosity.FileDataReaderWriters.SylvanCsv.csproj new file mode 100644 index 0000000..493992f --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters.SylvanCsv/Curiosity.FileDataReaderWriters.SylvanCsv.csproj @@ -0,0 +1,50 @@ + + + + netstandard2.1 + + Curiosity.FileDataReaderWriters.SylvanCsv + Curiosity.FileDataReaderWriters.SylvanCsv + Sylvan realization for working with csv files + Read and write to csv file. Low memory consumption due to bufferization. + Curiosity; FileDataReaderWriters; Sylvan; siisltd + English + + 1.0.0 + 1.0.0 + 1.0.0 + + Max Markelow (@markeli), Andrei Vinogradov (@anri-vin), Andrey Ioch (@DevCorvette), Timur Sidoriuk (@shockthunder) + SIIS Ltd + SIIS Ltd, 2024 + + false + MIT + + https://github.com/siisltd/Curiosity.Utils + git + https://github.com/siisltd/Curiosity.Utils + https://github.com/siisltd/Curiosity.Utils/tree/master/src/FileData/Curiosity.FileDataReaderWriters.Npoi/CHANGELOG.md + + 10 + enable + true + + + + siisltd.png + + + + + + + + + + + + + bin\Release\Curiosity.FileDataReaderWriters.Sylvan.xml + + diff --git a/src/FileData/Curiosity.FileDataReaderWriters/CHANGELOG.md b/src/FileData/Curiosity.FileDataReaderWriters/CHANGELOG.md new file mode 100644 index 0000000..46b34f7 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [1.0.0] - 2024-02-02 + +Package was released. diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Curiosity.FileDataReaderWriters.csproj b/src/FileData/Curiosity.FileDataReaderWriters/Curiosity.FileDataReaderWriters.csproj new file mode 100644 index 0000000..e2258f8 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Curiosity.FileDataReaderWriters.csproj @@ -0,0 +1,62 @@ + + + + netstandard2.1 + enable + + Curiosity.FileDataReaderWriters + Curiosity.FileDataReaderWriters + Base classes for working with xls/xlsx/csv files + Base format settings, excel constants and reader/writer intefaces. + Curiosity; FileDataReaderWriters; siisltd + English + + 1.0.0 + 1.0.0 + 1.0.0 + + Max Markelow (@markeli), Andrei Vinogradov (@anri-vin), Andrey Ioch (@DevCorvette), Timur Sidoriuk (@shockthunder) + SIIS Ltd + SIIS Ltd, 2024 + + false + MIT + + https://github.com/siisltd/Curiosity.Utils + git + https://github.com/siisltd/Curiosity.Utils + https://github.com/siisltd/Curiosity.Utils/tree/master/src/FileData/Curiosity.FileDataReaderWriters/CHANGELOG.md + + 10 + enable + true + + + + + siisltd.png + + + + + + + + + + + + + + + + + + $(NoWarn);1591 + $(NoWarn);1573 + + + + bin\Release\Curiosity.FileDataReaderWriters.xml + + diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Helpers/ExcelConstants.cs b/src/FileData/Curiosity.FileDataReaderWriters/Helpers/ExcelConstants.cs new file mode 100644 index 0000000..796f4ad --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Helpers/ExcelConstants.cs @@ -0,0 +1,29 @@ +namespace Curiosity.FileDataReaderWriters.Helpers; + +public static class ExcelConstants +{ + /// + /// Максимальное число строк в XLSX файле + /// + public const int XlsxRowsMax = 1048576; + + /// + /// Максимальное число колонок в XLSX файле + /// + public const int XlsxColMax = 16384; + + /// + /// Максимальное число строк в XLS файле + /// + public const int XlsRowsMax = 65536; + + /// + /// Максимальное число колонок в XLS файле + /// + public const int XlsColMax = 256; + + /// + /// Максимальное количество символов вмещающееся в ячейку + /// + public const int MaxCellLength = 32759; +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Helpers/Guard.cs b/src/FileData/Curiosity.FileDataReaderWriters/Helpers/Guard.cs new file mode 100644 index 0000000..0722901 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Helpers/Guard.cs @@ -0,0 +1,84 @@ +using System.Runtime.CompilerServices; + +namespace Curiosity.FileDataReaderWriters; + +public static class Guard +{ + /// + /// Проверяет, что параметр не null. + /// + /// Значение параметра. + /// Название параметра. + /// Тип параметра. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertNotNull(T? parameter, string parameterName) where T : class + { + if (parameter == null) + throw new ArgumentNullException(parameterName); + } + + /// + /// Проверяет, что параметр не null. + /// + /// Значение параметра. + /// Название параметра. + /// Тип параметра. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertHasValue(Nullable parameter, string parameterName) where T : struct + { + if (!parameter.HasValue) + throw new ArgumentNullException(parameterName); + } + + /// + /// Проверяет, что строка не пустая. + /// + /// Значение параметра + /// Название параметра + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertNotEmpty(string? parameter, string parameterName) + { + if (String.IsNullOrWhiteSpace(parameter)) + throw new ArgumentNullException(parameterName); + } + + /// + /// Проверяет, что значение находится в заданном диапазоне. + /// + /// Значение параметра + /// Минимальное допустимое значение + /// Максимальное допустимое значение + /// Название параметра + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertInRange(int parameter, int minValue, int maxValue, string parameterName) + { + if (parameter < minValue || parameter > maxValue) + throw new ArgumentOutOfRangeException(parameterName); + } + + /// + /// Проверяет, что значение не отрицательно. + /// + /// Значение параметра + /// Название параметра + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertNotNegative(int? parameter, string parameterName) + { + if (!parameter.HasValue) return; + AssertNotNegative(parameter.Value, parameterName); + } + + /// + /// Проверяет, что значение не отрицательно. + /// + /// Значение параметра + /// Название параметра + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertNotNegative(int parameter, string parameterName) + { + if (parameter < 0) + throw new ArgumentOutOfRangeException(parameterName); + } +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Readers/IFileDataReader.cs b/src/FileData/Curiosity.FileDataReaderWriters/Readers/IFileDataReader.cs new file mode 100644 index 0000000..4fe505d --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Readers/IFileDataReader.cs @@ -0,0 +1,40 @@ +using global::System; +using global::System.Collections.Generic; + +namespace Curiosity.FileDataReaderWriters.Readers; + +/// +/// Общий интерфейс для классов, которые читает загруженные файлы построчно. +/// +/// +/// Реализации должны внутри себя инкапсулировать все работу по чтению файла, обработке пустых строк. +/// +public interface IFileDataReader : IDisposable +{ + /// + /// Максимальное количество строк для данного типа файла. + /// + /// + /// Нужно для того, чтобы не пытаться бесконечно читать данные из файла. + /// + int MaxSupportedRowsCount { get; } + + /// + /// Номер текущей строки, которая прочитана. + /// + int CurrentRowIdx { get; } + + /// + /// Читает очередную строку в память и обрабатывает ее. + /// + /// Была ли прочитана еще одна строка? + /// + /// Данные строки можно получить через . + /// + bool Read(); + + /// + /// Возвращает текущую прочитанную строку в виде отдельных ячеек. + /// + IReadOnlyList? GetRow(); +} diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Resources/LNG.cs b/src/FileData/Curiosity.FileDataReaderWriters/Resources/LNG.cs new file mode 100644 index 0000000..5f536d9 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Resources/LNG.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using Curiosity.Localization; + +namespace Curiosity.FileDataReaderWriters.Resources +{ + internal static class LNG + { + private static readonly LocalizerCore Localizer; + private static readonly LocalizationOptions Options; + + static LNG() + { + Options = new LocalizationOptions(); + Localizer = new LocalizerCore(typeof(LNG).Assembly, Options); + } + + public static string Get(string source, params object[] arguments) + { + return Localizer.Get(Options.Prefix, source, false, arguments); + } + } +} diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Resources/Strings.en.resx b/src/FileData/Curiosity.FileDataReaderWriters/Resources/Strings.en.resx new file mode 100644 index 0000000..02f31ed --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Resources/Strings.en.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + File with extension "{0}" is not Excel (.xls/.xlsx) + + + File must contain at least one sheet + + \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Resources/Strings.resx b/src/FileData/Curiosity.FileDataReaderWriters/Resources/Strings.resx new file mode 100644 index 0000000..eade2a3 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Resources/Strings.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Файл с расширением "{0}" не является Excel (.xls/.xlsx) + + + Файл должен содержать хотя бы один лист + + \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Style/FontStyle.cs b/src/FileData/Curiosity.FileDataReaderWriters/Style/FontStyle.cs new file mode 100644 index 0000000..fdcae65 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Style/FontStyle.cs @@ -0,0 +1,11 @@ +using global::System; + +namespace Curiosity.FileDataReaderWriters.Style; + +[Flags] +public enum FontStyle +{ + None = 0, + Bold = 0x01, + Italic = 0x02 +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Style/FormatSettings.cs b/src/FileData/Curiosity.FileDataReaderWriters/Style/FormatSettings.cs new file mode 100644 index 0000000..1ce426a --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Style/FormatSettings.cs @@ -0,0 +1,32 @@ +namespace Curiosity.FileDataReaderWriters.Style; + +/// +/// Настройки формата ячеек +/// +public class FormatSettings +{ + /// + /// Начертание шрифта + /// + public FontStyle FontStyle { get; set; } + + /// + /// Горизонтальное выранивание текста + /// + public TextAlignment TextAlignment { get; set; } + + /// + /// Перенос слов на новую строку + /// + public bool WrapText { get; set; } + + /// + /// Размер шрифта в пунктах + /// + public int FontSize { get; set; } = 10; + + /// + /// Строка, указывающая формат (текст/число/деньги и т.д.). Можно посмотреть в форматах Excel + /// + public string DataFormat { get; set; } = "@"; +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Style/TextAlignment.cs b/src/FileData/Curiosity.FileDataReaderWriters/Style/TextAlignment.cs new file mode 100644 index 0000000..43355e2 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Style/TextAlignment.cs @@ -0,0 +1,9 @@ +namespace Curiosity.FileDataReaderWriters.Style; + +public enum TextAlignment +{ + Left = 0, + Center = 1, + Right = 2, + Justify = 3 +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Writers/CellData.cs b/src/FileData/Curiosity.FileDataReaderWriters/Writers/CellData.cs new file mode 100644 index 0000000..0d1d2f5 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Writers/CellData.cs @@ -0,0 +1,16 @@ +namespace Curiosity.FileDataReaderWriters.Writers; + +/// +/// Данные ячейки с форматом +/// +public class CellData +{ + public object? Value { get; } + public int? Format { get; } + + public CellData(object? value, int? format = null) + { + Value = value; + Format = format; + } +} \ No newline at end of file diff --git a/src/FileData/Curiosity.FileDataReaderWriters/Writers/IFileWriter.cs b/src/FileData/Curiosity.FileDataReaderWriters/Writers/IFileWriter.cs new file mode 100644 index 0000000..559e556 --- /dev/null +++ b/src/FileData/Curiosity.FileDataReaderWriters/Writers/IFileWriter.cs @@ -0,0 +1,50 @@ +using Curiosity.FileDataReaderWriters.Style; +using global::System; +using global::System.Collections.Generic; + +namespace Curiosity.FileDataReaderWriters.Writers; + +/// +/// Интерфейс для простого построчного вывода данных +/// +public interface IFileWriter : IDisposable +{ + /// + /// Добавляет формат с указанными настройками + /// + /// + /// + int AddFormat(FormatSettings formatSettings); + + /// + /// Добавляет формат по умолчанию. В Excel он называется "Общий/General" + /// + /// + int AddDefaultFormat(); + + /// + /// Добавляет заголовки таблицы + /// + public void AddHeaders(IReadOnlyList data); + + /// + /// Добавляет массив данных в 1 строку. + /// Автоматически переходит на новую строку + /// + public void AppendLine(IReadOnlyList data); + + /// + /// Добавляет данные в текущую строку без перехода на новую + /// + public void Append(object? value, int? format = null); + + /// + /// Переход на новую строку + /// + public void EndLine(); + + /// + /// Записывает данные из буфера в файл + /// + public void Flush(); +} \ No newline at end of file diff --git a/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/Curiosity.FileDataReaderWriters.UnitTests.csproj b/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/Curiosity.FileDataReaderWriters.UnitTests.csproj new file mode 100644 index 0000000..ce34f46 --- /dev/null +++ b/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/Curiosity.FileDataReaderWriters.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/NpoiXlsFileWriter_Should.cs b/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/NpoiXlsFileWriter_Should.cs new file mode 100644 index 0000000..8c5758c --- /dev/null +++ b/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/NpoiXlsFileWriter_Should.cs @@ -0,0 +1,87 @@ +using Curiosity.FileDataReaderWriters.Npoi; +using Curiosity.FileDataReaderWriters.Style; +using Curiosity.FileDataReaderWriters.Writers; +using Microsoft.Extensions.Logging; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using Xunit; + +namespace Curiosity.FileDataReaderWriters.UnitTests +{ + /// + /// Тесты записи для NPOI + /// + public class NpoiXlsFileWriter_Should + { + // [Fact] + public void WriteWithHeaders() + { + //ARRANGE + var fileName = "testFile.xlsx"; + var header1 = "Header1"; + var header2 = "Header2"; + var header3 = "Header3"; + var headerFormat = new FormatSettings() + { + FontSize = 20, + FontStyle = FontStyle.Bold, + TextAlignment = TextAlignment.Center, + DataFormat = "@" + }; + + var logger = new Logger(new LoggerFactory()); + var fileWriter = new NpoiXlsFileWriter(fileName, logger); + var formatNumber = fileWriter.AddFormat(headerFormat); + + var headers = new List(); + headers.Add(new CellData(header1, formatNumber)); + headers.Add(new CellData(header2)); + headers.Add(new CellData(header3, formatNumber)); + + var firstRow = new List(); + firstRow.Add(new CellData(1.23)); + firstRow.Add(new CellData(DateTime.Now)); + firstRow.Add(new CellData("blabla")); + firstRow.Add(new CellData(null)); + + var secondRow = new List(); + secondRow.Add(new CellData(3.43)); + secondRow.Add(new CellData(DateTime.UtcNow)); + secondRow.Add(new CellData("blabla2")); + + //ACT + fileWriter.AddHeaders(headers); + fileWriter.AppendLine(firstRow); + fileWriter.AppendLine(secondRow); + fileWriter.Dispose(); + + //ASSERT + try + { + var stream = File.Open(fileName, FileMode.Open, FileAccess.Read); + var fileReader = new XSSFWorkbook(stream); + var sheet = fileReader.GetSheetAt(0); + + var headersResult = sheet.GetRow(0); + Assert.Equal(headersResult.GetCell(0).StringCellValue, header1); + Assert.Equal(headersResult.GetCell(1).StringCellValue, header2); + Assert.Equal(headersResult.GetCell(2).StringCellValue, header3); + Assert.Equal(headersResult.GetCell(0).CellStyle.Alignment, HorizontalAlignment.Center); + Assert.Equal(headersResult.GetCell(2).CellStyle.GetDataFormatString(), headerFormat.DataFormat); + + var firstRowResult = sheet.GetRow(1); + Assert.Equal(firstRowResult.GetCell(0).NumericCellValue, firstRow[0].Value); + Assert.Equal(firstRowResult.GetCell(1).DateCellValue.ToLongDateString(), DateTime.Parse(firstRow[1].Value.ToString()).ToLongDateString()); + Assert.Equal(firstRowResult.GetCell(2).StringCellValue, firstRow[2].Value); + + var secondRowResult = sheet.GetRow(2); + Assert.Equal(secondRowResult.GetCell(0).NumericCellValue, secondRow[0].Value); + Assert.Equal(secondRowResult.GetCell(1).DateCellValue.ToLongDateString(), DateTime.Parse(secondRow[1].Value.ToString()).ToLongDateString()); + Assert.Equal(secondRowResult.GetCell(2).StringCellValue, secondRow[2].Value); + } + finally{ + File.Delete(fileName); + } + } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/NpoiXlsMultiFileWriter_Should.cs b/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/NpoiXlsMultiFileWriter_Should.cs new file mode 100644 index 0000000..857c664 --- /dev/null +++ b/tests/UnitTests/Curiosity.FileDataReaderWriters.UnitTests/NpoiXlsMultiFileWriter_Should.cs @@ -0,0 +1,112 @@ +using Curiosity.FileDataReaderWriters.Npoi; +using Curiosity.FileDataReaderWriters.Style; +using Curiosity.FileDataReaderWriters.Writers; +using Microsoft.Extensions.Logging; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using Xunit; + +namespace Curiosity.FileDataReaderWriters.UnitTests +{ + /// + /// Тесты записи для NPOI + /// + public class NpoiXlsMultiFileWriter_Should + { + // [Fact] + public void WriteWithHeaders() + { + //ARRANGE + var fileName = "testFile.xlsx"; + var header1 = "Header1"; + var header2 = "Header2"; + var header3 = "Header3"; + var headerFormat = new FormatSettings() + { + FontSize = 20, + FontStyle = FontStyle.Bold, + TextAlignment = TextAlignment.Center, + DataFormat = "@" + }; + + var logger = new Logger(new LoggerFactory()); + var fileWriter = new NpoiXlsMultiFileWriter(Directory.GetCurrentDirectory(), fileName, logger); + var formatNumber = fileWriter.AddFormat(headerFormat); + + var headers = new List(); + headers.Add(new CellData(header1, formatNumber)); + headers.Add(new CellData(header2)); + headers.Add(new CellData(header3, formatNumber)); + + var firstRow = new List(); + firstRow.Add(new CellData(1.23)); + firstRow.Add(new CellData(DateTime.Now)); + firstRow.Add(new CellData("blabla")); + firstRow.Add(new CellData(null)); + + var secondRow = new List(); + secondRow.Add(new CellData(3.43)); + secondRow.Add(new CellData(DateTime.UtcNow)); + secondRow.Add(new CellData("blabla2")); + + var secondFileData = Enumerable.Range(0, 1100000).Select(x => new CellData((double)x)); + + //ACT + fileWriter.AddHeaders(headers); + fileWriter.AppendLine(firstRow); + fileWriter.AppendLine(secondRow); + foreach (var data in secondFileData) + { + fileWriter.Append(data.Value); + fileWriter.EndLine(); + } + + fileWriter.Dispose(); + + //ASSERT + try + { + var stream = File.Open(fileName, FileMode.Open, FileAccess.Read); + var fileReader = new XSSFWorkbook(stream); + var sheet = fileReader.GetSheetAt(0); + + var headersResult = sheet.GetRow(0); + Assert.Equal(headersResult.GetCell(0).StringCellValue, header1); + Assert.Equal(headersResult.GetCell(1).StringCellValue, header2); + Assert.Equal(headersResult.GetCell(2).StringCellValue, header3); + Assert.Equal(headersResult.GetCell(0).CellStyle.Alignment, HorizontalAlignment.Center); + Assert.Equal(headersResult.GetCell(2).CellStyle.GetDataFormatString(), headerFormat.DataFormat); + + var firstRowResult = sheet.GetRow(1); + Assert.Equal(firstRowResult.GetCell(0).NumericCellValue, firstRow[0].Value); + Assert.Equal(firstRowResult.GetCell(1).DateCellValue.ToLongDateString(), DateTime.Parse(firstRow[1].Value.ToString()).ToLongDateString()); + Assert.Equal(firstRowResult.GetCell(2).StringCellValue, firstRow[2].Value); + + var secondRowResult = sheet.GetRow(2); + Assert.Equal(secondRowResult.GetCell(0).NumericCellValue, secondRow[0].Value); + Assert.Equal(secondRowResult.GetCell(1).DateCellValue.ToLongDateString(), DateTime.Parse(secondRow[1].Value.ToString()).ToLongDateString()); + Assert.Equal(secondRowResult.GetCell(2).StringCellValue, secondRow[2].Value); + + stream.Close(); + + var streamPart2 = File.Open($"{Path.GetFileNameWithoutExtension(fileName)}_part_2.xlsx", FileMode.Open, FileAccess.Read); + + var fileReaderPart2 = new XSSFWorkbook(streamPart2); + var sheetPart2 = fileReaderPart2.GetSheetAt(0); + + var headersResultPart2 = sheetPart2.GetRow(0); + Assert.Equal(headersResultPart2.GetCell(0).StringCellValue, header1); + Assert.Equal(headersResultPart2.GetCell(1).StringCellValue, header2); + Assert.Equal(headersResultPart2.GetCell(2).StringCellValue, header3); + + var firstRowPart2 = sheetPart2.GetRow(1); + Assert.Equal(firstRowPart2.GetCell(0).NumericCellValue, 1048573); + } + finally + { + File.Delete(fileName); + File.Delete($"{Path.GetFileNameWithoutExtension(fileName)}_part_2.xlsx"); + } + } + } +} \ No newline at end of file