Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace MiniExcelLib.Core.Abstractions;

public interface IMiniExcelWriteAdapterAsync
public interface IMiniExcelWriteAdapterAsync : IAsyncDisposable
{
Task<List<MiniExcelColumnMapping>?> GetColumnsAsync();
IAsyncEnumerable<CellWriteInfo[]> GetRowsAsync(List<MiniExcelColumnMapping> mappings, CancellationToken cancellationToken);
Expand Down
9 changes: 8 additions & 1 deletion src/MiniExcel.Core/Helpers/Polyfills.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ public static class Polyfills
return dictionary.TryGetValue(key, out var value) ? value : defaultValue;
}

[EditorBrowsable(EditorBrowsableState.Advanced)]
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{
key = kvp.Key;
value = kvp.Value;
}

extension(Math)
{
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static TNumber Clamp<TNumber>(TNumber value, TNumber min, TNumber max) where TNumber : unmanaged, IComparable<TNumber>
public static TNumber Clamp<TNumber>(TNumber value, TNumber min, TNumber max) where TNumber : IComparable<TNumber>
{
if (value.CompareTo(min) < 0) return min;
if (value.CompareTo(max) > 0) return max;
Expand Down
6 changes: 5 additions & 1 deletion src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using MiniExcelLib.Core.Attributes;

namespace MiniExcelLib.Core.Reflection;
Expand All @@ -16,7 +17,10 @@ public class MiniExcelColumnMapping
public string? ExcelIndexName { get; internal set; }
public bool ExcelHiddenColumn { get; internal set; }
public bool ExcelIgnoreColumn { get; internal set; }
public int ExcelFormatId { get; internal set; }
public int ExcelFormatId { get; internal set; } = -1;
public ColumnType ExcelColumnType { get; internal set; }
public Func<object?, object?>? CustomFormatter { get; set; }

[EditorBrowsable(EditorBrowsableState.Never)]
public void SetFormatId(int fmtId) => ExcelFormatId = fmtId;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
namespace MiniExcelLib.Core.WriteAdapters;

internal sealed class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable
internal sealed class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync
{
private readonly IAsyncEnumerable<T> _values = values;
private readonly MiniExcelBaseConfiguration _configuration = configuration;

private IAsyncEnumerator<T>? _enumerator;
private bool _empty;
private bool _disposed = false;
private bool _disposed;


public async Task<List<MiniExcelColumnMapping>?> GetColumnsAsync()
{
if (ColumnMappingsProvider.TryGetColumnMappings(typeof(T), _configuration, out var mappings))
{
return mappings;

Check warning on line 17 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Nullability of reference types in value of type 'List<MiniExcelColumnMapping?>' doesn't match target type 'List<MiniExcelColumnMapping>'.

Check warning on line 17 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in value of type 'List<MiniExcelColumnMapping?>' doesn't match target type 'List<MiniExcelColumnMapping>'.
}

_enumerator = _values.GetAsyncEnumerator();
Expand All @@ -24,7 +24,7 @@
return null;
}

return ColumnMappingsProvider.GetColumnMappingFromValue(_enumerator.Current, _configuration);

Check warning on line 27 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Nullability of reference types in value of type 'List<MiniExcelColumnMapping?>' doesn't match target type 'List<MiniExcelColumnMapping>'.

Check warning on line 27 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Possible null reference argument for parameter 'value' in 'List<MiniExcelColumnMapping?> ColumnMappingsProvider.GetColumnMappingFromValue(object value, MiniExcelBaseConfiguration configuration)'.

Check warning on line 27 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in value of type 'List<MiniExcelColumnMapping?>' doesn't match target type 'List<MiniExcelColumnMapping>'.

Check warning on line 27 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'value' in 'List<MiniExcelColumnMapping?> ColumnMappingsProvider.GetColumnMappingFromValue(object value, MiniExcelBaseConfiguration configuration)'.
}

public async IAsyncEnumerable<CellWriteInfo[]> GetRowsAsync(List<MiniExcelColumnMapping> mappings, [EnumeratorCancellation] CancellationToken cancellationToken)
Expand All @@ -44,7 +44,7 @@
do
{
cancellationToken.ThrowIfCancellationRequested();
yield return GetRowValues(_enumerator.Current, mappings);

Check warning on line 47 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Argument of type 'List<MiniExcelColumnMapping>' cannot be used for parameter 'mappings' of type 'List<MiniExcelColumnMapping?>' in 'CellWriteInfo[] AsyncEnumerableWriteAdapter<T>.GetRowValues(T currentValue, List<MiniExcelColumnMapping?> mappings)' due to differences in the nullability of reference types.

Check warning on line 47 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'List<MiniExcelColumnMapping>' cannot be used for parameter 'mappings' of type 'List<MiniExcelColumnMapping?>' in 'CellWriteInfo[] AsyncEnumerableWriteAdapter<T>.GetRowValues(T currentValue, List<MiniExcelColumnMapping?> mappings)' due to differences in the nullability of reference types.
}
while (await _enumerator.MoveNextAsync().ConfigureAwait(false));
}
Expand All @@ -60,9 +60,9 @@
var cellValue = currentValue switch
{
_ when map is null => null,
IDictionary<string, object> genericDictionary => genericDictionary[map.Key.ToString()],

Check warning on line 63 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Possible null reference argument for parameter 'key' in 'object IDictionary<string, object>.this[string key]'.
IDictionary dictionary => dictionary[map.Key],
_ => map.MemberAccessor.GetValue(currentValue)

Check warning on line 65 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'instance' in 'object? MiniExcelMemberAccessor.GetValue(object instance)'.
};
result.Add(new CellWriteInfo(cellValue, column, map));
}
Expand Down
89 changes: 51 additions & 38 deletions src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ namespace MiniExcelLib.OpenXml;

internal partial class OpenXmlWriter
{
private const string DefaultCellStyleIndex = "0";
private const string HeaderCellStyleIndex = "1";
private const string RegularCellStyleIndex = "2";
private static readonly DateTime ExcelZeroDate = new(1899, 12, 31);

private readonly Dictionary<string, ZipPackageInfo> _zipDictionary = [];
private Dictionary<string, string> _cellXfIdMap = [];

private IEnumerable<(SheetDto, object?)> GetSheets()
private IEnumerable<(SheetDto Sheet, object? Data)> GetSheets()
{
var sheetId = 0;
if (_value is IDictionary<string, object?> dictionary)
Expand Down Expand Up @@ -153,29 +157,39 @@ private string GetPanes()
return sb.ToString();
}

private (string, string?, string?) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull)
private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull)
{
if (valueIsNull)
return ("2", "str", string.Empty);
return (RegularCellStyleIndex, "str", string.Empty);

if (value is string str)
return ("2", "str", XmlHelper.EncodeXml(str));
return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(str));

var type = GetValueType(value, columnInfo);

if (columnInfo is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue)
{
var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture);
return ("2", "str", XmlHelper.EncodeXml(formattedStr));
return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(formattedStr));
}

if (type == typeof(DateTime))
return GetDateTimeValue((DateTime)value, columnInfo);

#if NET6_0_OR_GREATER
if (type == typeof(DateTimeOffset))
return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnInfo);

if (type == typeof(TimeSpan))
return GetTimeSpanValue((TimeSpan)value, columnInfo);

#if NET8_0_OR_GREATER
if (type == typeof(DateOnly))
return GetDateTimeValue(((DateOnly)value).ToDateTime(new TimeOnly()), columnInfo);
return GetDateTimeValue(((DateOnly)value).ToDateTime(default), columnInfo);

if (type == typeof(TimeOnly))
return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnInfo);
#endif

if (type.IsEnum)
{
string? description = null;
Expand All @@ -188,24 +202,23 @@ private string GetPanes()
}

description ??= value.ToString();
return ("2", "str", description);
return (RegularCellStyleIndex, "str", description);
}

if (TypeHelper.IsNumericType(type))
{
var cellValue = GetNumericValue(value, type);

if (columnInfo?.ExcelFormat is null)
{
var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? "n" : "str";
return ("2", dataType, cellValue);
return (RegularCellStyleIndex, dataType, cellValue);
}

return (columnInfo.ExcelFormatId.ToString(), null, cellValue);
}

if (type == typeof(bool))
return ("2", "b", (bool)value ? "1" : "0");
return (RegularCellStyleIndex, "b", (bool)value ? "1" : "0");

if (type == typeof(byte[]) && _configuration.EnableConvertByteArray)
{
Expand All @@ -216,25 +229,18 @@ private string GetPanes()
return ("4", "str", XmlHelper.EncodeXml(base64));
}

return ("2", "str", XmlHelper.EncodeXml(value.ToString()));
return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(value.ToString()));
}

private static Type? GetValueType(object value, MiniExcelColumnMapping? columnInfo)
private static Type GetValueType(object value, MiniExcelColumnMapping? columnInfo)
{
Type type;
if (columnInfo is not { Key: null })
{
// TODO: need to optimize
// Dictionary need to check type every time, so it's slow..
type = value.GetType();
type = Nullable.GetUnderlyingType(type) ?? type;
}
else
{
type = columnInfo.ExcludeNullableType; //sometime it doesn't need to re-get type like prop
}
if (columnInfo is { Key: null })
return columnInfo.ExcludeNullableType; //sometime it doesn't need to re-get type like prop

return type;
// TODO: need to optimize
// Dictionary need to check type every time, so it's slow..
var type = value.GetType();
return Nullable.GetUnderlyingType(type) ?? type;
}

private string GetNumericValue(object value, Type type)
Expand Down Expand Up @@ -303,30 +309,31 @@ private string GetFileValue(int rowIndex, int cellIndex, object value)
return base64;
}

private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping columnInfo)
//todo:reconsider cultureinfo
private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping? columnMapping)
{
string? cellValue;
if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture))
{
cellValue = value.ToString(_configuration.Culture);
return ("2", (string?)"str", cellValue);
return (RegularCellStyleIndex, (string?)"str", cellValue);
}

var oaDate = CorrectDateTimeValue(value);
cellValue = oaDate.ToString(CultureInfo.InvariantCulture);
var format = columnInfo?.ExcelFormat is not null ? columnInfo.ExcelFormatId.ToString() : "3";
var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "3";

return (format, null, cellValue);
}

private static double CorrectDateTimeValue(DateTime value)
{
var oaDate = value.ToOADate();

// Excel says 1900 was a leap year :( Replicate an incorrect behavior thanks
// to Lotus 1-2-3 decision from 1983...
// https://github.com/ClosedXML/ClosedXML/blob/develop/ClosedXML/Extensions/DateTimeExtensions.cs#L45
const int nonExistent1900Feb29SerialDate = 60;

var oaDate = value.ToOADate();
if (oaDate <= nonExistent1900Feb29SerialDate)
{
oaDate -= 1;
Expand All @@ -335,6 +342,17 @@ private static double CorrectDateTimeValue(DateTime value)
return oaDate;
}

private (string, string?, string) GetTimeSpanValue(TimeSpan value, MiniExcelColumnMapping? columnMapping)
{
if (value.TotalDays >= 1)
return GetDateTimeValue(ExcelZeroDate + value, columnMapping);

var cellValue = (value.TotalSeconds / 86400).ToString(CultureInfo.InvariantCulture);
var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "5";

return (format, null, cellValue);
}

private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex)
{
return (maxRowIndex, maxColumnIndex) switch
Expand Down Expand Up @@ -406,9 +424,4 @@ private string GetContentTypesXml()
sb.Append(ExcelXml.EndTypes);
return sb.ToString();
}

private string GetCellXfId(string styleIndex)
{
return _cellXfIdMap.GetValueOrDefault(styleIndex, styleIndex);
}
}
}
Loading
Loading