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