diff --git a/samples/xlsx/Issue527Template.xlsx b/samples/xlsx/Issue527Template.xlsx new file mode 100644 index 00000000..6e9e9c79 Binary files /dev/null and b/samples/xlsx/Issue527Template.xlsx differ diff --git a/src/MiniExcel/Csv/CsvReader.cs b/src/MiniExcel/Csv/CsvReader.cs index c9d9f3f7..171a10b7 100644 --- a/src/MiniExcel/Csv/CsvReader.cs +++ b/src/MiniExcel/Csv/CsvReader.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MiniExcelLibs.Exceptions; namespace MiniExcelLibs.Csv { @@ -14,101 +15,100 @@ internal class CsvReader : IExcelReader { private Stream _stream; private CsvConfiguration _config; + public CsvReader(Stream stream, IConfiguration configuration) { - this._stream = stream; - this._config = configuration == null ? CsvConfiguration.DefaultConfiguration : (CsvConfiguration)configuration; + _stream = stream; + _config = configuration == null ? CsvConfiguration.DefaultConfiguration : (CsvConfiguration)configuration; } public IEnumerable> Query(bool useHeaderRow, string sheetName, string startCell) { if (startCell != "A1") - throw new NotImplementedException("CSV not Implement startCell"); + throw new NotImplementedException("CSV does not implement parameter startCell"); + if (_stream.CanSeek) _stream.Position = 0; + var reader = _config.StreamReaderFunc(_stream); + var firstRow = true; + var headRows = new Dictionary(); + + string row; + for (var rowIndex = 1; (row = reader.ReadLine()) != null; rowIndex++) { - string[] read; - var firstRow = true; - Dictionary headRows = new Dictionary(); - string row; - for (var rowIndex = 1; (row = reader.ReadLine()) != null; rowIndex++) + string finalRow = row; + if (_config.ReadLineBreaksWithinQuotes) { - - string finalRow = row; - if (_config.ReadLineBreaksWithinQuotes) + while (finalRow.Count(c => c == '"') % 2 != 0) { - while (finalRow.Count(c => c == '"') % 2 != 0) + var nextPart = reader.ReadLine(); + if (nextPart == null) { - var nextPart = reader.ReadLine(); - if (nextPart == null) - { - break; - } - finalRow = string.Concat(finalRow, _config.NewLine, nextPart); + break; } + finalRow = string.Concat(finalRow, _config.NewLine, nextPart); } - read = Split(finalRow); + } + var read = Split(finalRow); - // invalid row check - if (read.Length < headRows.Count) - { - var colIndex = read.Length; - var headers = headRows.ToDictionary(x => x.Value, x => x.Key); - var rowValues = read.Select((x, i) => new { Key = headRows[i], Value = x }).ToDictionary(x => x.Key, x => (object)x.Value); - throw new Exceptions.ExcelColumnNotFoundException(null, - headRows[colIndex], null, rowIndex, headers, rowValues, $"Csv read error, Column: {colIndex} not found in Row: {rowIndex}"); - } + // invalid row check + if (read.Length < headRows.Count) + { + var colIndex = read.Length; + var headers = headRows.ToDictionary(x => x.Value, x => x.Key); + var rowValues = read + .Select((x, i) => new KeyValuePair(headRows[i], x)) + .ToDictionary(x => x.Key, x => x.Value); + + throw new ExcelColumnNotFoundException(columnIndex: null, headRows[colIndex], null, rowIndex, headers, rowValues, $"Csv read error: Column {colIndex} not found in Row {rowIndex}"); + } - //header - if (useHeaderRow) + //header + if (useHeaderRow) + { + if (firstRow) { - if (firstRow) - { - firstRow = false; - for (int i = 0; i <= read.Length - 1; i++) - headRows.Add(i, read[i]); - continue; - } - - var cell = CustomPropertyHelper.GetEmptyExpandoObject(headRows); + firstRow = false; for (int i = 0; i <= read.Length - 1; i++) - cell[headRows[i]] = read[i]; - - yield return cell; + headRows.Add(i, read[i]); continue; } + var headCell = CustomPropertyHelper.GetEmptyExpandoObject(headRows); + for (int i = 0; i <= read.Length - 1; i++) + headCell[headRows[i]] = read[i]; - //body - { - // record first row as reference - if (firstRow) - { - firstRow = false; - for (int i = 0; i <= read.Length - 1; i++) - headRows.Add(i, $"c{i + 1}"); - } + yield return headCell; + continue; + } - var cell = CustomPropertyHelper.GetEmptyExpandoObject(read.Length - 1, 0); - if (_config.ReadEmptyStringAsNull) - { - for (int i = 0; i <= read.Length - 1; i++) - cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]?.Length == 0 ? null : read[i]; - } - else - { - for (int i = 0; i <= read.Length - 1; i++) - cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]; - } + //body + if (firstRow) // record first row as reference + { + firstRow = false; + for (int i = 0; i <= read.Length - 1; i++) + headRows.Add(i, $"c{i + 1}"); + } - yield return cell; - } + var cell = CustomPropertyHelper.GetEmptyExpandoObject(read.Length - 1, 0); + if (_config.ReadEmptyStringAsNull) + { + for (int i = 0; i <= read.Length - 1; i++) + cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]?.Length == 0 ? null : read[i]; } + else + { + for (int i = 0; i <= read.Length - 1; i++) + cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]; + } + + yield return cell; } } public IEnumerable Query(string sheetName, string startCell, bool hasHeader) where T : class, new() { - return ExcelOpenXmlSheetReader.QueryImpl(Query(false, sheetName, startCell), startCell, hasHeader, _config); + var dynamicRecords = Query(false, sheetName, startCell); + return ExcelOpenXmlSheetReader.QueryImpl(dynamicRecords, startCell, hasHeader, _config); } private string[] Split(string row) @@ -119,9 +119,10 @@ private string[] Split(string row) } else { - return Regex.Split(row, $"[\t{_config.Seperator}](?=(?:[^\"]|\"[^\"]*\")*$)") - .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")).ToArray(); //this code from S.O : https://stackoverflow.com/a/11365961/9131476 + return Regex.Split(row, $"[\t{_config.Seperator}](?=(?:[^\"]|\"[^\"]*\")*$)") + .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")) + .ToArray(); } } @@ -143,56 +144,55 @@ public void Dispose() public IEnumerable> QueryRange(bool useHeaderRow, string sheetName, string startCell, string endCell) { if (startCell != "A1") - throw new NotImplementedException("CSV not Implement startCell"); + throw new NotImplementedException("CSV does not implement parameter startCell"); + if (_stream.CanSeek) _stream.Position = 0; + var reader = _config.StreamReaderFunc(_stream); + + string row; + var firstRow = true; + var headRows = new Dictionary(); + + while ((row = reader.ReadLine()) != null) { - var row = string.Empty; - string[] read; - var firstRow = true; - Dictionary headRows = new Dictionary(); - while ((row = reader.ReadLine()) != null) - { - read = Split(row); + var read = Split(row); - //header - if (useHeaderRow) + //header + if (useHeaderRow) + { + if (firstRow) { - if (firstRow) - { - firstRow = false; - for (int i = 0; i <= read.Length - 1; i++) - headRows.Add(i, read[i]); - continue; - } - - var cell = CustomPropertyHelper.GetEmptyExpandoObject(headRows); + firstRow = false; for (int i = 0; i <= read.Length - 1; i++) - cell[headRows[i]] = read[i]; - - yield return cell; + headRows.Add(i, read[i]); continue; } + var headCell = CustomPropertyHelper.GetEmptyExpandoObject(headRows); + for (int i = 0; i <= read.Length - 1; i++) + headCell[headRows[i]] = read[i]; - //body - { - var cell = CustomPropertyHelper.GetEmptyExpandoObject(read.Length - 1, 0); - for (int i = 0; i <= read.Length - 1; i++) - cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]; - yield return cell; - } + yield return headCell; + continue; } + + //body + var cell = CustomPropertyHelper.GetEmptyExpandoObject(read.Length - 1, 0); + for (int i = 0; i <= read.Length - 1; i++) + cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]; + + yield return cell; } } public IEnumerable QueryRange(string sheetName, string startCell, string endCel) where T : class, new() { return ExcelOpenXmlSheetReader.QueryImplRange(QueryRange(false, sheetName, startCell, endCel), startCell, endCel, this._config); } - public Task>> QueryAsyncRange(bool UseHeaderRow, string sheetName, string startCell, string endCel, CancellationToken cancellationToken = default) + public Task>> QueryAsyncRange(bool useHeaderRow, string sheetName, string startCell, string endCel, CancellationToken cancellationToken = default) { - return Task.Run(() => QueryRange(UseHeaderRow, sheetName, startCell, endCel), cancellationToken); + return Task.Run(() => QueryRange(useHeaderRow, sheetName, startCell, endCel), cancellationToken); } public Task> QueryAsyncRange(string sheetName, string startCell, string endCel, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new() diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs index b940dc2b..aee62229 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs @@ -448,6 +448,7 @@ private static void SetCellsValueAndHeaders(object cellValue, bool useHeaderRow, if (hasHeader) continue; } + var v = new T(); foreach (var pInfo in props) { @@ -455,38 +456,34 @@ private static void SetCellsValueAndHeaders(object cellValue, bool useHeaderRow, { foreach (var alias in pInfo.ExcelColumnAliases) { - if (!headersDic.TryGetValue(alias, out var columnId)) - continue; - - object newV = null; - var columnName = keys[columnId]; - item.TryGetValue(columnName, out var itemValue); - - if (itemValue == null) - continue; + if (headersDic.TryGetValue(alias, out var columnId)) + { + var columnName = keys[columnId]; + item.TryGetValue(columnName, out var aliasItemValue); - newV = TypeHelper.TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell, configuration); + if (aliasItemValue != null) + { + var newAliasValue = TypeHelper.TypeMapping(v, pInfo, aliasItemValue, rowIndex, startCell, configuration); + } + } } } //Q: Why need to check every time? A: it needs to check everytime, because it's dictionary + object itemValue = null; + if (pInfo.ExcelIndexName != null && keys.Contains(pInfo.ExcelIndexName)) { - object newV = null; - object itemValue = null; - if (pInfo.ExcelIndexName != null && keys.Contains(pInfo.ExcelIndexName)) - { - item.TryGetValue(pInfo.ExcelIndexName, out itemValue); - } - else if (headersDic.TryGetValue(pInfo.ExcelColumnName, out var columnId)) - { - var columnName = keys[columnId]; - item.TryGetValue(columnName, out itemValue); - } + item.TryGetValue(pInfo.ExcelIndexName, out itemValue); + } + else if (headersDic.TryGetValue(pInfo.ExcelColumnName, out var columnId)) + { + var columnName = keys[columnId]; + item.TryGetValue(columnName, out itemValue); + } - if (itemValue == null) - continue; - - newV = TypeHelper.TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell, configuration); + if (itemValue != null) + { + var newValue = TypeHelper.TypeMapping(v, pInfo, itemValue, rowIndex, startCell, configuration); } } rowIndex++; @@ -1317,6 +1314,7 @@ public IEnumerable> QueryRange(bool useHeaderRow, st first = false; continue; } + var v = new T(); foreach (var pInfo in props) { @@ -1326,32 +1324,33 @@ public IEnumerable> QueryRange(bool useHeaderRow, st { if (headersDic.TryGetValue(alias, out var value)) { - object newV = null; - object itemValue = item[keys[value]]; - - if (itemValue == null) - continue; - - newV = TypeHelper.TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell, configuration); + var columnName = keys[value]; + item.TryGetValue(columnName, out var aliasItemValue); + if (aliasItemValue != null) + { + object newAliasValue = TypeHelper.TypeMapping(v, pInfo, aliasItemValue, rowIndex, startCell, configuration); + } } } } //Q: Why need to check every time? A: it needs to check everytime, because it's dictionary + object itemValue = null; + if (pInfo.ExcelIndexName != null && keys.Contains(pInfo.ExcelIndexName)) { - object newV = null; - object itemValue = null; - if (pInfo.ExcelIndexName != null && keys.Contains(pInfo.ExcelIndexName)) - itemValue = item[pInfo.ExcelIndexName]; - else if (headersDic.TryGetValue(pInfo.ExcelColumnName, out var value)) - itemValue = item[keys[value]]; - - if (itemValue == null) - continue; + itemValue = item[pInfo.ExcelIndexName]; + } + else if (headersDic.TryGetValue(pInfo.ExcelColumnName, out var value)) + { + itemValue = item[keys[value]]; + } - newV = TypeHelper.TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell, configuration); + if (itemValue != null) + { + object newValue = TypeHelper.TypeMapping(v, pInfo, itemValue, rowIndex, startCell, configuration); } } + rowIndex++; yield return v; } diff --git a/src/MiniExcel/Reflection/Property.cs b/src/MiniExcel/Reflection/Property.cs index f670a5c7..2156402e 100644 --- a/src/MiniExcel/Reflection/Property.cs +++ b/src/MiniExcel/Reflection/Property.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Concurrent; using System.Linq; @@ -6,63 +5,53 @@ namespace MiniExcelLibs { - public abstract class Member - { - } + public abstract class Member { } public class Property : Member { - private static readonly ConcurrentDictionary m_cached = new ConcurrentDictionary(); - - private readonly MemberGetter m_geter; + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); - private readonly MemberSetter m_seter; + private readonly MemberGetter _getter; + private readonly MemberSetter _setter; public Property(PropertyInfo property) { Name = property.Name; Info = property; - if (property.CanRead == true) + if (property.CanRead) { CanRead = true; - m_geter = new MemberGetter(property); + _getter = new MemberGetter(property); } - if (property.CanWrite == true) + if (property.CanWrite) { CanWrite = true; - m_seter = new MemberSetter(property); + _setter = new MemberSetter(property); } } + public string Name { get; protected set; } public bool CanRead { get; private set; } - public bool CanWrite { get; private set; } public PropertyInfo Info { get; private set; } - - public string Name { get; protected set; } - + public static Property[] GetProperties(Type type) { - return m_cached.GetOrAdd(type, t => t.GetProperties().Select(p => new Property(p)).ToArray()); + return Cache.GetOrAdd(type, t => + t.GetProperties().Select(p => new Property(p)).ToArray()); } - public object GetValue(object instance) - { - if (m_geter == null) - { - throw new NotSupportedException(); - } - return m_geter.Invoke(instance); - } + public object GetValue(object instance) => _getter != null + ? _getter.Invoke(instance) + : throw new NotSupportedException(); public void SetValue(object instance, object value) { - if (m_seter == null) - { + if (_setter == null) throw new NotSupportedException($"{Name} can't set value"); - } - m_seter.Invoke(instance, value); + + _setter.Invoke(instance, value); } } } \ No newline at end of file diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs index 9695f508..3db52612 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Globalization; using System.IO; @@ -387,15 +388,13 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she { isHeaderRow = true; } - else if (row.InnerText.Contains("@merge") && mergeCells) + else if (mergeCells) { - mergeRowCount++; - continue; - } - else if (row.InnerText.Contains("@endmerge") && mergeCells) - { - mergeRowCount++; - continue; + if (row.InnerText.Contains("@merge") || row.InnerText.Contains("@endmerge")) + { + mergeRowCount++; + continue; + } } if (groupingStarted && !isCellIEnumerableValuesSet) @@ -537,14 +536,14 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r var cleanInnerXml = CleanXml(innerXml, endPrefix); // https://github.com/mini-software/MiniExcel/issues/771 Saving by template introduces unintended value replication in each row #771 - var notFirstRowInnerXmlElement = rowElement.Clone(); - foreach (XmlElement c in notFirstRowInnerXmlElement.SelectNodes("x:c", _ns)) + var notFirstRowElement = rowElement.Clone(); + foreach (XmlElement c in notFirstRowElement.SelectNodes("x:c", _ns)) { var v = c.SelectSingleNode("x:v", _ns); if (v != null && !_nonTemplateRegex.IsMatch(v.InnerText)) v.InnerText = string.Empty; } - var cleanNotFirstRowInnerXmlElement = CleanXml(notFirstRowInnerXmlElement.InnerXml, endPrefix); + var cleanNotFirstRowInnerXml = CleanXml(notFirstRowElement.InnerXml, endPrefix); foreach (var item in rowInfo.CellIEnumerableValues) { @@ -676,6 +675,11 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r { cellValueStr = ConvertToDateTimeString(propInfo, cellValue); } + else if (type?.IsEnum ?? false) + { + var description = CustomPropertyHelper.DescriptionAttr(type, cellValue); + cellValueStr = ExcelOpenXmlUtils.EncodeXML(description); + } else { cellValueStr = ExcelOpenXmlUtils.EncodeXML(cellValue?.ToString()); @@ -722,7 +726,7 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r if (isFirst) { // https://github.com/mini-software/MiniExcel/issues/771 Saving by template introduces unintended value replication in each row #771 - cleanInnerXml = cleanNotFirstRowInnerXmlElement; + cleanInnerXml = cleanNotFirstRowInnerXml; isFirst = false; } @@ -998,6 +1002,8 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary inputMaps } else { + // ==== add dimension element if not found ==== + var firstRow = rows[0].SelectNodes("x:c", _ns); var lastRow = rows[rows.Count - 1].SelectNodes("x:c", _ns); @@ -1116,12 +1122,13 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary inputMaps { if (!values.ContainsKey(f.Name)) { - values.Add(f.Name, new PropInfo + var propInfo = new PropInfo { FieldInfo = f, PropertyInfoOrFieldInfo = PropertyInfoOrFieldInfo.FieldInfo, UnderlyingTypePropType = Nullable.GetUnderlyingType(f.FieldType) ?? f.FieldType - }); + }; + values.Add(f.Name, propInfo); } } @@ -1160,7 +1167,7 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary inputMaps { c.SetAttribute("t", "str"); } - else if (TypeHelper.IsNumericType(type)) + else if (TypeHelper.IsNumericType(type) && !type.IsEnum) { c.SetAttribute("t", "n"); } @@ -1212,7 +1219,7 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary inputMaps { c.SetAttribute("t", "str"); } - else if (TypeHelper.IsNumericType(type)) + else if (TypeHelper.IsNumericType(type) && !type.IsEnum) { c.SetAttribute("t", "n"); } @@ -1227,7 +1234,7 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary inputMaps } else { - var cellValueStr = cellValue?.ToString(); // value did encodexml, so don't duplicate encode value https://gitee.com/dotnetchina/MiniExcel/issues/I4DQUN + var cellValueStr = cellValue?.ToString(); // value did encodexml, so don't duplicate encode value (https://gitee.com/dotnetchina/MiniExcel/issues/I4DQUN) if (isMultiMatch || cellValue is string) // if matchs count over 1 need to set type=str (https://user-images.githubusercontent.com/12729184/114530109-39d46d00-9c7d-11eb-8f6b-52ad8600aca3.png) { c.SetAttribute("t", "str"); @@ -1250,7 +1257,6 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary inputMaps v.InnerText = v.InnerText.Replace($"{{{{{propNames[0]}}}}}", cellValueStr); //TODO: auto check type and set value } - } //if (xRowInfo.CellIEnumerableValues != null) //2. From left to right, only the first set is used as the basis for the list // break; diff --git a/src/MiniExcel/Utils/AttributeExtension.cs b/src/MiniExcel/Utils/AttributeExtension.cs index dd45f0c4..4ee9ccf2 100644 --- a/src/MiniExcel/Utils/AttributeExtension.cs +++ b/src/MiniExcel/Utils/AttributeExtension.cs @@ -1,48 +1,42 @@ -namespace MiniExcelLibs.Utils -{ - using System; - using System.Linq; - using System.Reflection; +using System; +using System.Linq; +using System.Reflection; +namespace MiniExcelLibs.Utils +{ internal static class AttributeExtension { - internal static TValue GetAttributeValue( - this Type attrType, + this Type targetType, Func selector) where TAttribute : Attribute { - var attr = attrType.GetCustomAttributes(typeof(TAttribute), true).FirstOrDefault() as TAttribute; - return GetValueOrDefault(selector, attr); + var attributeType = targetType + .GetCustomAttributes(typeof(TAttribute), true) + .FirstOrDefault() as TAttribute; + + return GetValueOrDefault(selector, attributeType); } - private static TValue GetValueOrDefault - (Func selector, TAttribute attr) - where TAttribute : Attribute + private static TValue GetValueOrDefault( + Func selector, + TAttribute attr) where TAttribute : Attribute { - if (attr != null) - { - return selector(attr); - } - - return default(TValue); + return attr != null ? selector(attr) : default; } + internal static TAttribute GetAttribute( - this PropertyInfo prop, - bool isInherit = true - ) - where TAttribute : Attribute + this MemberInfo prop, + bool isInherit = true) where TAttribute : Attribute { return GetAttributeValue(prop, (TAttribute attr) => attr, isInherit); } internal static TValue GetAttributeValue( - this PropertyInfo prop, + this MemberInfo prop, Func selector, - bool isInherit = true - ) - where TAttribute : Attribute + bool isInherit = true ) where TAttribute : Attribute { - TAttribute attr = Attribute.GetCustomAttribute(prop, typeof(TAttribute), isInherit) as TAttribute; + var attr = Attribute.GetCustomAttribute(prop, typeof(TAttribute), isInherit) as TAttribute; return GetValueOrDefault(selector, attr); } @@ -51,5 +45,4 @@ internal static bool IsUseAttribute(this PropertyInfo prop) return Attribute.GetCustomAttribute(prop, typeof(TAttribute)) != null; } } - } diff --git a/src/MiniExcel/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs index d934285d..f8102160 100644 --- a/src/MiniExcel/Utils/CustomPropertyHelper.cs +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -152,13 +152,12 @@ internal static List GetExcelCustomPropertyInfos(Type type, str internal static string DescriptionAttr(Type type, object source) { - var fi = type.GetField(source.ToString()); - //For some database dirty data, there may be no way to change to the correct enumeration, will return NULL - if (fi == null) - return source.ToString(); - - var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); - return attributes.Length > 0 ? attributes[0].Description : source.ToString(); + 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 + ?? name; } private static IEnumerable ConvertToExcelCustomPropertyInfo(PropertyInfo[] props, Configuration configuration) diff --git a/src/MiniExcel/Utils/TypeHelper.cs b/src/MiniExcel/Utils/TypeHelper.cs index 259553ac..0873c9a5 100644 --- a/src/MiniExcel/Utils/TypeHelper.cs +++ b/src/MiniExcel/Utils/TypeHelper.cs @@ -36,6 +36,7 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals { if (isNullableUnderlyingType) type = Nullable.GetUnderlyingType(type) ?? type; + switch (Type.GetTypeCode(type)) { //case TypeCode.Byte: @@ -55,11 +56,11 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals } } - public static object TypeMapping(T v, ExcelColumnInfo pInfo, object newValue, object itemValue, int rowIndex, string startCell, Configuration _config) where T : class, new() + public static object TypeMapping(T v, ExcelColumnInfo pInfo, object itemValue, int rowIndex, string startCell, Configuration config) where T : class, new() { try { - return TypeMappingImpl(v, pInfo, ref newValue, itemValue, _config); + return TypeMappingImpl(v, pInfo, itemValue, config); } catch (Exception ex) when (ex is InvalidCastException || ex is FormatException) { @@ -72,30 +73,34 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals } } - private static object TypeMappingImpl(T v, ExcelColumnInfo pInfo, ref object newValue, object itemValue, Configuration _config) where T : class, new() + private static object TypeMappingImpl(T v, ExcelColumnInfo pInfo, object itemValue, Configuration config) where T : class, new() { + object newValue = null; if (pInfo.Nullable && string.IsNullOrWhiteSpace(itemValue?.ToString())) { - newValue = null; } else if (pInfo.ExcludeNullableType == typeof(Guid)) { - newValue = Guid.Parse(itemValue.ToString() ?? Guid.Empty.ToString()); + newValue = Guid.Parse(itemValue?.ToString() ?? Guid.Empty.ToString()); } else if (pInfo.ExcludeNullableType == typeof(DateTimeOffset)) { var vs = itemValue?.ToString(); if (pInfo.ExcelFormat != null) { - if (DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v)) + if (DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { - newValue = _v; + newValue = value; } } - else if (DateTimeOffset.TryParse(vs, _config.Culture, DateTimeStyles.None, out var _v)) - newValue = _v; + else if (DateTimeOffset.TryParse(vs, config.Culture, DateTimeStyles.None, out var value)) + { + newValue = value; + } else + { throw new InvalidCastException($"{vs} cannot be cast to DateTime"); + } } else if (pInfo.ExcludeNullableType == typeof(DateTime)) { @@ -110,21 +115,21 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals var vs = itemValue?.ToString(); if (pInfo.ExcelFormat != null) { - if (pInfo.Property.Info.PropertyType == typeof(DateTimeOffset) && DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v2)) + if (pInfo.Property.Info.PropertyType == typeof(DateTimeOffset) && DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var offsetValue)) { - newValue = _v2; + newValue = offsetValue; } - else if (DateTime.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v)) + else if (DateTime.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { - newValue = _v; + newValue = value; } } - else if (DateTime.TryParse(vs, _config.Culture, DateTimeStyles.None, out var _v)) - newValue = _v; - else if (DateTime.TryParseExact(vs, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v2)) - newValue = _v2; - else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var _d)) - newValue = DateTime.FromOADate(_d); + else if (DateTime.TryParse(vs, config.Culture, DateTimeStyles.None, out var dtValue)) + newValue = dtValue; + else if (DateTime.TryParseExact(vs, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dtExactValue)) + newValue = dtExactValue; + else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var doubleValue)) + newValue = DateTime.FromOADate(doubleValue); else throw new InvalidCastException($"{vs} cannot be cast to DateTime"); } @@ -146,7 +151,7 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals newValue = _v; } } - else if (DateOnly.TryParse(vs, _config.Culture, DateTimeStyles.None, out var _v)) + else if (DateOnly.TryParse(vs, config.Culture, DateTimeStyles.None, out var _v)) newValue = _v; else if (DateOnly.TryParseExact(vs, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v2)) newValue = _v2; @@ -168,24 +173,24 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals var vs = itemValue?.ToString(); if (pInfo.ExcelFormat != null) { - if (TimeSpan.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, out var _v)) + if (TimeSpan.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, out var value)) { - newValue = _v; + newValue = value; } } - else if (TimeSpan.TryParse(vs, _config.Culture, out var _v)) - newValue = _v; - else if (TimeSpan.TryParseExact(vs, "hh\\:mm\\:ss\\.fff", CultureInfo.InvariantCulture, out var _v2)) - newValue = _v2; - else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var _d)) - newValue = TimeSpan.FromMilliseconds(_d); + else if (TimeSpan.TryParse(vs, config.Culture, out var tsValue)) + newValue = tsValue; + else if (TimeSpan.TryParseExact(vs, @"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture, out var tsExactValue)) + newValue = tsExactValue; + else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var msValue)) + newValue = TimeSpan.FromMilliseconds(msValue); else throw new InvalidCastException($"{vs} cannot be cast to TimeSpan"); } else if (pInfo.ExcludeNullableType == typeof(double)) // && (!Regex.IsMatch(itemValue.ToString(), @"^-?\d+(\.\d+)?([eE][-+]?\d+)?$") || itemValue.ToString().Trim().Equals("NaN"))) { var invariantString = Convert.ToString(itemValue, CultureInfo.InvariantCulture); - newValue = double.TryParse(invariantString, NumberStyles.Any, CultureInfo.InvariantCulture, out var _v2) ? _v2 : double.NaN; + newValue = double.TryParse(invariantString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : double.NaN; } else if (pInfo.ExcludeNullableType == typeof(bool)) { @@ -204,10 +209,8 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals else if (pInfo.ExcludeNullableType.IsEnum) { var fieldInfo = pInfo.ExcludeNullableType.GetFields().FirstOrDefault(e => e.GetCustomAttribute(false)?.Description == itemValue?.ToString()); - if (fieldInfo != null) - newValue = Enum.Parse(pInfo.ExcludeNullableType, fieldInfo.Name, true); - else - newValue = Enum.Parse(pInfo.ExcludeNullableType, itemValue?.ToString(), true); + var value = fieldInfo?.Name ?? itemValue.ToString(); + newValue = Enum.Parse(pInfo.ExcludeNullableType, value, true); } else if (pInfo.ExcludeNullableType == typeof(Uri)) { @@ -219,7 +222,7 @@ public static bool IsNumericType(Type type, bool isNullableUnderlyingType = fals else { // Use pInfo.ExcludeNullableType to resolve : https://github.com/mini-software/MiniExcel/issues/138 - newValue = Convert.ChangeType(itemValue, pInfo.ExcludeNullableType, _config.Culture); + newValue = Convert.ChangeType(itemValue, pInfo.ExcludeNullableType, config.Culture); } pInfo.Property.SetValue(v, newValue); diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs index e0eea3ac..9ec4499d 100644 --- a/tests/MiniExcelTests/MiniExcelIssueTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs @@ -114,28 +114,28 @@ public void TestPR10() public void TestIssue289() { using var path = AutoDeletingPath.Create(); - Issue289Dto[] value = + DescriptionEnumDto[] value = [ - new() { Name="0001", UserType=Issue289Type.V1 }, - new() { Name="0002", UserType=Issue289Type.V2 }, - new() { Name="0003", UserType=Issue289Type.V3 } + new() { Name="0001", UserType=DescriptionEnum.V1 }, + new() { Name="0002", UserType=DescriptionEnum.V2 }, + new() { Name="0003", UserType=DescriptionEnum.V3 } ]; MiniExcel.SaveAs(path.ToString(), value); - var rows = MiniExcel.Query(path.ToString()).ToList(); + var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal(Issue289Type.V1, rows[0].UserType); - Assert.Equal(Issue289Type.V2, rows[1].UserType); - Assert.Equal(Issue289Type.V3, rows[2].UserType); + Assert.Equal(DescriptionEnum.V1, rows[0].UserType); + Assert.Equal(DescriptionEnum.V2, rows[1].UserType); + Assert.Equal(DescriptionEnum.V3, rows[2].UserType); } - private class Issue289Dto + private class DescriptionEnumDto { public string Name { get; set; } - public Issue289Type UserType { get; set; } + public DescriptionEnum UserType { get; set; } } - private enum Issue289Type + private enum DescriptionEnum { [Description("General User")] V1, [Description("General Administrator")] V2, @@ -3640,6 +3640,26 @@ public void Issue459() ms.SaveAsByTemplate(template, values); } + [Fact] + public void Issue527() + { + List row = + [ + new() { Name = "Bill", UserType = DescriptionEnum.V1 }, + new() { Name = "Bob", UserType = DescriptionEnum.V2 } + ]; + + var value = new { t = row }; + var template = PathHelper.GetFile("xlsx/Issue527Template.xlsx"); + + using var path = AutoDeletingPath.Create(); + MiniExcel.SaveAsByTemplate(path.FilePath, template, value); + + var rows = MiniExcel.Query(path.FilePath).ToList(); + Assert.Equal("General User", rows[1].B); + Assert.Equal("General Administrator", rows[2].B); + } + private class Issue585VO1 { public string Col1 { get; set; }