diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index c6ee8fae..fadcc395 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -62,7 +62,7 @@ public int[] SaveAs() GenerateEndXml(); _archive.Dispose(); - + return rowsWritten.ToArray(); } @@ -193,22 +193,23 @@ private int WriteValues(MiniExcelStreamWriter writer, object values) WriteEmptySheet(writer); return 0; } - var maxColumnIndex = props.Count; + int maxRowIndex; + var maxColumnIndex = props.Count(x => x != null && !x.ExcelIgnore); writer.Write(WorksheetXml.StartWorksheetWithRelationship); long dimensionPlaceholderPostition = 0; // We can write the dimensions directly if the row count is known - if (_configuration.FastMode && !isKnownCount) + if (isKnownCount) { - dimensionPlaceholderPostition = WriteDimensionPlaceholder(writer); + maxRowIndex = _printHeader ? count + 1 : count; + writer.Write(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); } - else if (isKnownCount) + else if (_configuration.FastMode) { - maxRowIndex = count + (_printHeader ? 1 : 0); - writer.Write(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, props.Count))); + dimensionPlaceholderPostition = WriteDimensionPlaceholder(writer); } //sheet view @@ -219,7 +220,7 @@ private int WriteValues(MiniExcelStreamWriter writer, object values) long columnWidthsPlaceholderPosition = 0; if (_configuration.EnableAutoWidth) { - columnWidthsPlaceholderPosition = WriteColumnWidthPlaceholders(writer, props); + columnWidthsPlaceholderPosition = WriteColumnWidthPlaceholders(writer, maxColumnIndex); widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); } else @@ -263,21 +264,23 @@ private int WriteValues(MiniExcelStreamWriter writer, object values) } if (_configuration.EnableAutoWidth) { - OverWriteColumnWidthPlaceholders(writer, columnWidthsPlaceholderPosition, widths?.Columns); + OverwriteColumnWidthPlaceholders(writer, columnWidthsPlaceholderPosition, widths?.Columns); } - var toSubtract = _printHeader ? 1 : 0; - return maxRowIndex - toSubtract; + if (_printHeader) + maxRowIndex--; + + return maxRowIndex; } - private static long WriteColumnWidthPlaceholders(MiniExcelStreamWriter writer, ICollection props) + private static long WriteColumnWidthPlaceholders(MiniExcelStreamWriter writer, int count) { var placeholderPosition = writer.Flush(); - writer.WriteWhitespace(WorksheetXml.GetColumnPlaceholderLength(props.Count)); + writer.WriteWhitespace(WorksheetXml.GetColumnPlaceholderLength(count)); return placeholderPosition; } - private static void OverWriteColumnWidthPlaceholders(MiniExcelStreamWriter writer, long placeholderPosition, IEnumerable columnWidths) + private static void OverwriteColumnWidthPlaceholders(MiniExcelStreamWriter writer, long placeholderPosition, IEnumerable columnWidths) { var position = writer.Flush(); @@ -300,29 +303,30 @@ private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable } writer.Write(WorksheetXml.Column(column.Index, column.Width)); } - if (!hasWrittenStart) + + if (hasWrittenStart) { - return; + writer.Write(WorksheetXml.EndCols); } - writer.Write(WorksheetXml.EndCols); } private void PrintHeader(MiniExcelStreamWriter writer, List props) { - var xIndex = 1; - var yIndex = 1; + const int yIndex = 1; writer.Write(WorksheetXml.StartRow(yIndex)); + var xIndex = 1; foreach (var p in props) { - if (p == null) + //reason : https://github.com/mini-software/MiniExcel/issues/142 + if (p != null) { - xIndex++; //reason : https://github.com/mini-software/MiniExcel/issues/142 - continue; + if (p.ExcelIgnore) + continue; + + var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); + WriteCell(writer, r, columnName: p.ExcelColumnName); } - - var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - WriteCell(writer, r, columnName: p.ExcelColumnName); xIndex++; } diff --git a/src/MiniExcel/OpenXml/ExcelWidthCollection.cs b/src/MiniExcel/OpenXml/ExcelWidthCollection.cs index c8eaca08..494c1ade 100644 --- a/src/MiniExcel/OpenXml/ExcelWidthCollection.cs +++ b/src/MiniExcel/OpenXml/ExcelWidthCollection.cs @@ -10,22 +10,21 @@ public sealed class ExcelColumnWidth public int Index { get; set; } public double Width { get; set; } - internal static IEnumerable FromProps(IEnumerable props, double? minWidth = null) + internal static IEnumerable FromProps(ICollection props, double? minWidth = null) { var i = 1; foreach (var p in props) { - if (p == null || (p.ExcelColumnWidth == null && minWidth == null)) + if (p?.ExcelColumnWidth != null || minWidth != null) { - i++; - continue; + var colIndex = p?.ExcelColumnIndex + 1; + yield return new ExcelColumnWidth + { + Index = colIndex ?? i, + Width = p?.ExcelColumnWidth ?? minWidth.Value + }; } - var colIndex = p.ExcelColumnIndex == null ? i : p.ExcelColumnIndex.GetValueOrDefault() + 1; - yield return new ExcelColumnWidth - { - Index = colIndex, - Width = p.ExcelColumnWidth ?? minWidth.Value, - }; + i++; } } @@ -38,7 +37,7 @@ public sealed class ExcelWidthCollection public IEnumerable Columns => _columnWidths.Values; - internal ExcelWidthCollection(double minWidth, double maxWidth, IEnumerable props) + internal ExcelWidthCollection(double minWidth, double maxWidth, ICollection props) { _maxWidth = maxWidth; _columnWidths = ExcelColumnWidth.FromProps(props, minWidth).ToDictionary(x => x.Index); @@ -46,13 +45,12 @@ internal ExcelWidthCollection(double minWidth, double maxWidth, IEnumerable @@ -61,13 +59,12 @@ public void AdjustWidth(int columnIndex, string columnValue) /// /// Rounds the result to 2 decimal places. /// - public static double GetApproximateRequiredCalibriWidth(int textLength) + public static double GetApproximateTextWidth(int textLength) { - double characterWidthFactor = 1.2; // Estimated factor for Calibri, 11pt - double padding = 2; // Add some padding for extra spacing - - double excelColumnWidth = (textLength * characterWidthFactor) + padding; + const double characterWidthFactor = 1.2; // Estimated factor for Calibri, 11pt + const double padding = 2; // Add some padding for extra spacing + var excelColumnWidth = (textLength * characterWidthFactor) + padding; return Math.Round(excelColumnWidth, 2); } } diff --git a/src/MiniExcel/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs index f8102160..d8eee380 100644 --- a/src/MiniExcel/Utils/CustomPropertyHelper.cs +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -121,7 +121,7 @@ internal static List SortCustomProps(List prop internal static List GetExcelCustomPropertyInfos(Type type, string[] keys, Configuration configuration) { - var flags = BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance; + const BindingFlags flags = BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance; var props = GetExcelPropertyInfo(type, flags, configuration) .Where(prop => prop.Property.Info.GetSetMethod() != null // why not .Property.CanWrite? because it will use private setter && !prop.Property.Info.GetAttributeValue((ExcelIgnoreAttribute x) => x.ExcelIgnore) @@ -154,9 +154,9 @@ internal static string DescriptionAttr(Type type, object source) { var name = source?.ToString(); return type - .GetField(name)? //For some database dirty data, there may be no way to change to the correct enumeration, will return NULL - .GetCustomAttribute(false)? - .Description + .GetField(name) //For some database dirty data, there may be no way to change to the correct enumeration, will return NULL + ?.GetCustomAttribute(false) + ?.Description ?? name; } @@ -243,8 +243,10 @@ internal static ExcellSheetInfo GetExcellSheetInfo(Type type, Configuration conf internal static List GetDictionaryColumnInfo(IDictionary dicString, IDictionary dic, Configuration configuration) { var props = new List(); - var keys = dicString?.Keys.ToList() ?? dic?.Keys - ?? throw new NotSupportedException(); + + var keys = dicString?.Keys.ToList() + ?? dic?.Keys + ?? throw new NotSupportedException(); foreach (var key in keys) { @@ -261,9 +263,7 @@ internal static void SetDictionaryColumnInfo(List props, object ExcelColumnName = key?.ToString() }; - // TODO:Dictionary value type is not fiexed - //var _t = - //var gt = Nullable.GetUnderlyingType(p.PropertyType); + // TODO:Dictionary value type is not fixed var isIgnore = false; if (configuration.DynamicColumns != null && configuration.DynamicColumns.Length > 0) { @@ -328,9 +328,10 @@ internal static List GetColumnInfoFromValue(object value, Confi } } - private static bool ValueIsNeededToDetermineProperties(Type type) => type == typeof(object) - || typeof(IDictionary).IsAssignableFrom(type) - || typeof(IDictionary).IsAssignableFrom(type); + private static bool ValueIsNeededToDetermineProperties(Type type) => + typeof(object) == type || + typeof(IDictionary).IsAssignableFrom(type) || + typeof(IDictionary).IsAssignableFrom(type); internal static ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName, Configuration configuration) { @@ -346,16 +347,20 @@ internal static ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string co var dynamicColumn = configuration.DynamicColumns .SingleOrDefault(col => string.Equals(col.Key, columnName, StringComparison.OrdinalIgnoreCase)); - if (dynamicColumn == null || dynamicColumn.Ignore) + if (dynamicColumn == null) return prop; prop.Nullable = true; prop.ExcelIgnore = dynamicColumn.Ignore; prop.ExcelColumnType = dynamicColumn.Type; - prop.ExcelColumnIndex = dynamicColumn.Index; prop.ExcelColumnWidth = dynamicColumn.Width; prop.CustomFormatter = dynamicColumn.CustomFormatter; + if (dynamicColumn.Index > -1) + { + prop.ExcelColumnIndex = dynamicColumn.Index; + } + if (dynamicColumn.Format != null) { prop.ExcelFormat = dynamicColumn.Format; diff --git a/src/MiniExcel/WriteAdapter/DataReaderWriteAdapter.cs b/src/MiniExcel/WriteAdapter/DataReaderWriteAdapter.cs index 3bf56271..d344161f 100644 --- a/src/MiniExcel/WriteAdapter/DataReaderWriteAdapter.cs +++ b/src/MiniExcel/WriteAdapter/DataReaderWriteAdapter.cs @@ -30,21 +30,8 @@ public List GetColumns() for (var i = 0; i < _reader.FieldCount; i++) { var columnName = _reader.GetName(i); - - if (!_configuration.DynamicColumnFirst) - { - var prop = CustomPropertyHelper.GetColumnInfosFromDynamicConfiguration(columnName, _configuration); - props.Add(prop); - continue; - } - - if (_configuration - .DynamicColumns - .Any(a => string.Equals( - a.Key, - columnName, - StringComparison.OrdinalIgnoreCase))) - + if (!_configuration.DynamicColumnFirst || + _configuration.DynamicColumns.Any(d => string.Equals(d.Key, columnName, StringComparison.OrdinalIgnoreCase))) { var prop = CustomPropertyHelper.GetColumnInfosFromDynamicConfiguration(columnName, _configuration); props.Add(prop); @@ -64,16 +51,17 @@ public IEnumerable> GetRows(List pro private IEnumerable GetRowValues(List props) { - for (int i = 0, column = 1; i < _reader.FieldCount; i++, column++) + var column = 1; + for (int i = 0; i < _reader.FieldCount; i++) { - if (_configuration.DynamicColumnFirst) - { - var columnIndex = _reader.GetOrdinal(props[i].Key.ToString()); - yield return new CellWriteInfo(_reader.GetValue(columnIndex), column, props[i]); - } - else + var prop = props[i]; + if (prop != null && !prop.ExcelIgnore) { - yield return new CellWriteInfo(_reader.GetValue(i), column, props[i]); + var columnIndex = _configuration.DynamicColumnFirst + ? _reader.GetOrdinal(prop.Key.ToString()) : i; + + yield return new CellWriteInfo(_reader.GetValue(columnIndex), column, prop); + column++; } } } diff --git a/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs b/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs index 2b93b2f2..e101fa00 100644 --- a/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs +++ b/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs @@ -154,8 +154,8 @@ private static void AssertExpectedWidth(string path, OpenXmlConfiguration config { var expectedWidth = column.Min.Value switch { - 1 => ExcelWidthCollection.GetApproximateRequiredCalibriWidth(AutoAdjustTestParameters.column1MaxStringLength), - 2 => ExcelWidthCollection.GetApproximateRequiredCalibriWidth(AutoAdjustTestParameters.column2MaxStringLength), + 1 => ExcelWidthCollection.GetApproximateTextWidth(AutoAdjustTestParameters.column1MaxStringLength), + 2 => ExcelWidthCollection.GetApproximateTextWidth(AutoAdjustTestParameters.column2MaxStringLength), 3 => configuration.MinWidth, 4 => configuration.MaxWidth, _ => throw new Exception("Unexpected column"), @@ -198,8 +198,8 @@ public static List> GetDictionaryTestData() => GetTes { EnableAutoWidth = true, FastMode = true, - MinWidth = ExcelWidthCollection.GetApproximateRequiredCalibriWidth(minStringLength), - MaxWidth = ExcelWidthCollection.GetApproximateRequiredCalibriWidth(maxStringLength) + MinWidth = ExcelWidthCollection.GetApproximateTextWidth(minStringLength), + MaxWidth = ExcelWidthCollection.GetApproximateTextWidth(maxStringLength) }; } } \ No newline at end of file diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs index 9ec4499d..50efaf67 100644 --- a/tests/MiniExcelTests/MiniExcelIssueTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs @@ -19,8 +19,6 @@ using Xunit.Abstractions; using static MiniExcelLibs.Tests.MiniExcelOpenXmlTests; using MiniExcelLibs.Picture; -using DocumentFormat.OpenXml.Spreadsheet; -using NPOI.Util; using TableStyles = MiniExcelLibs.OpenXml.TableStyles; namespace MiniExcelLibs.Tests; @@ -107,7 +105,7 @@ public void TestPR10() .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")) .ToArray() }; - var rows = MiniExcel.Query(path.ToString(), configuration: config).ToList(); + var rows = MiniExcel.Query(path, configuration: config).ToList(); } [Fact] @@ -535,11 +533,23 @@ public void TestIssue401(bool autoFilter, int count) var config = new OpenXmlConfiguration { AutoFilter = autoFilter }; using (var connection = Db.GetConnection("Data Source=:memory:")) { - using var command = new SQLiteCommand(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2", connection); connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = + """ + SELECT + 'MiniExcel' as Column1, + 1 as Column2 + + UNION ALL + SELECT 'Github', 2 + """; + using var reader = command.ExecuteReader(); MiniExcel.SaveAs(path.ToString(), reader, configuration: config); } + var xml = Helpers.GetZipFileContent(path.ToString(), "xl/worksheets/sheet1.xml"); var cnt = Regex.Matches(xml, "autoFilter").Count; Assert.Equal(count, cnt); @@ -708,7 +718,7 @@ public async Task TestIssue338() { var path = PathHelper.GetFile("csv/TestIssue338.csv"); var row = (await MiniExcel.QueryAsync(path)).FirstOrDefault(); - Assert.Equal("���IJ�������", row.A); + Assert.Equal("���IJ�������", row!.A); } { var path = PathHelper.GetFile("csv/TestIssue338.csv"); @@ -717,7 +727,7 @@ public async Task TestIssue338() StreamReaderFunc = stream => new StreamReader(stream, Encoding.GetEncoding("gb2312")) }; var row = (await MiniExcel.QueryAsync(path, configuration: config)).FirstOrDefault(); - Assert.Equal("中文测试内容", row.A); + Assert.Equal("中文测试内容", row!.A); } { var path = PathHelper.GetFile("csv/TestIssue338.csv"); @@ -728,7 +738,7 @@ public async Task TestIssue338() await using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) { var row = (await stream.QueryAsync(configuration: config, excelType: ExcelType.CSV)).FirstOrDefault(); - Assert.Equal("中文测试内容", row.A); + Assert.Equal("中文测试内容", row!.A); } } } @@ -1723,7 +1733,9 @@ private class TestIssue280Dto public void TestIssue279() { var path = PathHelper.GetFile("/csv/TestHeader.csv"); +#pragma warning disable CS0618 // Type or member is obsolete using var dt = MiniExcel.QueryAsDataTable(path, true, null, ExcelType.CSV); +#pragma warning restore CS0618 Assert.Equal("A1", dt.Rows[0]["Column1"]); Assert.Equal("A2", dt.Rows[1]["Column1"]); Assert.Equal("B1", dt.Rows[0]["Column2"]); @@ -1756,7 +1768,7 @@ public void TestIssue267() { var path = PathHelper.GetFile("/xlsx/TestIssue267.xlsx"); var row = MiniExcel.Query(path).SingleOrDefault(); - Assert.Equal(10618, row.A); + Assert.Equal(10618, row!.A); Assert.Equal("2021-02-23", row.B); Assert.Equal(43.199999999999996, row.C); Assert.Equal(1.2, row.D); @@ -2149,14 +2161,14 @@ public void Issue235() new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } })); - users.TableName = "users"; + users!.TableName = "users"; var department = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } })); - department.TableName = "department"; + department!.TableName = "department"; dataSet.Tables.Add(users); dataSet.Tables.Add(department); @@ -2192,7 +2204,9 @@ public void Issue235() public void Issue233() { var path = PathHelper.GetFile("xlsx/TestIssue233.xlsx"); +#pragma warning disable CS0618 // Type or member is obsolete using var dt = MiniExcel.QueryAsDataTable(path); +#pragma warning restore CS0618 var rows = dt.Rows; Assert.Equal(0.55, rows[0]["Size"]); @@ -2328,7 +2342,9 @@ public void Issue230() public void Issue229() { var path = PathHelper.GetFile("xlsx/TestIssue229.xlsx"); +#pragma warning disable CS0618 // Type or member is obsolete using var dt = MiniExcel.QueryAsDataTable(path); +#pragma warning restore CS0618 foreach (DataColumn column in dt.Columns) { var v = dt.Rows[3][column]; @@ -2448,7 +2464,9 @@ public void Issue223() using var path = AutoDeletingPath.Create(); MiniExcel.SaveAs(path.ToString(), value); +#pragma warning disable CS0618 // Type or member is obsolete using var dt = MiniExcel.QueryAsDataTable(path.ToString()); +#pragma warning restore CS0618 var columns = dt.Columns; Assert.Equal(typeof(object), columns[0].DataType); Assert.Equal(typeof(object), columns[1].DataType); @@ -2554,7 +2572,9 @@ public void Issue216() MiniExcel.SaveAs(path.ToString(), value); { +#pragma warning disable CS0618 // Type or member is obsolete using var table = MiniExcel.QueryAsDataTable(path.ToString()); +#pragma warning restore CS0618 Assert.Equal("Test1", table.Columns[0].ColumnName); Assert.Equal("Test2", table.Columns[1].ColumnName); Assert.Equal("1", table.Rows[0]["Test1"]); @@ -2564,7 +2584,9 @@ public void Issue216() } { +#pragma warning disable CS0618 // Type or member is obsolete using var dt = MiniExcel.QueryAsDataTable(path.ToString(), false); +#pragma warning restore CS0618 Assert.Equal("Test1", dt.Rows[0]["A"]); Assert.Equal("Test2", dt.Rows[0]["B"]); Assert.Equal("1", dt.Rows[1]["A"]); @@ -2851,7 +2873,7 @@ public void Issue87() using var path = AutoDeletingPath.Create(); var value = new { - Tests = Enumerable.Range(1, 5).Select((s, i) => new { test1 = i, test2 = i }) + Tests = Enumerable.Range(1, 5).Select((_, i) => new { test1 = i, test2 = i }) }; var rows = MiniExcel.Query(templatePath).ToList(); @@ -2900,11 +2922,10 @@ public void Issue206() using var path = AutoDeletingPath.Create(); using var dt = new DataTable(); - { - dt.Columns.Add("name"); - dt.Columns.Add("department"); - dt.Rows.Add("Jack", "HR"); - } + dt.Columns.Add("name"); + dt.Columns.Add("department"); + dt.Rows.Add("Jack", "HR"); + var value = new Dictionary { ["employees"] = dt }; MiniExcel.SaveAsByTemplate(path.ToString(), templatePath, value); @@ -3659,7 +3680,43 @@ public void Issue527() Assert.Equal("General User", rows[1].B); Assert.Equal("General Administrator", rows[2].B); } - + + [Fact] + public void TestIssue584() + { + var excelconfig = new OpenXmlConfiguration + { + FastMode = true, + DynamicColumns = + [ + new DynamicExcelColumn("Id") { Ignore = true } + ] + }; + + using var conn = Db.GetConnection(); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = + """ + WITH test('Id', 'Name') AS ( + VALUES + (1, 'test1'), + (2, 'test2'), + (3, 'test3') + ) + SELECT * FROM test; + """; + using var reader = cmd.ExecuteReader(); + + using var path = AutoDeletingPath.Create(); + MiniExcel.SaveAs(path.FilePath, reader, configuration: excelconfig, overwriteFile: true); + + var rows = MiniExcel.Query(path.FilePath).ToList(); + Assert.All(rows, x => Assert.Single(x)); + Assert.Equal("Name", rows[0].A); + } + private class Issue585VO1 { public string Col1 { get; set; }