17 Commits 169a866482 ... f232275980

Author SHA1 Message Date
  Piotr Czajkowski f232275980 Info about version 2 2 months ago
  Piotr Czajkowski cdab5f084d No trick 2 months ago
  Piotr Czajkowski 5ad168d310 More order 2 months ago
  Piotr Czajkowski b0729f13f5 Order 2 months ago
  Piotr Czajkowski 9ff7824252 Added WriteAll 2 months ago
  Piotr Czajkowski 3fa518940e Added ExcelDynamicWriter 2 months ago
  Piotr Czajkowski 44cc1a0413 Added ReadDifficult 2 months ago
  Piotr Czajkowski 64ffc60617 We need constant number of columns, even if empty 2 months ago
  Piotr Czajkowski 1cbe2aa0e3 Aligned interfaces 2 months ago
  Piotr Czajkowski dd7cede41c Added ReadAll 2 months ago
  Piotr Czajkowski 0276fbffb8 Able to get worksheet by position 2 months ago
  Piotr Czajkowski d23b3ea5cd This should work better 2 months ago
  Piotr Czajkowski 2ec47d386d Able to get type 2 months ago
  Piotr Czajkowski 4190ba370c Have a first test 2 months ago
  Piotr Czajkowski 481c510f1a Added ExcelDynamicReader 2 months ago
  Piotr Czajkowski 42fb40709d Added MapHeader 2 months ago
  Piotr Czajkowski ffa0ea667a Starting with dynamic 2 months ago

+ 106 - 0
ExcelORM/ExcelORM/ExcelDynamicReader.cs

@@ -0,0 +1,106 @@
+using ClosedXML.Excel;
+using ExcelORM.Models;
+
+namespace ExcelORM;
+
+public class ExcelDynamicReader
+{
+    private readonly IXLWorkbook xlWorkbook;
+    public bool SkipHidden { get; set; }
+    public bool ObeyFilter { get; set; }
+
+    public ExcelDynamicReader(string? path)
+    {
+        xlWorkbook = new XLWorkbook(path);
+    }
+
+    private IEnumerable<List<DynamicCell>> ProcessRows(IEnumerable<IXLRow> rows, List<DynamicCell> mapping)
+    {
+        foreach (var row in rows)
+        {
+            if (SkipHidden && row.IsHidden) continue;
+
+            var dynamicRow = new List<DynamicCell>();
+            foreach (var item in mapping)
+            {
+                var cell = row.Cell(item.Position);
+                if (cell == null || cell.Value.IsBlank)
+                {
+                    dynamicRow.Add(item);
+                    continue;
+                }
+
+                if (item.Type == null) item.Type = cell.Value.ValueType();
+
+                var cellItem = item with
+                {
+                    Value = cell.Value.ToObject()
+                };
+
+                dynamicRow.Add(cellItem);
+            }
+
+            yield return dynamicRow;
+        }
+    }
+
+    private IEnumerable<List<DynamicCell>> Read(IXLWorksheet? worksheet, uint startFrom = 1, uint skip = 0)
+    {
+        if (worksheet == null) yield break;
+
+        var firstRow = worksheet.Row((int)startFrom);
+        if (firstRow.IsEmpty())
+            firstRow = worksheet.RowsUsed().First(x => x.RowNumber() > startFrom && !x.IsEmpty());
+
+        var mapping = DynamicCell.MapHeader(firstRow.CellsUsed());
+        if (mapping == null || mapping.Count == 0) yield break;
+
+        var rowsToProcess = (ObeyFilter && worksheet.AutoFilter.IsEnabled) switch
+        {
+            true => worksheet.AutoFilter.VisibleRows
+                .Where(x => x.RowNumber() > firstRow.RowNumber())
+                .Select(x => x.WorksheetRow()),
+            false => worksheet.RowsUsed().Where(x => x.RowNumber() > firstRow.RowNumber())
+
+        };
+
+        rowsToProcess = rowsToProcess
+            .Skip((int)skip);
+
+        foreach (var item in ProcessRows(rowsToProcess, mapping))
+            yield return item;
+    }
+
+    public IEnumerable<List<DynamicCell>> Read(string? worksheetName, uint startFrom = 1, uint skip = 0)
+    {
+        var worksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Name.Equals(worksheetName, StringComparison.InvariantCultureIgnoreCase));
+        if (worksheet == null) yield break;
+
+        foreach (var value in Read(worksheet, startFrom, skip))
+            yield return value;
+    }
+
+    public IEnumerable<List<DynamicCell>> Read(int worksheetIndex = 1, uint startFrom = 1, uint skip = 0)
+    {
+        if (worksheetIndex > xlWorkbook.Worksheets.Count) yield break;
+
+        var worksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Position == worksheetIndex);
+        if (worksheet == null) yield break;
+
+        foreach (var value in Read(worksheet, startFrom, skip))
+            yield return value;
+    }
+
+    public IEnumerable<DynamicWorksheet> ReadAll(uint startFrom = 1, uint skip = 0)
+    {
+        foreach (var worksheet in xlWorkbook.Worksheets)
+        {
+            yield return new DynamicWorksheet
+            {
+                Name = worksheet.Name,
+                Position = worksheet.Position,
+                Cells = Read(worksheet, startFrom, skip)
+            };
+        }
+    }
+}

+ 70 - 0
ExcelORM/ExcelORM/ExcelDynamicWriter.cs

@@ -0,0 +1,70 @@
+using ClosedXML.Excel;
+using ExcelORM.Models;
+
+namespace ExcelORM;
+
+public class ExcelDynamicWriter
+{
+    private readonly IXLWorkbook xlWorkbook;
+    public ExcelDynamicWriter(string? path = null)
+    {
+        xlWorkbook = File.Exists(path) ? new XLWorkbook(path) : new XLWorkbook();
+    }
+
+    private static int GenerateHeader(IXLWorksheet worksheet, IEnumerable<DynamicCell> firstRow)
+    {
+        var rowIndex = 1;
+        foreach (var item in firstRow)
+            worksheet.Cell(rowIndex, item.Position).Value = item.Header;
+
+        return ++rowIndex;
+    }
+
+    private static void Write(IEnumerable<List<DynamicCell>> values, IXLWorksheet worksheet, bool append)
+    {
+        var rowIndex = append switch
+        {
+            true => worksheet.LastRowUsed().RowNumber() + 1,
+            false => GenerateHeader(worksheet, values.First()),
+        };
+
+        foreach (var row in values)
+        {
+            foreach (var cell in row)
+            {
+                if (cell.Value == null) continue;
+
+                worksheet.Cell(rowIndex, cell.Position).Value = XLCellValue.FromObject(cell.Value);
+            }
+
+            rowIndex++;
+        }
+    }
+
+    public void Write(IEnumerable<List<DynamicCell>>? values, string? worksheetName = null, bool append = false)
+    {
+        if (values == null) return;
+
+        var xlWorksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Name.Equals(worksheetName, StringComparison.InvariantCultureIgnoreCase));
+        
+        xlWorksheet ??= !string.IsNullOrWhiteSpace(worksheetName) ?
+            xlWorkbook.AddWorksheet(worksheetName)
+            : xlWorkbook.Worksheets.Count == 0 ? xlWorkbook.AddWorksheet() : xlWorkbook.Worksheets.First();
+
+        Write(values, xlWorksheet, append);
+    }
+
+    public void WriteAll(IEnumerable<DynamicWorksheet>? dynamicWorksheets, bool append = false)
+    {
+        if (dynamicWorksheets == null) return;
+
+        foreach (var dynamicWorksheet in dynamicWorksheets)
+            Write(dynamicWorksheet.Cells, dynamicWorksheet.Name, append);
+    }
+
+    public void SaveAs(string path, IExcelConverter? converter = null)
+    {
+        xlWorkbook.SaveAs(path);
+        converter?.MakeCompatible(path);
+    } 
+}

+ 3 - 3
ExcelORM/ExcelORM/ExcelORM.csproj

@@ -4,7 +4,7 @@
         <TargetFramework>net7.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
-        <Version>1.0.4</Version>
+        <Version>2.0.0</Version>
         <PackageProjectUrl>https://git.liox.eu/pczajkowski/ExcelORM</PackageProjectUrl>
         <RepositoryUrl>https://github.com/pczajkowski/ExcelORM</RepositoryUrl>
         <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
@@ -19,8 +19,8 @@
 
     <ItemGroup>
 	    <PackageReference Include="ClosedXML" Version="0.102.1" />
-	    <None Include="../../README.md" Pack="true" PackagePath="\"/>
-	    <None Include="../../LICENSE" Pack="true" PackagePath=""/>
+	    <None Include="../../README.md" Pack="true" PackagePath="\" />
+	    <None Include="../../LICENSE" Pack="true" PackagePath="" />
     </ItemGroup>
 
 </Project>

+ 34 - 18
ExcelORM/ExcelORM/ExcelReader.cs

@@ -1,4 +1,5 @@
 using ClosedXML.Excel;
+using ExcelORM.Models;
 
 namespace ExcelORM;
 
@@ -38,20 +39,6 @@ public class ExcelReader
         }
     }
 
-    public IEnumerable<T> Read<T>(uint startFrom = 1, uint skip = 0) where T : class, new()
-    {
-        return xlWorkbook.Worksheets.SelectMany(worksheet => Read<T>(worksheet, startFrom, skip));
-    }
-
-    public IEnumerable<T> Read<T>(string? worksheetName, uint startFrom = 1, uint skip = 0) where T : class, new()
-    {
-        var worksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Name.Equals(worksheetName, StringComparison.InvariantCultureIgnoreCase));
-        if (worksheet == null) yield break;
-
-        foreach (var value in Read<T>(worksheet, startFrom, skip))
-            yield return value;
-    }
-
     private IEnumerable<T> Read<T>(IXLWorksheet? worksheet, uint startFrom, uint skip) where T : class, new()
     {
         if (worksheet == null) yield break;
@@ -59,7 +46,7 @@ public class ExcelReader
         var firstRow = worksheet.Row((int)startFrom);
         if (firstRow.IsEmpty())
             firstRow = worksheet.RowsUsed().First(x => x.RowNumber() > startFrom && !x.IsEmpty());
-        
+
         var mapping = Mapping.MapProperties<T>(firstRow.CellsUsed());
         if (mapping == null) yield break;
 
@@ -69,13 +56,42 @@ public class ExcelReader
                 .Where(x => x.RowNumber() > firstRow.RowNumber())
                 .Select(x => x.WorksheetRow()),
             false => worksheet.RowsUsed().Where(x => x.RowNumber() > firstRow.RowNumber())
-                
+
         };
 
         rowsToProcess = rowsToProcess
             .Skip((int)skip);
-        
+
         foreach (var item in ProcessRows<T>(rowsToProcess, mapping))
             yield return item;
-    } 
+    }
+
+    public IEnumerable<T> Read<T>(string? worksheetName, uint startFrom = 1, uint skip = 0) where T : class, new()
+    {
+        var worksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Name.Equals(worksheetName, StringComparison.InvariantCultureIgnoreCase));
+        if (worksheet == null) yield break;
+
+        foreach (var value in Read<T>(worksheet, startFrom, skip))
+            yield return value;
+    }
+
+    public IEnumerable<T> Read<T>(int worksheetIndex = 1, uint startFrom = 1, uint skip = 0) where T : class, new()
+    {
+        if (worksheetIndex > xlWorkbook.Worksheets.Count) yield break;
+
+        var worksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Position == worksheetIndex);
+        if (worksheet == null) yield break;
+
+        foreach (var value in Read<T>(worksheet, startFrom, skip))
+            yield return value;
+    }
+
+    public IEnumerable<T> ReadAll<T>(uint startFrom = 1, uint skip = 0) where T : class, new()
+    {
+        foreach (var worksheet in xlWorkbook.Worksheets)
+        {
+            foreach (var item in Read<T>(worksheet, startFrom, skip))
+                yield return item;
+        }
+    }
 }

+ 12 - 12
ExcelORM/ExcelORM/ExcelWriter.cs

@@ -26,17 +26,6 @@ public class ExcelWriter
         return ++rowIndex;
     }
 
-    public void Write<T>(IEnumerable<T> values, string? worksheetName = null, bool append = false) where T : class, new()
-    {
-        var xlWorksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Name.Equals(worksheetName, StringComparison.InvariantCultureIgnoreCase));
-        
-        xlWorksheet ??= !string.IsNullOrWhiteSpace(worksheetName) ?
-            xlWorkbook.AddWorksheet(worksheetName)
-            : xlWorkbook.Worksheets.Count == 0 ? xlWorkbook.AddWorksheet() : xlWorkbook.Worksheets.First();
-
-        Write(values, xlWorksheet, append);
-    }
-
     private static void Write<T>(IEnumerable<T> values, IXLWorksheet worksheet, bool append) where T : class, new()
     {
         var enumerable = values as T[] ?? values.ToArray();
@@ -56,7 +45,7 @@ public class ExcelWriter
             {
                 var valueToSet = property.GetValue(value);
                 if (valueToSet == null) continue;
-                
+
                 worksheet.Cell(rowIndex, cellIndex).Value = XLCellValue.FromObject(valueToSet);
                 cellIndex++;
             }
@@ -65,6 +54,17 @@ public class ExcelWriter
         }
     }
 
+    public void Write<T>(IEnumerable<T> values, string? worksheetName = null, bool append = false) where T : class, new()
+    {
+        var xlWorksheet = xlWorkbook.Worksheets.FirstOrDefault(x => x.Name.Equals(worksheetName, StringComparison.InvariantCultureIgnoreCase));
+        
+        xlWorksheet ??= !string.IsNullOrWhiteSpace(worksheetName) ?
+            xlWorkbook.AddWorksheet(worksheetName)
+            : xlWorkbook.Worksheets.Count == 0 ? xlWorkbook.AddWorksheet() : xlWorkbook.Worksheets.First();
+
+        Write(values, xlWorksheet, append);
+    }
+
     public void SaveAs(string path, IExcelConverter? converter = null)
     {
         xlWorkbook.SaveAs(path);

+ 31 - 0
ExcelORM/ExcelORM/Models/DynamicCell.cs

@@ -0,0 +1,31 @@
+using ClosedXML.Excel;
+
+namespace ExcelORM.Models
+{
+    public record DynamicCell
+    {
+        public int Position { get; set; }
+        public string? Header { get; set; }
+        public Type? Type { get; set; }
+        public object? Value { get; set; }
+
+        public static List<DynamicCell>? MapHeader(IXLCells? headerCells)
+        {
+            if (headerCells == null || !headerCells.Any()) return null;
+
+            var map = new List<DynamicCell>();
+            foreach(var cell in headerCells)
+            {
+                var headerItem = new DynamicCell
+                {
+                    Position = cell.Address.ColumnNumber,
+                    Header = cell.Value.GetText()
+                };
+
+                map.Add(headerItem);
+            }
+
+            return map;
+        }
+    }
+}

+ 9 - 0
ExcelORM/ExcelORM/Models/DynamicWorksheet.cs

@@ -0,0 +1,9 @@
+namespace ExcelORM.Models
+{
+    public record DynamicWorksheet
+    {
+        public string? Name { get; set; }
+        public int Position { get; set; }
+        public IEnumerable<List<DynamicCell>>? Cells { get; set; }
+    }
+}

+ 16 - 2
ExcelORM/ExcelORM/TypeExtensions.cs

@@ -3,10 +3,10 @@ using ClosedXML.Excel;
 
 namespace ExcelORM;
 
-// Borrowed from https://github.com/ClosedXML/ClosedXML/blob/develop/ClosedXML/Excel/XLCellValue.cs#L361
 public static class TypeExtensions
 {
-    private static object? ToObject(this XLCellValue value)
+    // Borrowed from https://github.com/ClosedXML/ClosedXML/blob/develop/ClosedXML/Excel/XLCellValue.cs#L361
+    public static object? ToObject(this XLCellValue value)
     {
         return value.Type switch
         {
@@ -21,6 +21,20 @@ public static class TypeExtensions
         };
     }
 
+    public static Type ValueType(this XLCellValue value)
+    {
+        return value.Type switch
+        {
+            XLDataType.Blank => typeof(string),
+            XLDataType.Boolean => typeof(bool),
+            XLDataType.Number => typeof(double?),
+            XLDataType.Text => typeof(string),
+            XLDataType.DateTime => typeof(DateTime?),
+            XLDataType.TimeSpan => typeof(TimeSpan?),
+            _ => throw new InvalidCastException()
+        };
+    }
+
     public static void SetPropertyValue<T>(this T currentObject, PropertyInfo property, XLCellValue value)
     {
         var valueToSet = value.ToObject();

+ 51 - 0
ExcelORM/ExcelORMTests/DynamicReaderTests.cs

@@ -0,0 +1,51 @@
+using ExcelORM;
+
+namespace ExcelORMTests;
+
+public class DynamicReaderTests
+{
+    private const string RegularFile = "testFiles/first.xlsx";
+    private const string DifferentTypesFile = "testFiles/differentTypes.xlsx";
+    private const string MultipleSheetsFile = "testFiles/multipleSheets.xlsx";
+    private const string DifficultFile = "testFiles/dynamicDifficult.xlsx";
+
+    [Fact]
+    public void Read()
+    {
+        var reader = new ExcelDynamicReader(RegularFile);
+        var results = reader.Read("Sheet 1").ToArray();
+        Assert.NotEmpty(results);
+    }
+
+    [Fact]
+    public void ReadDifferentTypes()
+    {
+        var reader = new ExcelDynamicReader(DifferentTypesFile);
+        var results = reader.Read().ToArray();
+        Assert.NotEmpty(results);
+
+        var first = results.First();
+        Assert.Equal(typeof(string), first[0].Type);
+        Assert.Equal(typeof(DateTime?), first[1].Type);
+        Assert.Equal(typeof(TimeSpan?), first[2].Type);
+        Assert.Equal(typeof(double?), first[3].Type);
+        Assert.Equal(typeof(double?), first[4].Type);
+    }
+
+    [Fact]
+    public void ReadAll()
+    {
+        var reader = new ExcelDynamicReader(MultipleSheetsFile);
+        var results = reader.ReadAll().ToArray();
+        Assert.NotEmpty(results);
+    }
+
+    [Fact]
+    public void ReadDifficult()
+    {
+        var reader = new ExcelDynamicReader(DifficultFile);
+        var results = reader.Read().ToArray();
+        Assert.NotEmpty(results);
+        Assert.Equal(results.First().Count, results.Last().Count);
+    }
+}

+ 57 - 0
ExcelORM/ExcelORMTests/DynamicWriterTests.cs

@@ -0,0 +1,57 @@
+using ExcelORM;
+
+namespace ExcelORMTests;
+
+public class DynamicWriterTests
+{
+    private const string DifficultFile = "testFiles/dynamicDifficult.xlsx";
+    private const string MultipleSheetsFile = "testFiles/multipleSheets.xlsx";
+
+    [Fact]
+    public void Write()
+    {
+        var testFile = Path.GetRandomFileName();
+        testFile = Path.ChangeExtension(testFile, "xlsx");
+
+        var reader = new ExcelDynamicReader(DifficultFile);
+        var results = reader.Read().ToArray();
+        Assert.NotEmpty(results);
+
+        var writer = new ExcelDynamicWriter();
+        writer.Write(results);
+        writer.SaveAs(testFile);
+
+        var savedReader = new ExcelDynamicReader(testFile);
+        var savedResults = savedReader.Read().ToArray();
+        Assert.NotEmpty(savedResults);
+        Assert.True(results.First().SequenceEqual(savedResults.First()));
+        Assert.True(results.Last().SequenceEqual(savedResults.Last()));
+
+        File.Delete(testFile);
+    }
+
+    [Fact]
+    public void WriteAll()
+    {
+        var testFile = Path.GetRandomFileName();
+        testFile = Path.ChangeExtension(testFile, "xlsx");
+
+        var reader = new ExcelDynamicReader(MultipleSheetsFile);
+        var results = reader.ReadAll().ToArray();
+        Assert.NotEmpty(results);
+        
+        var writer = new ExcelDynamicWriter();
+        writer.WriteAll(results);
+        writer.SaveAs(testFile);
+
+        var savedReader = new ExcelDynamicReader(testFile);
+        var savedResults = savedReader.ReadAll().ToArray();
+        Assert.NotEmpty(savedResults);
+        Assert.Equal(results.First().Name, savedResults.First().Name);
+        Assert.Equal(results.First().Cells?.Count(), savedResults.First().Cells?.Count());
+        Assert.Equal(results.Last().Name, savedResults.Last().Name);
+        Assert.Equal(results.Last().Cells?.Count(), savedResults.Last().Cells?.Count());
+
+        File.Delete(testFile);
+    }
+}

+ 5 - 2
ExcelORM/ExcelORMTests/ExcelORMTests.csproj

@@ -10,8 +10,8 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/>
-        <PackageReference Include="xunit" Version="2.4.2"/>
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
+        <PackageReference Include="xunit" Version="2.4.2" />
         <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
             <PrivateAssets>all</PrivateAssets>
@@ -27,6 +27,9 @@
     </ItemGroup>
 
     <ItemGroup>
+      <None Update="testFiles\dynamicDifficult.xlsx">
+        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      </None>
       <None Update="testFiles\hidden.xlsx">
         <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       </None>

+ 1 - 1
ExcelORM/ExcelORMTests/ReaderTests.cs

@@ -63,7 +63,7 @@ public class ReaderTests
     public void ReadMultipleSheets()
     {
         var reader = new ExcelReader(MultipleSheetsFile);
-        var results = reader.Read<Test>().ToArray();
+        var results = reader.ReadAll<Test>().ToArray();
         Assert.NotEmpty(results);
         Assert.Equal(6, results.Length);
     }

BIN
ExcelORM/ExcelORMTests/testFiles/dynamicDifficult.xlsx


+ 3 - 1
README.md

@@ -15,6 +15,8 @@ It currently supports properties of types as supported by ClosedXML, so:
 - DateTime
 - TimeSpan
 
-And thanks to my "trick" also their nullable variants.
+And their nullable variants.
+
+In version 2 I've added ability to read data dynamically without a need to create a special type. Useful when you need to read/write some not so organized data.
 
 As always, feel free to use it however you desire. But I provide you with no guarantee whatsoever. Enjoy!