Skip to content

Commit aab8627

Browse files
swmalswmal
andauthored
Formula Calc improvements (#2047)
* Added validation for numberformats (brackets) during calc. Fix for REF errors in OFFSET function. * Fixed wrong data type being returned from Bin2Hex function (Decimal->String). Small adjustments of numberformat validation * Fixed small merge error --------- Co-authored-by: swmal <{ID}+username}@users.noreply.github.com>
1 parent 36785ef commit aab8627

12 files changed

Lines changed: 231 additions & 10 deletions

File tree

src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,13 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn
744744
}
745745
else
746746
{
747-
f._ws.SetValueInner(f._row, f._column, cr.ResultValue ?? 0D);
747+
var dVal = cr.ResultValue;
748+
if (cr.DataType == DataType.Decimal && dVal != null && dVal is double && cr.ResultNumeric == 0d)
749+
{
750+
// this is to avoid "-0" results from the ToString method.
751+
dVal = 0d;
752+
}
753+
f._ws.SetValueInner(f._row, f._column, dVal ?? 0D);
748754
}
749755
}
750756
}

src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ public override object GetRangeValue(string worksheetName, int row, int column)
615615
SetCurrentWorksheet(worksheetName);
616616
return _currentWorksheet.GetValue(row, column);
617617
}
618-
public override string GetFormat(object value, string format)
618+
public override string GetFormat(object value, string format, out bool isValidFormat)
619619
{
620620
if (_workbook.NumberFormatToTextHandler == null)
621621
{
@@ -634,10 +634,12 @@ public override string GetFormat(object value, string format)
634634
ft = new ExcelFormatTranslator(format, -1);
635635
}
636636

637-
return ValueToTextHandler.FormatValue(value, false, ft, null);
637+
var frmt = ValueToTextHandler.FormatValue(value, false, ft, null, out isValidFormat);
638+
return frmt;
638639
}
639640
else
640641
{
642+
isValidFormat = true;
641643
var arg = new NumberFormatToTextArgs(_currentWorksheet, _context.CurrentCell.Row, _context.CurrentCell.Column, value, _currentWorksheet.GetStyleInner(_context.CurrentCell.Row, _context.CurrentCell.Column));
642644
return _workbook.NumberFormatToTextHandler(arg);
643645
}

src/EPPlus/FormulaParsing/Excel/Functions/Engineering/Bin2Hex.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
4242
if (number.Length < 10)
4343
{
4444
var n = Convert.ToInt32(number, 2);
45-
return CreateResult(n.ToString(formatString), DataType.Decimal);
45+
return CreateResult(n.ToString(formatString), DataType.String);
4646
}
4747
else
4848
{

src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Offset.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
5656
var toRow = (height != 0 ? adr.FromRow + height - 1 : adr.ToRow) + (int)rowOffset;
5757
var toCol = (width != 0 ? adr.FromCol + width - 1 : adr.ToCol) + (int)colOffset;
5858

59+
if(fromRow < 1 || fromCol < 1 || toRow > ExcelPackage.MaxRows || toCol > ExcelPackage.MaxColumns)
60+
{
61+
return CompileResult.GetErrorResult(eErrorType.Ref);
62+
}
5963
var newRange = context.ExcelDataProvider.GetRange(adr.WorksheetName, fromRow, fromCol, toRow, toCol);
6064

6165
return CreateAddressResult(newRange, DataType.ExcelRange);

src/EPPlus/FormulaParsing/Excel/Functions/Text/Text.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
5050
var format = ArgToString(arguments, 1);
5151
var invariantFormat = GetInvariantFormat(format);
5252

53-
var result = context.ExcelDataProvider.GetFormat(value, invariantFormat);
53+
var result = context.ExcelDataProvider.GetFormat(value, invariantFormat, out bool isValidFormat);
54+
if(!isValidFormat)
55+
{
56+
return CreateResult(eErrorType.Value);
57+
}
5458

5559
return CreateResult(result, DataType.String);
5660
}

src/EPPlus/FormulaParsing/ExcelDataProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ internal ExcelDataProvider() { }
138138
public abstract int ExcelMaxRows { get; }
139139

140140
public abstract object GetRangeValue(string worksheetName, int row, int column);
141-
public abstract string GetFormat(object value, string format);
141+
public abstract string GetFormat(object value, string format, out bool isValidFormat);
142142

143143
public abstract void Reset();
144144
public abstract IRangeInfo GetRange(string worksheet, int fromRow, int fromCol, int toRow, int toCol);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
06/27/2025 EPPlus Software AB Improved format handling
12+
*************************************************************************************************/
13+
using System;
14+
using System.Collections.Generic;
15+
using System.Linq;
16+
using System.Text;
17+
18+
namespace OfficeOpenXml.Style.XmlAccess
19+
{
20+
internal static class BracketTextValidator
21+
{
22+
private static HashSet<string> _colors = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) { "Black", "Green", "White", "Blue", "Magenta", "Yellow", "Cyan", "Red" };
23+
24+
private static HashSet<char> _operators = new HashSet<char> { '>', '<', '=' };
25+
26+
public static bool IsValid(string text)
27+
{
28+
if(string.IsNullOrEmpty(text)) return false;
29+
if (_colors.Contains(text)) return true;
30+
if (text.StartsWith("$")) return true;
31+
if(IsCondition(text)) return true;
32+
// indexed colors
33+
if (text.ToLowerInvariant().StartsWith("color") && int.TryParse(text.Substring(5), out int n) && n >= 1 && n <= 56) return true;
34+
return false;
35+
}
36+
37+
public static bool IsCondition(string text)
38+
{
39+
40+
if (string.IsNullOrEmpty(text)) return false;
41+
42+
text = text.Replace(" ", "");
43+
44+
int i = 0;
45+
if (text.StartsWith(">=")) i = 2;
46+
else if (text.StartsWith("<=")) i = 2;
47+
else if (text.StartsWith(">") || text.StartsWith("<") || text.StartsWith("=")) i = 1;
48+
else return false;
49+
50+
if (i >= text.Length) return false;
51+
52+
bool isValidNumber = false;
53+
bool hasDecimal = false;
54+
55+
if (text[i] == '-') i++; // tillåt negativt tal
56+
57+
for (; i < text.Length; i++)
58+
{
59+
char c = text[i];
60+
if (char.IsDigit(c))
61+
{
62+
isValidNumber = true;
63+
}
64+
else if (c == '.' && !hasDecimal)
65+
{
66+
hasDecimal = true;
67+
}
68+
else
69+
{
70+
return false;
71+
}
72+
}
73+
74+
return isValidNumber;
75+
76+
}
77+
}
78+
}

src/EPPlus/Style/XmlAccess/ExcelFormatTranslator.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ internal class FormatPart
3838
internal string NetFormat { get; set; }
3939
internal string NetFormatForWidth { get; set; }
4040
internal string FractionFormat { get; set; }
41+
42+
internal bool IsValid { get; set; } = true;
43+
4144
internal eSystemDateFormat SpecialDateFormat { get; set; }
4245
internal bool ContainsTextPlaceholder { get; set; } = false;
4346
internal void SetFormat(string format, bool containsAmPm, bool forColWidth)
@@ -76,7 +79,11 @@ internal ExcelFormatTranslator(string format, int numFmtID)
7679
else
7780
{
7881
ToNetFormat(format, false);
79-
ToNetFormat(format, true);
82+
if(f.IsValid)
83+
{
84+
ToNetFormat(format, true);
85+
}
86+
8087
}
8188
}
8289

@@ -248,6 +255,12 @@ private void ToNetFormat(string ExcelFormat, bool forColWidth)
248255
bracketText.StartsWith("=")) //Conditional
249256
{
250257
f.SpecialDateFormat = eSystemDateFormat.Conditional;
258+
var isValidFormat = BracketTextValidator.IsValid(bracketText);
259+
if(!isValidFormat)
260+
{
261+
sb.Append($"[{bracketText}]");
262+
f.IsValid = false;
263+
}
251264
}
252265
else if (bracketText.ContainsOnlyCharacter('h'))
253266
{
@@ -275,7 +288,16 @@ private void ToNetFormat(string ExcelFormat, bool forColWidth)
275288
}
276289
else
277290
{
278-
sb.Append(bracketText);
291+
var isValidFormat = BracketTextValidator.IsValid(bracketText);
292+
if(!isValidFormat)
293+
{
294+
sb.Append($"[{bracketText}]");
295+
f.IsValid = false;
296+
}
297+
else
298+
{
299+
sb.Append(bracketText);
300+
}
279301
}
280302
f.SpecialDateFormat = eSystemDateFormat.Conditional;
281303
}

src/EPPlus/Utils/ValueToTextHandler.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal static string GetFormattedText(object Value, ExcelWorkbook wb, int styl
3131
var styles = wb.Styles;
3232
ExcelFormatTranslator nf = GetNumberFormat(styleId, styles).FormatTranslator;
3333

34-
return FormatValue(v, forWidthCalc, nf, cultureInfo);
34+
return FormatValue(v, forWidthCalc, nf, cultureInfo, out bool isValid);
3535
}
3636

3737
internal static ExcelNumberFormatXml GetNumberFormat(int styleId, ExcelStyles styles)
@@ -72,10 +72,15 @@ internal static ExcelNumberFormatXml GetDxfNumberFormat(int styleId, ExcelStyles
7272
return null;
7373
}
7474

75-
internal static string FormatValue(object v, bool forWidthCalc, ExcelFormatTranslator nf, CultureInfo overrideCultureInfo)
75+
internal static string FormatValue(object v, bool forWidthCalc, ExcelFormatTranslator nf, CultureInfo overrideCultureInfo, out bool isValidFormat)
7676
{
7777
if (v == null) v = 0;
7878
var f = nf.GetFormatPart(v);
79+
isValidFormat = f.IsValid;
80+
if(isValidFormat == false)
81+
{
82+
return null;
83+
}
7984
string format;
8085
if (forWidthCalc)
8186
{

src/EPPlusTest/EPPlus.Test.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@
274274
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
275275
</None>
276276
</ItemGroup>
277+
<ItemGroup>
278+
<Folder Include="Style\XmlAccess\" />
279+
</ItemGroup>
277280
<PropertyGroup>
278281
<SsdtUnitTestVersion>3.1</SsdtUnitTestVersion>
279282
</PropertyGroup>

0 commit comments

Comments
 (0)