From 4182d162d4b1f4d061d7422adcf51589d48bc266 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sat, 10 Oct 2026 14:30:23 -0400 Subject: [PATCH 1/3] Add OpenXmlCellValue for per-cell formulas and styles on export Wrapping a value sets a formula, number format or font color for that cell alone. Cell formats now share one cache with column formats, which also gives the minimal style builder the numFmt offset it lacked when inserting into an existing workbook. --- .../Constants/WorksheetXml.cs | 10 ++ .../Models/OpenXmlCellValue.cs | 30 ++++ .../Builder/DefaultSheetStyleBuilder.cs | 155 ++++++++++------- .../Builder/MinimalSheetStyleBuilder.cs | 64 +++---- .../Styles/Builder/SheetStyleBuildContext.cs | 7 +- .../Styles/Builder/SheetStyleBuilderBase.cs | 67 +++++++- .../Styles/Builder/SheetStyleFormatsCache.cs | 80 +++++++-- .../Writer/OpenXmlWriter.XmlGeneration.cs | 4 +- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs | 91 ++++++++-- .../CellValues/OpenXmlCellValueAsyncTests.cs | 156 ++++++++++++++++++ .../CellValues/OpenXmlCellValueTests.cs | 31 ++++ 11 files changed, 566 insertions(+), 129 deletions(-) create mode 100644 src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs create mode 100644 tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueAsyncTests.cs create mode 100644 tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index d5f14f9e..b20f0fab 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -67,6 +67,16 @@ internal static string Cell(string cellReference, string? cellType, string style }; } + /// + /// A formula cell, with its cached result when is not null. + /// + internal static string FormulaCell(string cellReference, string? cellType, string styleIndex, string formula, string? cachedValue) + { + var type = cachedValue is null ? "" : $" t=\"{cellType}\""; + var value = cachedValue is null ? "" : $"{XmlHelper.EncodeXml(cachedValue)}"; + return $"""{XmlHelper.EncodeXml(formula)}{value}"""; + } + internal static string Autofilter(string dimensionRef) => $""; internal static string Drawing(int sheetIndex) => $""; } diff --git a/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs b/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs new file mode 100644 index 00000000..0ca6d116 --- /dev/null +++ b/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs @@ -0,0 +1,30 @@ +using System.Drawing; + +namespace MiniExcelLib.OpenXml.Models; + +/// +/// A value to export together with a formula or style of its own, overriding the column's settings for that cell. +/// +/// The value written to the cell. When is set, it is the cached result of the formula. +public sealed class OpenXmlCellValue(object? value) +{ + /// + /// The value written to the cell, or the cached result when is set. + /// + public object? Value { get; } = value; + + /// + /// A formula for the cell, without the leading equals sign. + /// + public string? Formula { get; set; } + + /// + /// A number format for the cell, replacing the column's format. + /// + public string? NumberFormat { get; set; } + + /// + /// The font color of the cell. + /// + public Color? FontColor { get; set; } +} diff --git a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs index aa6f6073..a93e1657 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs @@ -20,6 +20,20 @@ internal partial class DefaultSheetStyleBuilder(SheetStyleBuilderContext context private readonly OpenXmlStyleOptions _styleOptions = styleOptions; + private string CellHorizontalAlignment => _styleOptions.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }; + + private string CellVerticalAlignment => _styleOptions.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }; + protected internal override SheetStyleElementInfos GetGeneratedElementInfos() { return GeneratedElementInfos; @@ -28,20 +42,7 @@ protected internal override SheetStyleElementInfos GetGeneratedElementInfos() [CreateSyncVersion] protected override async Task GenerateNumFmtAsync() { - const int numFmtIndex = 166; - var index = 0; - foreach (var map in Context.SheetStyleFormatsCache.FormatMappings) - { - index++; - - /* - * "bottom" }; - var cellHorizontalAlignment = _styleOptions.HorizontalAlignment switch - { - HorizontalCellAlignment.Center => "center", - HorizontalCellAlignment.Right => "right", - _ => "general" - }; + var cellHorizontalAlignment = CellHorizontalAlignment; + var cellVerticalAlignment = CellVerticalAlignment; - var cellVerticalAlignment = _styleOptions.VerticalAlignment switch - { - VerticalCellAlignment.Top => "top", - VerticalCellAlignment.Center => "center", - _ => "bottom" - }; - /* Empty style is required because Excel always considers the first one to be the default and ignores all its properties * * */ @@ -529,42 +521,77 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteEndElementAsync().ConfigureAwait(false); await NewWriter.WriteEndElementAsync().ConfigureAwait(false); - const int numFmtIndex = 166; - for (var i = 1; i <= Context.CustomFormatCount; i++) - { - /* - * - * - * - */ - await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + i + Context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{Context.OldElementInfos.FontCount}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{Context.OldElementInfos.FillCount}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{Context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); - await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "relativeIndent", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "justifyLastLine", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "shrinkToFit", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "readingOrder", null, "0").ConfigureAwait(false); - await NewWriter.WriteEndElementAsync().ConfigureAwait(false); - await NewWriter.WriteStartElementAsync(OldReader.Prefix, "protection", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "locked", null, "1").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "hidden", null, "0").ConfigureAwait(false); - await NewWriter.WriteEndElementAsync().ConfigureAwait(false); - await NewWriter.WriteEndElementAsync().ConfigureAwait(false); - } + await GenerateCustomCellXfsAsync().ConfigureAwait(false); + } + + [CreateSyncVersion] + protected override async Task GenerateCustomFontAsync(string argb) + { + /* + * + * + * + * + * + * + * + */ + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "font", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "vertAlign", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "val", null, "baseline").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "sz", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "val", null, "11").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "color", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "rgb", null, argb).ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "name", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "val", null, "Calibri").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "family", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "val", null, "2").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + } + + [CreateSyncVersion] + protected override async Task GenerateCustomCellXfAsync(string numFmtId, string? fontId) + { + /* + * + * + * + */ + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, numFmtId).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, fontId ?? $"{Context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{Context.OldElementInfos.FillCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{Context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); + if (fontId is not null) + await NewWriter.WriteAttributeStringAsync(null, "applyFont", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, CellHorizontalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, CellVerticalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "relativeIndent", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "justifyLastLine", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "shrinkToFit", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "readingOrder", null, "0").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "protection", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "locked", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "hidden", null, "0").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } } diff --git a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs index 7afce315..41d77a07 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs @@ -20,20 +20,7 @@ protected internal override SheetStyleElementInfos GetGeneratedElementInfos() [CreateSyncVersion] protected override async Task GenerateNumFmtAsync() { - const int numFmtIndex = 166; - var index = 0; - foreach (var map in Context.SheetStyleFormatsCache.FormatMappings) - { - index++; - - /* - * - */ - await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmt", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + Context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, map.Format).ConfigureAwait(false); - await NewWriter.WriteEndElementAsync().ConfigureAwait(false); - } + await GenerateCustomNumFmtsAsync().ConfigureAwait(false); } [CreateSyncVersion] @@ -44,6 +31,8 @@ protected override async Task GenerateFontAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "font", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + + await GenerateCustomFontsAsync().ConfigureAwait(false); } [CreateSyncVersion] @@ -119,20 +108,37 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); await NewWriter.WriteEndElementAsync().ConfigureAwait(false); - const int numFmtIndex = 166; - for (var i = 1; i <= Context.CustomFormatCount; i++) - { - /* - * + */ + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "font", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "color", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "rgb", null, argb).ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + } + + [CreateSyncVersion] + protected override async Task GenerateCustomCellXfAsync(string numFmtId, string? fontId) + { + /* + * + */ + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, numFmtId).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, fontId ?? "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "borderId", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); + if (fontId is not null) + await NewWriter.WriteAttributeStringAsync(null, "applyFont", null, "1").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } } diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs index 35cd1826..00765c0a 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs @@ -28,7 +28,9 @@ internal sealed partial class SheetStyleBuilderContext(Dictionary SheetStyleFormatsCache.FormatMappingsCount; + public int CustomNumberFormatCount => SheetStyleFormatsCache.NumberFormats.Count; + public int CustomFontCount => SheetStyleFormatsCache.FontColors.Count; + public int CustomCellFormatCount => SheetStyleFormatsCache.CellFormats.Count; [CreateSyncVersion] public async Task CreateAsync(SheetStyleElementInfos generatedElementInfos, CancellationToken cancellationToken = default) @@ -115,6 +117,9 @@ public void UpdateFormatIds(ICollection mappings) { SheetStyleFormatsCache.AddMappings(mappings); } + + public int GetCellFormatIndex(string? numberFormat, int builtInNumberFormatId, System.Drawing.Color? fontColor) + => SheetStyleFormatsCache.GetCellFormatIndex(numberFormat, builtInNumberFormatId, fontColor); [CreateSyncVersion] public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancellationToken = default) diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs index 7984c02d..b4663253 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs @@ -106,12 +106,12 @@ protected virtual async Task WriteAttributesAsync(string element, CancellationTo { var value = element switch { - "numFmts" => (Context.OldElementInfos.NumFmtCount + Context.GeneratedElementInfos.NumFmtCount + Context.CustomFormatCount).ToString(), - "fonts" => (Context.OldElementInfos.FontCount + Context.GeneratedElementInfos.FontCount).ToString(), + "numFmts" => (Context.OldElementInfos.NumFmtCount + Context.GeneratedElementInfos.NumFmtCount + Context.CustomNumberFormatCount).ToString(), + "fonts" => (Context.OldElementInfos.FontCount + Context.GeneratedElementInfos.FontCount + Context.CustomFontCount).ToString(), "fills" => (Context.OldElementInfos.FillCount + Context.GeneratedElementInfos.FillCount).ToString(), "borders" => (Context.OldElementInfos.BorderCount + Context.GeneratedElementInfos.BorderCount).ToString(), "cellStyleXfs" => (Context.OldElementInfos.CellStyleXfCount + Context.GeneratedElementInfos.CellStyleXfCount).ToString(), - "cellXfs" => (Context.OldElementInfos.CellXfCount + Context.GeneratedElementInfos.CellXfCount + Context.CustomFormatCount).ToString(), + "cellXfs" => (Context.OldElementInfos.CellXfCount + Context.GeneratedElementInfos.CellXfCount + Context.CustomCellFormatCount).ToString(), _ => OldReader.Value }; await NewWriter.WriteStringAsync(value).ConfigureAwait(false); @@ -198,7 +198,7 @@ protected virtual async Task GenerateElementBeforEndElementAsync() protected virtual async Task GenerateNumFmtsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmts", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (Context.OldElementInfos.NumFmtCount + Context.GeneratedElementInfos.NumFmtCount + Context.CustomFormatCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (Context.OldElementInfos.NumFmtCount + Context.GeneratedElementInfos.NumFmtCount + Context.CustomNumberFormatCount).ToString()).ConfigureAwait(false); await GenerateNumFmtAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -215,7 +215,7 @@ protected virtual async Task GenerateNumFmtsAsync() protected virtual async Task GenerateFontsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "fonts", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (Context.OldElementInfos.FontCount + Context.GeneratedElementInfos.FontCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (Context.OldElementInfos.FontCount + Context.GeneratedElementInfos.FontCount + Context.CustomFontCount).ToString()).ConfigureAwait(false); await GenerateFontAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -283,11 +283,66 @@ protected virtual async Task GenerateCellStyleXfsAsync() protected virtual async Task GenerateCellXfsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "cellXfs", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (Context.OldElementInfos.CellXfCount + Context.GeneratedElementInfos.CellXfCount + Context.CustomFormatCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (Context.OldElementInfos.CellXfCount + Context.GeneratedElementInfos.CellXfCount + Context.CustomCellFormatCount).ToString()).ConfigureAwait(false); await GenerateCellXfAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); } [CreateSyncVersion] protected abstract Task GenerateCellXfAsync(); + + // Custom number formats get numFmtIds from 167 upwards, after any the workbook already had + private string GetCustomNumberFormatId(int position) => (167 + position + Context.OldElementInfos.NumFmtCount).ToString(); + + [CreateSyncVersion] + protected async Task GenerateCustomNumFmtsAsync() + { + var numberFormats = Context.SheetStyleFormatsCache.NumberFormats; + for (var i = 0; i < numberFormats.Count; i++) + { + /* + * + */ + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmt", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, GetCustomNumberFormatId(i)).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, numberFormats[i]).ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + } + } + + [CreateSyncVersion] + protected async Task GenerateCustomFontsAsync() + { + foreach (var color in Context.SheetStyleFormatsCache.FontColors) + { + await GenerateCustomFontAsync($"{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}").ConfigureAwait(false); + } + } + + /// + /// Writes a font element in the given ARGB color, otherwise like the builder's default font. + /// + [CreateSyncVersion] + protected abstract Task GenerateCustomFontAsync(string argb); + + [CreateSyncVersion] + protected async Task GenerateCustomCellXfsAsync() + { + var firstCustomFontId = Context.OldElementInfos.FontCount + Context.GeneratedElementInfos.FontCount; + foreach (var format in Context.SheetStyleFormatsCache.CellFormats) + { + var numFmtId = format.CustomNumberFormat is { } position + ? GetCustomNumberFormatId(position) + : format.BuiltInNumberFormatId.ToString(); + var fontId = format.FontColor is { } fontColor ? (firstCustomFontId + fontColor).ToString() : null; + + await GenerateCustomCellXfAsync(numFmtId, fontId).ConfigureAwait(false); + } + } + + /// + /// Writes an xf element for a regular cell with the given number format and, when not null, font. + /// + [CreateSyncVersion] + protected abstract Task GenerateCustomCellXfAsync(string numFmtId, string? fontId); } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs index 2feb550b..3ec55603 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs @@ -1,30 +1,82 @@ +using System.Drawing; +// A cell format added on top of the builder's own. CustomNumberFormat and FontColor are positions in +// NumberFormats and FontColors, or null; BuiltInNumberFormatId applies when there is no custom number format. +using CustomCellFormat = (int? CustomNumberFormat, int BuiltInNumberFormatId, int? FontColor); + namespace MiniExcelLib.OpenXml.Styles.Builder; // todo: find a way to make it compatible with SharedStringsDiskCache internal class SheetStyleFormatsCache { - private readonly Dictionary _formatMappings = []; - private int _stylesCount; - - internal int FormatMappingsCount => _formatMappings.Count; - internal IEnumerable<(string Format, int FormatId)> FormatMappings => _formatMappings.Select(x => (x.Key, x.Value)); - + private readonly Dictionary _numberFormatIndexes = []; + private readonly List _numberFormats = []; + private readonly Dictionary _fontColorIndexes = []; + private readonly List _fontColors = []; + private readonly Dictionary _cellFormatIndexes = []; + private readonly List _cellFormats = []; + private int _firstCellFormatIndex; + + internal IReadOnlyList NumberFormats => _numberFormats; + internal IReadOnlyList FontColors => _fontColors; + internal IReadOnlyList CellFormats => _cellFormats; + public void AddMappings(IEnumerable mappings) { foreach (var mapping in mappings.Where(map => map is { ExcelIgnoreColumn: false })) { if (!string.IsNullOrWhiteSpace(mapping!.ExcelFormat) && new OpenXmlNumberFormatHelper(mapping.ExcelFormat).IsValid) { - if (!_formatMappings.TryGetValue(mapping.ExcelFormat, out var formatId)) - { - formatId = _stylesCount++; - _formatMappings.Add(mapping.ExcelFormat, formatId); - } + mapping.SetFormatId(GetCellFormatIndex(mapping.ExcelFormat, 0, null)); + } + } + } - mapping.SetFormatId(formatId); + /// + /// Gets the cellXfs index of a format, adding the format when it is new. + /// + /// A valid custom number format, or null. + /// The built-in numFmtId used when is null. + /// The font color, or null for the default font. + internal int GetCellFormatIndex(string? numberFormat, int builtInNumberFormatId, Color? fontColor) + { + int? customNumberFormat = null; + if (numberFormat is not null) + { + if (!_numberFormatIndexes.TryGetValue(numberFormat, out var position)) + { + position = _numberFormats.Count; + _numberFormats.Add(numberFormat); + _numberFormatIndexes.Add(numberFormat, position); } + + customNumberFormat = position; + builtInNumberFormatId = 0; } + + int? fontColorPosition = null; + if (fontColor is { } color) + { + var argb = color.ToArgb(); + if (!_fontColorIndexes.TryGetValue(argb, out var position)) + { + position = _fontColors.Count; + _fontColors.Add(color); + _fontColorIndexes.Add(argb, position); + } + + fontColorPosition = position; + } + + CustomCellFormat format = (customNumberFormat, builtInNumberFormatId, fontColorPosition); + if (!_cellFormatIndexes.TryGetValue(format, out var index)) + { + index = _firstCellFormatIndex + _cellFormats.Count; + _cellFormats.Add(format); + _cellFormatIndexes.Add(format, index); + } + + return index; } - - internal void SetCurrentIndex(int index) => _stylesCount = index; + + internal void SetCurrentIndex(int index) => _firstCellFormatIndex = index; } diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs index 29e3aa00..ae476fd5 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs @@ -157,7 +157,7 @@ private string GetPanes() return sb.ToString(); } - private (string StyleIndex, string DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull) + private (string StyleIndex, string DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull, bool useRuntimeType = false) { if (valueIsNull) return (RegularCellStyleIndex, GetStringType(), string.Empty); @@ -168,7 +168,7 @@ private string GetPanes() return (styleIndex, GetStringType(), str); } - var type = GetValueType(value, columnMapping); + var type = useRuntimeType ? Nullable.GetUnderlyingType(value.GetType()) ?? value.GetType() : GetValueType(value, columnMapping); if (columnMapping is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index 2c7aeaff..c55422e8 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -440,6 +440,12 @@ private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List ExportAsync(object value, OpenXmlConfiguration? configuration = null) + { + var stream = new MemoryStream(); + await _exporter.ExportAsync(stream, value, configuration: configuration); + stream.Position = 0; + return new XLWorkbook(stream); + } + + [Fact] + public async Task FormulaIsWrittenWithItsCachedResultAsync() + { + Dictionary[] rows = + [ + new() { ["A"] = 3, ["B"] = new OpenXmlCellValue(6) { Formula = "A2*2" }, ["C"] = new OpenXmlCellValue("x3") { Formula = "=\"x\"&A2" } } + ]; + + using var workbook = await ExportAsync(rows); + var sheet = workbook.Worksheet(1); + + Assert.Equal("A2*2", sheet.Cell("B2").FormulaA1); + Assert.Equal(6, sheet.Cell("B2").CachedValue.GetNumber()); + Assert.Equal("\"x\"&A2", sheet.Cell("C2").FormulaA1); + Assert.Equal("x3", sheet.Cell("C2").CachedValue.GetText()); + Assert.False(sheet.Cell("A2").HasFormula); + } + + [Fact] + public async Task FontColorAndNumberFormatApplyToTheCellOnlyAsync() + { + Dictionary[] rows = + [ + new() { ["Value"] = new OpenXmlCellValue(0.25) { NumberFormat = "0.0%", FontColor = Color.Red } }, + new() { ["Value"] = new OpenXmlCellValue("grey") { FontColor = Color.Gray } }, + new() { ["Value"] = 1.5 } + ]; + + using var workbook = await ExportAsync(rows); + var sheet = workbook.Worksheet(1); + + Assert.Equal(0.25, sheet.Cell("A2").GetDouble()); + Assert.Equal("0.0%", sheet.Cell("A2").Style.NumberFormat.Format); + Assert.Equal(Color.Red.ToArgb(), sheet.Cell("A2").Style.Font.FontColor.Color.ToArgb()); + Assert.Equal("grey", sheet.Cell("A3").GetString()); + Assert.Equal(Color.Gray.ToArgb(), sheet.Cell("A3").Style.Font.FontColor.Color.ToArgb()); + Assert.Equal(1.5, sheet.Cell("A4").GetDouble()); + Assert.NotEqual(Color.Red.ToArgb(), sheet.Cell("A4").Style.Font.FontColor.Color.ToArgb()); + } + + [Fact] + public async Task FontColorKeepsTheColumnFormatAsync() + { + Dictionary[] rows = + [ + new() { ["Amount"] = new OpenXmlCellValue(1.23456) { FontColor = Color.Blue } }, + new() { ["Amount"] = 2.5 } + ]; + var configuration = new OpenXmlConfiguration { DynamicColumns = [new DynamicExcelColumn("Amount") { Format = "0.000" }] }; + + using var workbook = await ExportAsync(rows, configuration); + var sheet = workbook.Worksheet(1); + + Assert.Equal(1.23456, sheet.Cell("A2").GetDouble()); + Assert.Equal("0.000", sheet.Cell("A2").Style.NumberFormat.Format); + Assert.Equal(Color.Blue.ToArgb(), sheet.Cell("A2").Style.Font.FontColor.Color.ToArgb()); + Assert.Equal("0.000", sheet.Cell("A3").Style.NumberFormat.Format); + } + + [Fact] + public async Task WrappedValuesInTypedRowsUseTheirOwnTypeAsync() + { + FormattedRow[] rows = + [ + new() { Amount = new OpenXmlCellValue(1.23456) { FontColor = Color.Blue }, When = new OpenXmlCellValue(new DateTime(2024, 5, 6)) { FontColor = Color.Blue } } + ]; + + using var workbook = await ExportAsync(rows); + var sheet = workbook.Worksheet(1); + + Assert.Equal(1.23456, sheet.Cell("A2").GetDouble()); + Assert.Equal("0.000", sheet.Cell("A2").Style.NumberFormat.Format); + Assert.Equal(new DateTime(2024, 5, 6), sheet.Cell("B2").GetDateTime()); + Assert.Equal(14, sheet.Cell("B2").Style.NumberFormat.NumberFormatId); + Assert.Equal(Color.Blue.ToArgb(), sheet.Cell("B2").Style.Font.FontColor.Color.ToArgb()); + } + + [Fact] + public async Task CellsWithTheSameStyleShareOneFormatAsync() + { + var rows = Enumerable.Range(1, 50) + .Select(i => new Dictionary { ["Value"] = new OpenXmlCellValue(i) { NumberFormat = "0.00", FontColor = Color.Gray } }) + .ToArray(); + + var stream = new MemoryStream(); + await _exporter.ExportAsync(stream, rows); + stream.Position = 0; + + using var archive = new ZipArchive(stream); + using var reader = new StreamReader(archive.GetEntry("xl/styles.xml")!.Open()); + var styles = XDocument.Parse(await reader.ReadToEndAsync()); + XNamespace ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"; + + Assert.Single(styles.Descendants(ns + "numFmt"), e => (string?)e.Attribute("formatCode") == "0.00"); + Assert.Single(styles.Descendants(ns + "font"), e => (string?)e.Element(ns + "color")?.Attribute("rgb") == "FF808080"); + } + + [Fact] + public async Task NullValueKeepsItsStyleAsync() + { + Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(null) { FontColor = Color.Red } }]; + + using var workbook = await ExportAsync(rows); + var cell = workbook.Worksheet(1).Cell("A2"); + + Assert.True(cell.IsEmpty()); + Assert.Equal(Color.Red.ToArgb(), cell.Style.Font.FontColor.Color.ToArgb()); + } + + [Fact] + public async Task MinimalStylesSupportCellValuesAsync() + { + Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(0.5) { NumberFormat = "0%", FontColor = Color.Green } }]; + + using var workbook = await ExportAsync(rows, new OpenXmlConfiguration { TableStyles = TableStyles.None }); + var cell = workbook.Worksheet(1).Cell("A2"); + + Assert.Equal("0%", cell.Style.NumberFormat.Format); + Assert.Equal(Color.Green.ToArgb(), cell.Style.Font.FontColor.Color.ToArgb()); + } + + [Fact] + public async Task InvalidNumberFormatThrowsAsync() + { + Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(1) { NumberFormat = "0.0\"" } }]; + + await Assert.ThrowsAsync(() => _exporter.ExportAsync(new MemoryStream(), rows)); + } +} diff --git a/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs b/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs new file mode 100644 index 00000000..729319f1 --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs @@ -0,0 +1,31 @@ +using System.Drawing; +using ClosedXML.Excel; +using MiniExcelLib.OpenXml.Models; + +namespace MiniExcelLib.OpenXml.Tests.CellValues; + +public class OpenXmlCellValueTests +{ + private readonly OpenXmlExporter _exporter = MiniExcelV2.Exporters.GetOpenXmlExporter(); + + [Fact] + public void FormulaFontColorAndNumberFormatAreWritten() + { + Dictionary[] rows = + [ + new() { ["A"] = 3, ["B"] = new OpenXmlCellValue(0.6) { Formula = "A2/5", NumberFormat = "0.0%", FontColor = Color.Gray } } + ]; + + var stream = new MemoryStream(); + _exporter.Export(stream, rows); + stream.Position = 0; + + using var workbook = new XLWorkbook(stream); + var cell = workbook.Worksheet(1).Cell("B2"); + + Assert.Equal("A2/5", cell.FormulaA1); + Assert.Equal(0.6, cell.CachedValue.GetNumber()); + Assert.Equal("0.0%", cell.Style.NumberFormat.Format); + Assert.Equal(Color.Gray.ToArgb(), cell.Style.Font.FontColor.Color.ToArgb()); + } +} From e73c2da16972a3594154f8186af347e2e9535dea Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sun, 11 Oct 2026 08:15:18 -0400 Subject: [PATCH 2/3] Style OpenXmlCellValue through OpenXmlCellStyle Shares the style type with the editor, so properties added to it later apply to exported cells as well. --- src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs | 6 +++--- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs | 4 ++-- .../CellValues/OpenXmlCellValueAsyncTests.cs | 14 +++++++------- .../CellValues/OpenXmlCellValueTests.cs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs b/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs index 0ca6d116..a96d12d6 100644 --- a/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs +++ b/src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using MiniExcelLib.OpenXml.Styles; namespace MiniExcelLib.OpenXml.Models; @@ -24,7 +24,7 @@ public sealed class OpenXmlCellValue(object? value) public string? NumberFormat { get; set; } /// - /// The font color of the cell. + /// The style of the cell, such as its font color. /// - public Color? FontColor { get; set; } + public OpenXmlCellStyle Style { get; } = new(); } diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index c55422e8..76ea51a0 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -496,7 +496,7 @@ private async Task WriteCellValueOverrideAsync(MiniExcelStreamWriter writer, str ? (DefaultCellStyleIndex, null, null) : GetCellValue(rowIndex, cellIndex, value!, numberFormat is null ? columnMapping : null, false, useRuntimeType: true); - if (numberFormat is not null || cell.FontColor is not null) + if (numberFormat is not null || cell.Style.FontColor is not null) { // Without a number format of its own, the cell keeps the one it would have had var builtInNumberFormatId = 0; @@ -510,7 +510,7 @@ private async Task WriteCellValueOverrideAsync(MiniExcelStreamWriter writer, str builtInNumberFormatId = 21; } - styleIndex = _sheetStyleBuilderContext.GetCellFormatIndex(numberFormat, builtInNumberFormatId, cell.FontColor).ToString(); + styleIndex = _sheetStyleBuilderContext.GetCellFormatIndex(numberFormat, builtInNumberFormatId, cell.Style.FontColor).ToString(); } if (cellValue is not null) diff --git a/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueAsyncTests.cs index ab3e3348..37700a6d 100644 --- a/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueAsyncTests.cs @@ -48,8 +48,8 @@ public async Task FontColorAndNumberFormatApplyToTheCellOnlyAsync() { Dictionary[] rows = [ - new() { ["Value"] = new OpenXmlCellValue(0.25) { NumberFormat = "0.0%", FontColor = Color.Red } }, - new() { ["Value"] = new OpenXmlCellValue("grey") { FontColor = Color.Gray } }, + new() { ["Value"] = new OpenXmlCellValue(0.25) { NumberFormat = "0.0%", Style = { FontColor = Color.Red } } }, + new() { ["Value"] = new OpenXmlCellValue("grey") { Style = { FontColor = Color.Gray } } }, new() { ["Value"] = 1.5 } ]; @@ -70,7 +70,7 @@ public async Task FontColorKeepsTheColumnFormatAsync() { Dictionary[] rows = [ - new() { ["Amount"] = new OpenXmlCellValue(1.23456) { FontColor = Color.Blue } }, + new() { ["Amount"] = new OpenXmlCellValue(1.23456) { Style = { FontColor = Color.Blue } } }, new() { ["Amount"] = 2.5 } ]; var configuration = new OpenXmlConfiguration { DynamicColumns = [new DynamicExcelColumn("Amount") { Format = "0.000" }] }; @@ -89,7 +89,7 @@ public async Task WrappedValuesInTypedRowsUseTheirOwnTypeAsync() { FormattedRow[] rows = [ - new() { Amount = new OpenXmlCellValue(1.23456) { FontColor = Color.Blue }, When = new OpenXmlCellValue(new DateTime(2024, 5, 6)) { FontColor = Color.Blue } } + new() { Amount = new OpenXmlCellValue(1.23456) { Style = { FontColor = Color.Blue } }, When = new OpenXmlCellValue(new DateTime(2024, 5, 6)) { Style = { FontColor = Color.Blue } } } ]; using var workbook = await ExportAsync(rows); @@ -106,7 +106,7 @@ public async Task WrappedValuesInTypedRowsUseTheirOwnTypeAsync() public async Task CellsWithTheSameStyleShareOneFormatAsync() { var rows = Enumerable.Range(1, 50) - .Select(i => new Dictionary { ["Value"] = new OpenXmlCellValue(i) { NumberFormat = "0.00", FontColor = Color.Gray } }) + .Select(i => new Dictionary { ["Value"] = new OpenXmlCellValue(i) { NumberFormat = "0.00", Style = { FontColor = Color.Gray } } }) .ToArray(); var stream = new MemoryStream(); @@ -125,7 +125,7 @@ public async Task CellsWithTheSameStyleShareOneFormatAsync() [Fact] public async Task NullValueKeepsItsStyleAsync() { - Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(null) { FontColor = Color.Red } }]; + Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(null) { Style = { FontColor = Color.Red } } }]; using var workbook = await ExportAsync(rows); var cell = workbook.Worksheet(1).Cell("A2"); @@ -137,7 +137,7 @@ public async Task NullValueKeepsItsStyleAsync() [Fact] public async Task MinimalStylesSupportCellValuesAsync() { - Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(0.5) { NumberFormat = "0%", FontColor = Color.Green } }]; + Dictionary[] rows = [new() { ["Value"] = new OpenXmlCellValue(0.5) { NumberFormat = "0%", Style = { FontColor = Color.Green } } }]; using var workbook = await ExportAsync(rows, new OpenXmlConfiguration { TableStyles = TableStyles.None }); var cell = workbook.Worksheet(1).Cell("A2"); diff --git a/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs b/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs index 729319f1..c0fb71e5 100644 --- a/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/CellValues/OpenXmlCellValueTests.cs @@ -13,7 +13,7 @@ public void FormulaFontColorAndNumberFormatAreWritten() { Dictionary[] rows = [ - new() { ["A"] = 3, ["B"] = new OpenXmlCellValue(0.6) { Formula = "A2/5", NumberFormat = "0.0%", FontColor = Color.Gray } } + new() { ["A"] = 3, ["B"] = new OpenXmlCellValue(0.6) { Formula = "A2/5", NumberFormat = "0.0%", Style = { FontColor = Color.Gray } } } ]; var stream = new MemoryStream(); From 6db7accddebad4d8f22fb43061a4ac76d4054c63 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Sun, 11 Oct 2026 08:18:19 -0400 Subject: [PATCH 3/3] Document OpenXmlCellValue in README_V2 --- README_V2.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README_V2.md b/README_V2.md index 8402a915..f12b6f86 100644 --- a/README_V2.md +++ b/README_V2.md @@ -745,6 +745,28 @@ exporter.Export("Book1.xlsx", dt, configuration: config); ![image](docs/images/freeze-pane-1.png) +#### 14. Per-cell formulas and formatting + +Wrapping a value in `OpenXmlCellValue` sets a formula, number format or style for that one cell, overriding the column's settings: + +```csharp +var rows = new[] +{ + new Dictionary + { + ["Input"] = 3, + ["Share"] = new OpenXmlCellValue(0.25) { NumberFormat = "0.0%", Style = { FontColor = Color.Red } }, + ["Double"] = new OpenXmlCellValue(6) { Formula = "A2*2" } + } +}; + +var exporter = MiniExcelV2.Exporters.GetOpenXmlExporter(); +exporter.Export("Book1.xlsx", rows); +``` + +The value given with a formula is written as its cached result, which Excel shows until it recalculates. Pass `null` to leave the result for Excel to calculate. +A cell with only a style keeps the number format it would otherwise have had. + ### Fill Data To Excel Template