diff --git a/src/MiniExcel/Attributes/ExcelColumnIndexAttribute.cs b/src/MiniExcel/Attributes/ExcelColumnIndexAttribute.cs index db05785..cfc711c 100644 --- a/src/MiniExcel/Attributes/ExcelColumnIndexAttribute.cs +++ b/src/MiniExcel/Attributes/ExcelColumnIndexAttribute.cs @@ -7,7 +7,8 @@ public class ExcelColumnIndexAttribute : Attribute { public int ExcelColumnIndex { get; set; } - public ExcelColumnIndexAttribute(string columnName) => Init(Helpers.GetColumnIndex(columnName)); + public ExcelColumnIndexAttribute(string columnName) => Init(ColumnHelper + .GetColumnIndex(columnName)); public ExcelColumnIndexAttribute(int columnIndex) => Init(columnIndex); private void Init(int columnIndex) diff --git a/src/MiniExcel/Csv/CsvReader.cs b/src/MiniExcel/Csv/CsvReader.cs index 002211f..d500d72 100644 --- a/src/MiniExcel/Csv/CsvReader.cs +++ b/src/MiniExcel/Csv/CsvReader.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using static MiniExcelLibs.Utils.Helpers; namespace MiniExcelLibs.Csv { @@ -43,7 +42,7 @@ namespace MiniExcelLibs.Csv continue; } - var cell = Helpers.GetEmptyExpandoObject(headRows); + var cell = CustomPropertyHelper.GetEmptyExpandoObject(headRows); for (int i = 0; i <= read.Length - 1; i++) cell[headRows[i]] = read[i]; @@ -54,9 +53,9 @@ namespace MiniExcelLibs.Csv //body { - var cell = Helpers.GetEmptyExpandoObject(read.Length - 1, 0); + var cell = CustomPropertyHelper.GetEmptyExpandoObject(read.Length - 1, 0); for (int i = 0; i <= read.Length - 1; i++) - cell[Helpers.GetAlphabetColumnName(i)] = read[i]; + cell[ColumnHelper.GetAlphabetColumnName(i)] = read[i]; yield return cell; } } @@ -80,7 +79,7 @@ namespace MiniExcelLibs.Csv row = reader.ReadLine(); read = Split(cf, row); - var props = Helpers.GetExcelCustomPropertyInfos(type, read); + var props = CustomPropertyHelper.GetExcelCustomPropertyInfos(type, read); var index = 0; foreach (var v in read) { @@ -112,7 +111,7 @@ namespace MiniExcelLibs.Csv if (itemValue == null) continue; - newV = TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell); + newV = TypeHelper.TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell); } } diff --git a/src/MiniExcel/Csv/CsvWriter.cs b/src/MiniExcel/Csv/CsvWriter.cs index 2498be5..72680fb 100644 --- a/src/MiniExcel/Csv/CsvWriter.cs +++ b/src/MiniExcel/Csv/CsvWriter.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using static MiniExcelLibs.Utils.Helpers; namespace MiniExcelLibs.Csv { @@ -72,7 +71,7 @@ namespace MiniExcelLibs.Csv { mode = "Properties"; genericType = item.GetType(); - props = Helpers.GetSaveAsProperties(genericType); + props = CustomPropertyHelper.GetSaveAsProperties(genericType); } break; diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index a230649..dc02c5e 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -27,7 +27,7 @@ public static IEnumerable Query(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() { - using (var stream = Helpers.OpenSharedRead(path)) + using (var stream = FileHelper.OpenSharedRead(path)) foreach (var item in Query(stream, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), startCell, configuration)) yield return item; //Foreach yield return twice reason : https://stackoverflow.com/questions/66791982/ienumerable-extract-code-lazy-loading-show-stream-was-not-readable } @@ -39,7 +39,7 @@ public static IEnumerable Query(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) { - using (var stream = Helpers.OpenSharedRead(path)) + using (var stream = FileHelper.OpenSharedRead(path)) foreach (var item in Query(stream, useHeaderRow, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), startCell, configuration)) yield return item; } @@ -76,7 +76,7 @@ /// public static DataTable QueryAsDataTable(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) { - using (var stream = Helpers.OpenSharedRead(path)) + using (var stream = FileHelper.OpenSharedRead(path)) return QueryAsDataTable(stream, useHeaderRow, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), startCell, configuration); } @@ -90,7 +90,7 @@ public static List GetSheetNames(string path) { - using (var stream = Helpers.OpenSharedRead(path)) + using (var stream = FileHelper.OpenSharedRead(path)) return GetSheetNames(stream); } @@ -102,7 +102,7 @@ public static ICollection GetColumns(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) { - using (var stream = Helpers.OpenSharedRead(path)) + using (var stream = FileHelper.OpenSharedRead(path)) return GetColumns(stream, useHeaderRow, sheetName, excelType, startCell, configuration); } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs index 611ed6d..5f12fd4 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs @@ -10,7 +10,6 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using System.Xml; -using static MiniExcelLibs.Utils.Helpers; namespace MiniExcelLibs.OpenXml { @@ -370,7 +369,7 @@ namespace MiniExcelLibs.OpenXml private static IDictionary GetCell(bool useHeaderRow, int maxColumnIndex, Dictionary headRows, int startColumnIndex) { - return useHeaderRow ? Helpers.GetEmptyExpandoObject(headRows) : Helpers.GetEmptyExpandoObject(maxColumnIndex, startColumnIndex); + return useHeaderRow ? CustomPropertyHelper.GetEmptyExpandoObject(headRows) : CustomPropertyHelper.GetEmptyExpandoObject(maxColumnIndex, startColumnIndex); } private void SetCellsValueAndHeaders(object cellValue, bool useHeaderRow, ref Dictionary headRows, ref bool isFirstRow, ref IDictionary cell, int columnIndex) @@ -395,7 +394,7 @@ namespace MiniExcelLibs.OpenXml else { //if not using First Head then using A,B,C as index - cell[Helpers.GetAlphabetColumnName(columnIndex)] = cellValue; + cell[ColumnHelper.GetAlphabetColumnName(columnIndex)] = cellValue; } } @@ -413,7 +412,7 @@ namespace MiniExcelLibs.OpenXml if (first) { //TODO: alert don't duplicate column name - props = Helpers.GetExcelCustomPropertyInfos(type, headers); + props = CustomPropertyHelper.GetExcelCustomPropertyInfos(type, headers); first = false; } var v = new T(); @@ -428,7 +427,7 @@ namespace MiniExcelLibs.OpenXml if (itemValue == null) continue; - newV = TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell); + newV = TypeHelper.TypeMapping(v, pInfo, newV, itemValue, rowIndex, startCell); } } rowIndex++; diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 3acddb6..a42aa58 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -9,7 +9,6 @@ using System.IO.Compression; using System.Linq; using System.Text; using System.Threading.Tasks; -using static MiniExcelLibs.Utils.Helpers; namespace MiniExcelLibs.OpenXml { @@ -139,7 +138,7 @@ namespace MiniExcelLibs.OpenXml throw new NotImplementedException($"MiniExcel not support only {genericType.Name} value generic type"); else if (genericType == typeof(string) || genericType == typeof(DateTime) || genericType == typeof(Guid)) throw new NotImplementedException($"MiniExcel not support only {genericType.Name} generic type"); - props = Helpers.GetSaveAsProperties(genericType); + props = CustomPropertyHelper.GetSaveAsProperties(genericType); maxColumnIndex = props.Count; } @@ -317,7 +316,7 @@ namespace MiniExcelLibs.OpenXml type = p.ExcludeNullableType; //sometime it doesn't need to re-get type like prop } - if (Helpers.IsNumericType(type)) + if (TypeHelper.IsNumericType(type)) { t = "n"; v = value.ToString(); @@ -476,9 +475,9 @@ namespace MiniExcelLibs.OpenXml else if (maxColumnIndex == 1) dimensionRef = $"A{maxRowIndex}"; else if (maxRowIndex == 0) - dimensionRef = $"A1:{Helpers.GetAlphabetColumnName(maxColumnIndex - 1)}1"; + dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}1"; else - dimensionRef = $"A1:{Helpers.GetAlphabetColumnName(maxColumnIndex - 1)}{maxRowIndex}"; + dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}{maxRowIndex}"; return dimensionRef; } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs index a5daedb..82b1e0e 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs @@ -57,31 +57,31 @@ namespace MiniExcelLibs.OpenXml //TODO: width,height var xy1 = refs[0]; - X1 = Helpers.GetColumnIndex(StringHelper.GetLetter(refs[0])); + X1 = ColumnHelper.GetColumnIndex(StringHelper.GetLetter(refs[0])); Y1 = StringHelper.GetNumber(xy1); var xy2 = refs[1]; - X2 = Helpers.GetColumnIndex(StringHelper.GetLetter(refs[1])); + X2 = ColumnHelper.GetColumnIndex(StringHelper.GetLetter(refs[1])); Y2 = StringHelper.GetNumber(xy2); Width = Math.Abs(X1 - X2) + 1; ; Height = Math.Abs(Y1 - Y2) + 1; } - public string XY1 { get { return $"{Helpers.GetAlphabetColumnName(X1)}{Y1}"; } } + public string XY1 { get { return $"{ColumnHelper.GetAlphabetColumnName(X1)}{Y1}"; } } public int X1 { get; set; } public int Y1 { get; set; } - public string XY2 { get { return $"{Helpers.GetAlphabetColumnName(X2)}{Y2}"; } } + public string XY2 { get { return $"{ColumnHelper.GetAlphabetColumnName(X2)}{Y2}"; } } public int X2 { get; set; } public int Y2 { get; set; } - public string Ref { get { return $"{Helpers.GetAlphabetColumnName(X1)}{Y1}:{Helpers.GetAlphabetColumnName(X2)}{Y2}"; } } + public string Ref { get { return $"{ColumnHelper.GetAlphabetColumnName(X1)}{Y1}:{ColumnHelper.GetAlphabetColumnName(X2)}{Y2}"; } } public XmlElement MergeCell { get; set; } public int Width { get; internal set; } public int Height { get; internal set; } public string ToXmlString(string prefix) { - return $"<{prefix}mergeCell ref=\"{Helpers.GetAlphabetColumnName(X1)}{Y1}:{Helpers.GetAlphabetColumnName(X2)}{Y2}\"/>"; + return $"<{prefix}mergeCell ref=\"{ColumnHelper.GetAlphabetColumnName(X1)}{Y1}:{ColumnHelper.GetAlphabetColumnName(X2)}{Y2}\"/>"; } } @@ -520,7 +520,7 @@ namespace MiniExcelLibs.OpenXml { c.SetAttribute("t", "str"); } - else if (Helpers.IsNumericType(type)) + else if (TypeHelper.IsNumericType(type)) { c.SetAttribute("t", "n"); } @@ -568,7 +568,7 @@ namespace MiniExcelLibs.OpenXml { c.SetAttribute("t", "str"); } - else if (Helpers.IsNumericType(type)) + else if (TypeHelper.IsNumericType(type)) { c.SetAttribute("t", "n"); } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs index 51f8b6e..38fd2e0 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs @@ -35,7 +35,7 @@ namespace MiniExcelLibs.OpenXml public void SaveAsByTemplate(string templatePath, object value) { - using (var stream = Helpers.OpenSharedRead(templatePath)) + using (var stream = FileHelper.OpenSharedRead(templatePath)) SaveAsByTemplateImpl(stream, value); } public void SaveAsByTemplate(byte[] templateBtyes, object value) diff --git a/src/MiniExcel/Utils/ColumnHelper.cs b/src/MiniExcel/Utils/ColumnHelper.cs new file mode 100644 index 0000000..16dc0ff --- /dev/null +++ b/src/MiniExcel/Utils/ColumnHelper.cs @@ -0,0 +1,67 @@ +namespace MiniExcelLibs.Utils +{ + using System.Collections.Generic; + using System.IO; + + // For Row/Column Index + internal static partial class ColumnHelper + { + private const int GENERAL_COLUMN_INDEX = 255; + private const int MAX_COLUMN_INDEX = 16383; + private static Dictionary _IntMappingAlphabet; + private static Dictionary _AlphabetMappingInt; + static ColumnHelper() + { + if (_IntMappingAlphabet == null && _AlphabetMappingInt == null) + { + _IntMappingAlphabet = new Dictionary(); + _AlphabetMappingInt = new Dictionary(); + for (int i = 0; i <= GENERAL_COLUMN_INDEX; i++) + { + _IntMappingAlphabet.Add(i, IntToLetters(i)); + _AlphabetMappingInt.Add(IntToLetters(i), i); + } + } + } + + public static string GetAlphabetColumnName(int columnIndex) + { + CheckAndSetMaxColumnIndex(columnIndex); + return _IntMappingAlphabet[columnIndex]; + } + + public static int GetColumnIndex(string columnName) + { + var columnIndex = _AlphabetMappingInt[columnName]; + CheckAndSetMaxColumnIndex(columnIndex); + return columnIndex; + } + + private static void CheckAndSetMaxColumnIndex(int columnIndex) + { + if (columnIndex >= _IntMappingAlphabet.Count) + { + if (columnIndex > MAX_COLUMN_INDEX) + throw new InvalidDataException($"ColumnIndex {columnIndex} over excel vaild max index."); + for (int i = _IntMappingAlphabet.Count; i <= columnIndex; i++) + { + _IntMappingAlphabet.Add(i, IntToLetters(i)); + _AlphabetMappingInt.Add(IntToLetters(i), i); + } + } + } + + internal static string IntToLetters(int value) + { + value = value + 1; + string result = string.Empty; + while (--value >= 0) + { + result = (char)('A' + value % 26) + result; + value /= 26; + } + return result; + } + } + +} diff --git a/src/MiniExcel/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs new file mode 100644 index 0000000..834aee7 --- /dev/null +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -0,0 +1,150 @@ +namespace MiniExcelLibs.Utils +{ + using MiniExcelLibs.Attributes; + using System; + using System.Collections.Generic; + using System.Dynamic; + using System.Linq; + using System.Reflection; + + internal class ExcelCustomPropertyInfo + { + public int? ExcelColumnIndex { get; set; } + public string ExcelColumnName { get; set; } + public PropertyInfo Property { get; set; } + public Type ExcludeNullableType { get; set; } + public bool Nullable { get; internal set; } + public string ExcelFormat { get; internal set; } + } + + internal static partial class CustomPropertyHelper + { + internal static IDictionary GetEmptyExpandoObject(int maxColumnIndex, int startCellIndex) + { + // TODO: strong type mapping can ignore this + // TODO: it can recode better performance + var cell = (IDictionary)new ExpandoObject(); + for (int i = startCellIndex; i <= maxColumnIndex; i++) + { + var key = ColumnHelper.GetAlphabetColumnName(i); + if (!cell.ContainsKey(key)) + cell.Add(key, null); + } + return cell; + } + + internal static IDictionary GetEmptyExpandoObject(Dictionary hearrows) + { + // TODO: strong type mapping can ignore this + // TODO: it can recode better performance + var cell = (IDictionary)new ExpandoObject(); + foreach (var hr in hearrows) + if (!cell.ContainsKey(hr.Value)) + cell.Add(hr.Value, null); + return cell; + } + + internal static List GetSaveAsProperties(this Type type) + { + List props = GetExcelPropertyInfo(type, BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.Property.GetGetMethod() != null && !prop.Property.GetAttributeValue((ExcelIgnoreAttribute x) => x.ExcelIgnore)) + .ToList() /*ignore without set*/; + + if (props.Count == 0) + throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0"); + + // https://github.com/shps951023/MiniExcel/issues/142 + //TODO: need optimize performance + { + var withCustomIndexProps = props.Where(w => w.ExcelColumnIndex != null && w.ExcelColumnIndex > -1); + if (withCustomIndexProps.GroupBy(g => g.ExcelColumnIndex).Any(_ => _.Count() > 1)) + throw new InvalidOperationException($"Duplicate column name"); + + var maxColumnIndex = props.Count - 1; + if (withCustomIndexProps.Any()) + maxColumnIndex = Math.Max((int)withCustomIndexProps.Max(w => w.ExcelColumnIndex), maxColumnIndex); + + var withoutCustomIndexProps = props.Where(w => w.ExcelColumnIndex == null).ToList(); + + List newProps = new List(); + var index = 0; + for (int i = 0; i <= maxColumnIndex; i++) + { + var p1 = withCustomIndexProps.SingleOrDefault(s => s.ExcelColumnIndex == i); + if (p1 != null) + { + newProps.Add(p1); + } + else + { + var p2 = withoutCustomIndexProps.ElementAtOrDefault(index); + if (p2 == null) + { + newProps.Add(null); + } + else + { + p2.ExcelColumnIndex = i; + newProps.Add(p2); + } + index++; + } + } + return newProps; + } + } + + internal static List GetExcelCustomPropertyInfos(Type type, string[] headers) + { + List props = GetExcelPropertyInfo(type, BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.Property.GetSetMethod() != null && !prop.Property.GetAttributeValue((ExcelIgnoreAttribute x) => x.ExcelIgnore)) + .ToList() /*ignore without set*/; + + if (props.Count == 0) + throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0"); + + { + var withCustomIndexProps = props.Where(w => w.ExcelColumnIndex != null && w.ExcelColumnIndex > -1); + if (withCustomIndexProps.GroupBy(g => g.ExcelColumnIndex).Any(_ => _.Count() > 1)) + throw new InvalidOperationException($"Duplicate column name"); + + foreach (var p in props) + { + if (p.ExcelColumnIndex != null) + { + if (p.ExcelColumnIndex >= headers.Length) + throw new ArgumentException($"ExcelColumnIndex {p.ExcelColumnIndex} over haeder max index {headers.Length}"); + p.ExcelColumnName = headers[(int)p.ExcelColumnIndex]; + if (p.ExcelColumnName == null) + throw new InvalidOperationException($"{p.Property.DeclaringType.Name} {p.Property.Name}'s ExcelColumnIndex {p.ExcelColumnIndex} can't find excel column name"); + } + } + } + + return props; + } + + private static IEnumerable GetExcelPropertyInfo(Type type, BindingFlags bindingFlags) + { + return type.GetProperties(bindingFlags) + // solve : https://github.com/shps951023/MiniExcel/issues/138 + .Select(p => + { + var gt = Nullable.GetUnderlyingType(p.PropertyType); + var excelNameAttr = p.GetAttribute(); + var excelIndexAttr = p.GetAttribute(); + return new ExcelCustomPropertyInfo + { + Property = p, + ExcludeNullableType = gt ?? p.PropertyType, + Nullable = gt != null ? true : false, + ExcelColumnName = excelNameAttr?.ExcelColumnName ?? p.Name, + ExcelColumnIndex = excelIndexAttr?.ExcelColumnIndex, + ExcelFormat = p.GetAttribute()?.Format, + }; + }); + } + + } + +} diff --git a/src/MiniExcel/Utils/FileHelper.cs b/src/MiniExcel/Utils/FileHelper.cs new file mode 100644 index 0000000..7dce7f9 --- /dev/null +++ b/src/MiniExcel/Utils/FileHelper.cs @@ -0,0 +1,10 @@ +namespace MiniExcelLibs.Utils +{ + using System.IO; + + internal static partial class FileHelper + { + public static FileStream OpenSharedRead(string path) => File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + +} diff --git a/src/MiniExcel/Utils/Helpers.cs b/src/MiniExcel/Utils/Helpers.cs deleted file mode 100644 index 676a66b..0000000 --- a/src/MiniExcel/Utils/Helpers.cs +++ /dev/null @@ -1,316 +0,0 @@ -namespace MiniExcelLibs.Utils -{ - using MiniExcelLibs.Attributes; - using System; - using System.Collections.Generic; - using System.Dynamic; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Reflection; - - internal static partial class Helpers - { - public static FileStream OpenSharedRead(string path) => File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - } - - // For Row/Column Index - internal static partial class Helpers - { - private const int GENERAL_COLUMN_INDEX = 255; - private const int MAX_COLUMN_INDEX = 16383; - private static Dictionary _IntMappingAlphabet; - private static Dictionary _AlphabetMappingInt; - static Helpers() - { - if (_IntMappingAlphabet == null && _AlphabetMappingInt == null) - { - _IntMappingAlphabet = new Dictionary(); - _AlphabetMappingInt = new Dictionary(); - for (int i = 0; i <= GENERAL_COLUMN_INDEX; i++) - { - _IntMappingAlphabet.Add(i, IntToLetters(i)); - _AlphabetMappingInt.Add(IntToLetters(i), i); - } - } - } - - public static string GetAlphabetColumnName(int columnIndex) - { - CheckAndSetMaxColumnIndex(columnIndex); - return _IntMappingAlphabet[columnIndex]; - } - - public static int GetColumnIndex(string columnName) - { - var columnIndex = _AlphabetMappingInt[columnName]; - CheckAndSetMaxColumnIndex(columnIndex); - return columnIndex; - } - - private static void CheckAndSetMaxColumnIndex(int columnIndex) - { - if (columnIndex >= _IntMappingAlphabet.Count) - { - if (columnIndex > MAX_COLUMN_INDEX) - throw new InvalidDataException($"ColumnIndex {columnIndex} over excel vaild max index."); - for (int i = _IntMappingAlphabet.Count; i <= columnIndex; i++) - { - _IntMappingAlphabet.Add(i, IntToLetters(i)); - _AlphabetMappingInt.Add(IntToLetters(i), i); - } - } - } - - internal static string IntToLetters(int value) - { - value = value + 1; - string result = string.Empty; - while (--value >= 0) - { - result = (char)('A' + value % 26) + result; - value /= 26; - } - return result; - } - } - - internal static partial class Helpers - { - internal static IDictionary GetEmptyExpandoObject(int maxColumnIndex, int startCellIndex) - { - // TODO: strong type mapping can ignore this - // TODO: it can recode better performance - var cell = (IDictionary)new ExpandoObject(); - for (int i = startCellIndex; i <= maxColumnIndex; i++) - { - var key = GetAlphabetColumnName(i); - if (!cell.ContainsKey(key)) - cell.Add(key, null); - } - return cell; - } - - internal static IDictionary GetEmptyExpandoObject(Dictionary hearrows) - { - // TODO: strong type mapping can ignore this - // TODO: it can recode better performance - var cell = (IDictionary)new ExpandoObject(); - foreach (var hr in hearrows) - if (!cell.ContainsKey(hr.Value)) - cell.Add(hr.Value, null); - return cell; - } - - internal static List GetSaveAsProperties(this Type type) - { - List props = GetExcelPropertyInfo(type, BindingFlags.Public | BindingFlags.Instance) - .Where(prop => prop.Property.GetGetMethod() != null && !prop.Property.GetAttributeValue((ExcelIgnoreAttribute x) => x.ExcelIgnore)) - .ToList() /*ignore without set*/; - - if (props.Count == 0) - throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0"); - - // https://github.com/shps951023/MiniExcel/issues/142 - //TODO: need optimize performance - { - var withCustomIndexProps = props.Where(w => w.ExcelColumnIndex != null && w.ExcelColumnIndex > -1); - if (withCustomIndexProps.GroupBy(g => g.ExcelColumnIndex).Any(_ => _.Count() > 1)) - throw new InvalidOperationException($"Duplicate column name"); - - var maxColumnIndex = props.Count - 1; - if (withCustomIndexProps.Any()) - maxColumnIndex = Math.Max((int)withCustomIndexProps.Max(w => w.ExcelColumnIndex), maxColumnIndex); - - var withoutCustomIndexProps = props.Where(w => w.ExcelColumnIndex == null).ToList(); - - List newProps = new List(); - var index = 0; - for (int i = 0; i <= maxColumnIndex; i++) - { - var p1 = withCustomIndexProps.SingleOrDefault(s => s.ExcelColumnIndex == i); - if (p1 != null) - { - newProps.Add(p1); - } - else - { - var p2 = withoutCustomIndexProps.ElementAtOrDefault(index); - if (p2 == null) - { - newProps.Add(null); - } - else - { - p2.ExcelColumnIndex = i; - newProps.Add(p2); - } - index++; - } - } - return newProps; - } - } - - internal class ExcelCustomPropertyInfo - { - public int? ExcelColumnIndex { get; set; } - public string ExcelColumnName { get; set; } - public PropertyInfo Property { get; set; } - public Type ExcludeNullableType { get; set; } - public bool Nullable { get; internal set; } - public string ExcelFormat { get; internal set; } - } - - internal static List GetExcelCustomPropertyInfos(Type type, string[] headers) - { - List props = GetExcelPropertyInfo(type, BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance) - .Where(prop => prop.Property.GetSetMethod() != null && !prop.Property.GetAttributeValue((ExcelIgnoreAttribute x) => x.ExcelIgnore)) - .ToList() /*ignore without set*/; - - if (props.Count == 0) - throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0"); - - { - var withCustomIndexProps = props.Where(w => w.ExcelColumnIndex != null && w.ExcelColumnIndex > -1); - if (withCustomIndexProps.GroupBy(g => g.ExcelColumnIndex).Any(_ => _.Count() > 1)) - throw new InvalidOperationException($"Duplicate column name"); - - foreach (var p in props) - { - if (p.ExcelColumnIndex != null) - { - if (p.ExcelColumnIndex >= headers.Length) - throw new ArgumentException($"ExcelColumnIndex {p.ExcelColumnIndex} over haeder max index {headers.Length}"); - p.ExcelColumnName = headers[(int)p.ExcelColumnIndex]; - if (p.ExcelColumnName == null) - throw new InvalidOperationException($"{p.Property.DeclaringType.Name} {p.Property.Name}'s ExcelColumnIndex {p.ExcelColumnIndex} can't find excel column name"); - } - } - } - - return props; - } - - private static IEnumerable GetExcelPropertyInfo(Type type, BindingFlags bindingFlags) - { - return type.GetProperties(bindingFlags) - // solve : https://github.com/shps951023/MiniExcel/issues/138 - .Select(p => - { - var gt = Nullable.GetUnderlyingType(p.PropertyType); - var excelNameAttr = p.GetAttribute(); - var excelIndexAttr = p.GetAttribute(); - return new ExcelCustomPropertyInfo - { - Property = p, - ExcludeNullableType = gt ?? p.PropertyType, - Nullable = gt != null ? true : false, - ExcelColumnName = excelNameAttr?.ExcelColumnName ?? p.Name, - ExcelColumnIndex = excelIndexAttr?.ExcelColumnIndex, - ExcelFormat = p.GetAttribute()?.Format, - }; - }); - } - - public static bool IsNumericType(Type type, bool isNullableUnderlyingType = false) - { - if (isNullableUnderlyingType) - type = Nullable.GetUnderlyingType(type) ?? type; - switch (Type.GetTypeCode(type)) - { - //case TypeCode.Byte: - //case TypeCode.SByte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Single: - return true; - default: - return false; - } - } - - public static object TypeMapping(T v, ExcelCustomPropertyInfo pInfo, object newValue, object itemValue, int rowIndex, string startCell) where T : class, new() - { - try - { - return TypeMappingImpl(v, pInfo, ref newValue, itemValue); - } - catch (Exception ex) when (ex is InvalidCastException || ex is FormatException) - { - var columnName = pInfo.ExcelColumnName ?? pInfo.Property.Name; - var startRowIndex = ReferenceHelper.ConvertCellToXY(startCell).Item2; - var errorRow = startRowIndex + rowIndex + 1; - throw new InvalidCastException($"ColumnName : {columnName}, CellRow : {errorRow}, Value : {itemValue}, it can't cast to {pInfo.Property.PropertyType.Name} type."); - } - } - - private static object TypeMappingImpl(T v, ExcelCustomPropertyInfo pInfo, ref object newValue, object itemValue) where T : class, new() - { - if (pInfo.ExcludeNullableType == typeof(Guid)) - { - newValue = Guid.Parse(itemValue.ToString()); - } - else if (pInfo.ExcludeNullableType == typeof(DateTime)) - { - // fix issue 257 https://github.com/shps951023/MiniExcel/issues/257 - if (itemValue is DateTime || itemValue is DateTime?) - { - newValue = itemValue; - pInfo.Property.SetValue(v, newValue); - return newValue; - } - - var vs = itemValue?.ToString(); - if (pInfo.ExcelFormat != null) - { - if (DateTime.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v)) - { - newValue = _v; - } - } - else if (DateTime.TryParse(vs, CultureInfo.InvariantCulture, 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 = DateTimeHelper.FromOADate(_d); - else - throw new InvalidCastException($"{vs} can't cast to datetime"); - } - else if (pInfo.ExcludeNullableType == typeof(bool)) - { - var vs = itemValue.ToString(); - if (vs == "1") - newValue = true; - else if (vs == "0") - newValue = false; - else - newValue = bool.Parse(vs); - } - else if (pInfo.Property.PropertyType == typeof(string)) - { - newValue = XmlEncoder.DecodeString(itemValue?.ToString()); - } - else if (pInfo.Property.PropertyType.IsEnum) - { - newValue = Enum.Parse(pInfo.Property.PropertyType, itemValue?.ToString(), true); - } - else - { - // Use pInfo.ExcludeNullableType to resolve : https://github.com/shps951023/MiniExcel/issues/138 - newValue = Convert.ChangeType(itemValue, pInfo.ExcludeNullableType); - } - - pInfo.Property.SetValue(v, newValue); - return newValue; - } - } - -} diff --git a/src/MiniExcel/Utils/TypeHelper.cs b/src/MiniExcel/Utils/TypeHelper.cs new file mode 100644 index 0000000..e062a84 --- /dev/null +++ b/src/MiniExcel/Utils/TypeHelper.cs @@ -0,0 +1,115 @@ +namespace MiniExcelLibs.Utils +{ + using MiniExcelLibs.Attributes; + using System; + using System.Collections.Generic; + using System.Dynamic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + + internal static partial class TypeHelper + { + public static bool IsNumericType(Type type, bool isNullableUnderlyingType = false) + { + if (isNullableUnderlyingType) + type = Nullable.GetUnderlyingType(type) ?? type; + switch (Type.GetTypeCode(type)) + { + //case TypeCode.Byte: + //case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + default: + return false; + } + } + + + public static object TypeMapping(T v, ExcelCustomPropertyInfo pInfo, object newValue, object itemValue, int rowIndex, string startCell) where T : class, new() + { + try + { + return TypeMappingImpl(v, pInfo, ref newValue, itemValue); + } + catch (Exception ex) when (ex is InvalidCastException || ex is FormatException) + { + var columnName = pInfo.ExcelColumnName ?? pInfo.Property.Name; + var startRowIndex = ReferenceHelper.ConvertCellToXY(startCell).Item2; + var errorRow = startRowIndex + rowIndex + 1; + throw new InvalidCastException($"ColumnName : {columnName}, CellRow : {errorRow}, Value : {itemValue}, it can't cast to {pInfo.Property.PropertyType.Name} type."); + } + } + + private static object TypeMappingImpl(T v, ExcelCustomPropertyInfo pInfo, ref object newValue, object itemValue) where T : class, new() + { + if (pInfo.ExcludeNullableType == typeof(Guid)) + { + newValue = Guid.Parse(itemValue.ToString()); + } + else if (pInfo.ExcludeNullableType == typeof(DateTime)) + { + // fix issue 257 https://github.com/shps951023/MiniExcel/issues/257 + if (itemValue is DateTime || itemValue is DateTime?) + { + newValue = itemValue; + pInfo.Property.SetValue(v, newValue); + return newValue; + } + + var vs = itemValue?.ToString(); + if (pInfo.ExcelFormat != null) + { + if (DateTime.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v)) + { + newValue = _v; + } + } + else if (DateTime.TryParse(vs, CultureInfo.InvariantCulture, 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 = DateTimeHelper.FromOADate(_d); + else + throw new InvalidCastException($"{vs} can't cast to datetime"); + } + else if (pInfo.ExcludeNullableType == typeof(bool)) + { + var vs = itemValue.ToString(); + if (vs == "1") + newValue = true; + else if (vs == "0") + newValue = false; + else + newValue = bool.Parse(vs); + } + else if (pInfo.Property.PropertyType == typeof(string)) + { + newValue = XmlEncoder.DecodeString(itemValue?.ToString()); + } + else if (pInfo.Property.PropertyType.IsEnum) + { + newValue = Enum.Parse(pInfo.Property.PropertyType, itemValue?.ToString(), true); + } + else + { + // Use pInfo.ExcludeNullableType to resolve : https://github.com/shps951023/MiniExcel/issues/138 + newValue = Convert.ChangeType(itemValue, pInfo.ExcludeNullableType); + } + + pInfo.Property.SetValue(v, newValue); + return newValue; + } + + } +}