Skip to content

Commit

Permalink
Custom DateTime format (#616)
Browse files Browse the repository at this point in the history
* - goto/jumpmark

* + generate numberformats

* + `FormatId`

* ~ first working shot

* ~ assign responsibilities correctly

* ~ fix last test issues

* + extend tests

* ~ clean up

* ~ simplify `DateOnly` handling

* + `DateOnly` to Tests

* Update ExcelOpenXmlSheetWriter.cs

Update GenerateSheetByIDataReader

* + datetime format for async part

---------

Co-authored-by: Gary Jia <35099424+jiaguangli@users.noreply.github.com>
  • Loading branch information
DancePanda42 and jiaguangli authored Jun 13, 2024
1 parent 12bb1c0 commit 00a445c
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 99 deletions.
3 changes: 3 additions & 0 deletions src/MiniExcel/Attributes/ExcelColumnAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class ExcelColumnAttribute : Attribute
private int _index = -1;
private string _xName;

internal int FormatId { get; set; } = -1;

public string Name { get; set; }

public string[] Aliases { get; set; }
Expand Down Expand Up @@ -52,6 +54,7 @@ private void Init(int index, string columnName = null)
public class DynamicExcelColumn : ExcelColumnAttribute
{
public string Key { get; set; }

public DynamicExcelColumn(string key)
{
Key = key;
Expand Down
74 changes: 70 additions & 4 deletions src/MiniExcel/OpenXml/Constants/ExcelXml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
using MiniExcelLibs.OpenXml.Models;
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml.Models;
using MiniExcelLibs.Utils;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MiniExcelLibs.OpenXml.Constants
{
Expand Down Expand Up @@ -52,10 +57,21 @@ static ExcelXml()
</x:cellXfs>
</x:styleSheet>";

internal static readonly string DefaultStylesXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
#region StyleSheet

private const int startUpNumFmts = 1;
private const string NumFmtsToken = "{{numFmts}}";
private const string NumFmtsCountToken = "{{numFmtCount}}";

private const int startUpCellXfs = 5;
private const string cellXfsToken = "{{cellXfs}}";
private const string cellXfsCountToken = "{{cellXfsCount}}";

internal static readonly string DefaultStylesXml = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<x:styleSheet xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">
<x:numFmts count=""1"">
<x:numFmts count=""{NumFmtsCountToken}"">
<x:numFmt numFmtId=""0"" formatCode="""" />
{NumFmtsToken}
</x:numFmts>
<x:fonts count=""2"">
<x:font>
Expand Down Expand Up @@ -133,7 +149,7 @@ static ExcelXml()
<x:protection locked=""1"" hidden=""0"" />
</x:xf>
</x:cellStyleXfs>
<x:cellXfs count=""4"">
<x:cellXfs count=""{cellXfsCountToken}"">
<x:xf></x:xf>
<x:xf numFmtId=""0"" fontId=""1"" fillId=""2"" borderId=""1"" xfId=""0"" applyNumberFormat=""1"" applyFill=""0"" applyBorder=""1"" applyAlignment=""1"" applyProtection=""1"">
<x:alignment horizontal=""left"" vertical=""bottom"" textRotation=""0"" wrapText=""0"" indent=""0"" relativeIndent=""0"" justifyLastLine=""0"" shrinkToFit=""0"" readingOrder=""0"" />
Expand All @@ -150,12 +166,15 @@ static ExcelXml()
<x:xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""1"" xfId=""0"" applyBorder=""1"" applyAlignment=""1"">
<x:alignment horizontal=""fill""/>
</x:xf>
{cellXfsToken}
</x:cellXfs>
<x:cellStyles count=""1"">
<x:cellStyle name=""Normal"" xfId=""0"" builtinId=""0"" />
</x:cellStyles>
</x:styleSheet>";

#endregion

internal static readonly string DefaultWorkbookXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<x:workbook xmlns:r=""http://schemas.openxmlformats.org/officeDocument/2006/relationships""
xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">
Expand Down Expand Up @@ -231,5 +250,52 @@ internal static string DrawingXml(FileDto file, int fileIndex)

internal static string Sheet(SheetDto sheetDto, int sheetId)
=> $@"<x:sheet name=""{ExcelOpenXmlUtils.EncodeXML(sheetDto.Name)}"" sheetId=""{sheetId}""{(string.IsNullOrWhiteSpace(sheetDto.State) ? string.Empty : $" state=\"{sheetDto.State}\"")} r:id=""{sheetDto.ID}"" />";

internal static string SetupStyleXml(string styleXml, ICollection<ExcelColumnAttribute> columns)
{
const int numFmtIndex = 166;

var sb = new StringBuilder(styleXml);
var columnsToApply = GenerateStyleIds(columns);

var numFmts = columnsToApply.Select((x, i) =>
{
return new
{
numFmt =
$@"<x:numFmt numFmtId=""{numFmtIndex + i}"" formatCode=""{x.Format}"" />",
cellXfs =
$@"<x:xf numFmtId=""{numFmtIndex + i}"" fontId=""0"" fillId=""0"" borderId=""1"" xfId=""0"" applyNumberFormat=""1"" applyFill=""1"" applyBorder=""1"" applyAlignment=""1"" applyProtection=""1"">
<x:alignment horizontal=""general"" vertical=""bottom"" textRotation=""0"" wrapText=""0"" indent=""0"" relativeIndent=""0"" justifyLastLine=""0"" shrinkToFit=""0"" readingOrder=""0"" />
<x:protection locked=""1"" hidden=""0"" />
</x:xf>"
};
}).ToArray();

sb.Replace(NumFmtsToken, string.Join(string.Empty, numFmts.Select(x => x.numFmt)));
sb.Replace(NumFmtsCountToken, (startUpNumFmts + numFmts.Length).ToString());

sb.Replace(cellXfsToken, string.Join(string.Empty, numFmts.Select(x => x.cellXfs)));
sb.Replace(cellXfsCountToken, (5 + numFmts.Length).ToString());
return sb.ToString();
}

private static IEnumerable<ExcelColumnAttribute> GenerateStyleIds(ICollection<ExcelColumnAttribute> dynamicColumns)
{
if (dynamicColumns == null)
yield break;

int index = 0;
foreach (var g in dynamicColumns?.Where(x => !string.IsNullOrWhiteSpace(x.Format) && new ExcelNumberFormat(x.Format).IsValid).GroupBy(x => x.Format))
{
foreach (var col in g)
col.FormatId = startUpCellXfs + index;

yield return g.First();
index++;
}
}

}
}
39 changes: 19 additions & 20 deletions src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationTo
{
await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken);
await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken);
await GenerateStylesXmlAsync(cancellationToken);
}

private async Task CreateSheetXmlAsync(object value, string sheetPath, CancellationToken cancellationToken)
Expand All @@ -47,27 +48,27 @@ private async Task CreateSheetXmlAsync(object value, string sheetPath, Cancellat
if (value == null)
{
await WriteEmptySheetAsync(writer);
goto End; //for re-using code
}

//DapperRow

switch (value)
else
{
case IDataReader dataReader:
await GenerateSheetByIDataReaderAsync(writer, dataReader);
break;
case IEnumerable enumerable:
await GenerateSheetByEnumerableAsync(writer, enumerable);
break;
case DataTable dataTable:
await GenerateSheetByDataTableAsync(writer, dataTable);
break;
default:
throw new NotImplementedException($"Type {value.GetType().FullName} is not implemented. Please open an issue.");
//DapperRow

switch (value)
{
case IDataReader dataReader:
await GenerateSheetByIDataReaderAsync(writer, dataReader);
break;
case IEnumerable enumerable:
await GenerateSheetByEnumerableAsync(writer, enumerable);
break;
case DataTable dataTable:
await GenerateSheetByDataTableAsync(writer, dataTable);
break;
default:
throw new NotImplementedException($"Type {value.GetType().FullName} is not implemented. Please open an issue.");
}
}
}
End: //for re-using code
_zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, ExcelContentTypes.Worksheet));
}

Expand Down Expand Up @@ -455,8 +456,6 @@ private async Task GenerateEndXmlAsync(CancellationToken cancellationToken)
{
await AddFilesToZipAsync(cancellationToken);

await GenerateStylesXmlAsync(cancellationToken);

await GenerateDrawinRelXmlAsync(cancellationToken);

await GenerateDrawingXmlAsync(cancellationToken);
Expand All @@ -479,7 +478,7 @@ private async Task AddFilesToZipAsync(CancellationToken cancellationToken)
/// </summary>
private async Task GenerateStylesXmlAsync(CancellationToken cancellationToken)
{
var styleXml = GetStylesXml();
var styleXml = GetStylesXml(_configuration.DynamicColumns);

await CreateZipEntryAsync(
ExcelFileNames.Styles,
Expand Down
70 changes: 27 additions & 43 deletions src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MiniExcelLibs.OpenXml.Constants;
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml.Constants;
using MiniExcelLibs.OpenXml.Models;
using MiniExcelLibs.Utils;
using MiniExcelLibs.Zip;
Expand Down Expand Up @@ -108,6 +109,7 @@ private ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName
if (dynamicColumn.Format != null)
{
prop.ExcelFormat = dynamicColumn.Format;
prop.ExcelFormatId = dynamicColumn.FormatId;
}

if (dynamicColumn.Aliases != null)
Expand Down Expand Up @@ -158,14 +160,26 @@ private Tuple<string, string, string> GetCellValue(int rowIndex, int cellIndex,
return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(str));
}

if (columnInfo?.ExcelFormat != null && value is IFormattable formattableValue)
var type = GetValueType(value, columnInfo);


if (columnInfo?.ExcelFormat != null && columnInfo?.ExcelFormatId == -1 && value is IFormattable formattableValue)
{
var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture);
return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(formattedStr));
}

var type = GetValueType(value, columnInfo);
if (type == typeof(DateTime))
{
return GetDateTimeValue((DateTime)value, columnInfo);
}

#if NET6_0_OR_GREATER
if (type == typeof(DateOnly))
{
return GetDateTimeValue(((DateOnly)value).ToDateTime(new TimeOnly()), columnInfo);
}
#endif
if (type.IsEnum)
{
var description = CustomPropertyHelper.DescriptionAttr(type, value);
Expand Down Expand Up @@ -193,33 +207,6 @@ private Tuple<string, string, string> GetCellValue(int rowIndex, int cellIndex,
return Tuple.Create("4", "str", ExcelOpenXmlUtils.EncodeXML(base64));
}

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

#if NET6_0_OR_GREATER
if (type == typeof(DateOnly))
{
if (_configuration.Culture != CultureInfo.InvariantCulture)
{
var cellValue = ((DateOnly)value).ToString(_configuration.Culture);
return Tuple.Create("2", "str", cellValue);
}

if (columnInfo == null || columnInfo.ExcelFormat == null)
{
var oaDate = CorrectDateTimeValue((DateTime)value);
var cellValue = oaDate.ToString(CultureInfo.InvariantCulture);
return Tuple.Create<string, string, string>("3", null, cellValue);
}

// TODO: now it'll lose date type information
var formattedCellValue = ((DateOnly)value).ToString(columnInfo.ExcelFormat, _configuration.Culture);
return Tuple.Create("2", "str", formattedCellValue);
}
#endif

return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(value.ToString()));
}

Expand Down Expand Up @@ -325,24 +312,21 @@ private string GetFileValue(int rowIndex, int cellIndex, object value)
return base64;
}

private Tuple<string, string, string> GetDateTimeValue(object value, ExcelColumnInfo columnInfo)
private Tuple<string, string, string> GetDateTimeValue(DateTime value, ExcelColumnInfo columnInfo)
{
string cellValue = null;
if (_configuration.Culture != CultureInfo.InvariantCulture)
{
var cellValue = ((DateTime)value).ToString(_configuration.Culture);
cellValue = (value).ToString(_configuration.Culture);
return Tuple.Create("2", "str", cellValue);
}

var oaDate = CorrectDateTimeValue(value);
cellValue = oaDate.ToString(CultureInfo.InvariantCulture);
if (columnInfo == null || columnInfo.ExcelFormat == null)
{
var oaDate = CorrectDateTimeValue((DateTime)value);
var cellValue = oaDate.ToString(CultureInfo.InvariantCulture);
return Tuple.Create<string, string, string>("3", null, cellValue);
}

// TODO: now it'll lose date type information
var formattedCellValue = ((DateTime)value).ToString(columnInfo.ExcelFormat, _configuration.Culture);
return Tuple.Create("2", "str", formattedCellValue);
else
return Tuple.Create(columnInfo.ExcelFormatId.ToString(), (string)null, cellValue);
}

private static double CorrectDateTimeValue(DateTime value)
Expand Down Expand Up @@ -375,14 +359,14 @@ private string GetDimensionRef(int maxRowIndex, int maxColumnIndex)
return dimensionRef;
}

private string GetStylesXml()
private string GetStylesXml(ICollection<ExcelColumnAttribute> columns)
{
switch (_configuration.TableStyles)
{
case TableStyles.None:
return ExcelXml.NoneStylesXml;
return ExcelXml.SetupStyleXml(ExcelXml.NoneStylesXml, columns);
case TableStyles.Default:
return ExcelXml.DefaultStylesXml;
return ExcelXml.SetupStyleXml(ExcelXml.DefaultStylesXml, columns);
default:
return string.Empty;
}
Expand Down
Loading

0 comments on commit 00a445c

Please sign in to comment.