Skip to content

Commit 0092bcd

Browse files
karlkallmanswmal
andauthored
Feature/trimfunctions (#2357)
* WIP * WIP * WIP * Added support for trim-operators and TRIMRANGE function. * Implemented trim-operators and trimrange function. Added improved tests * Added test workbook * Fixed spelling in file names, order in asserts and check Worksheet.Dimention before loop in TrimRangeCore. * Changed descriptions in FunctionMetadata attribute, added unit test for cross worksheet references --------- Co-authored-by: swmal <897655+swmal@users.noreply.github.com>
1 parent cd1fe30 commit 0092bcd

12 files changed

Lines changed: 674 additions & 0 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,10 @@ public BuiltInFunctions()
353353
Functions["indirect"] = new Indirect();
354354
Functions["offset"] = new Offset();
355355
Functions["transpose"] = new Transpose();
356+
Functions["trimrange"] = new TrimRange();
357+
Functions["_tro_all"] = new TroAll();
358+
Functions["_tro_leading"] = new TroLeading();
359+
Functions["_tro_trailing"] = new TroTrailing();
356360
Functions["filter"] = new FilterFunction();
357361
Functions["sort"] = new SortFunction();
358362
Functions["sortby"] = new SortBy();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions
14+
{
15+
internal enum TrimCols
16+
{
17+
None = 0,
18+
TrimLeading = 1,
19+
TrimTrailing = 2,
20+
TrimAll = 3
21+
}
22+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
2+
using OfficeOpenXml.FormulaParsing.Ranges;
3+
using System;
4+
using System.Collections.Generic;
5+
6+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions
7+
{
8+
internal abstract class TrimFunctionsBase : ExcelFunction
9+
{
10+
public override int ArgumentMinLength => 1;
11+
public override string NamespacePrefix => "_xlfn.";
12+
13+
protected CompileResult ExecuteTrim(IList<FunctionArgument> arguments, TrimMode rowMode, TrimMode colMode, ParsingContext context)
14+
{
15+
var range = arguments[0].ValueAsRangeInfo;
16+
var result = TrimRangeCore(range, rowMode, colMode, context, out var error);
17+
if (error != null) return error;
18+
return CreateDynamicArrayResult(result, DataType.ExcelRange);
19+
}
20+
21+
private InMemoryRange TrimRangeCore(IRangeInfo range, TrimMode rowMode, TrimMode colMode, ParsingContext context, out CompileResult error)
22+
{
23+
error = null;
24+
int nRows = range.Size.NumberOfRows;
25+
int nCols = range.Size.NumberOfCols;
26+
ExcelWorksheet ws;
27+
if(range.Address != null && range.Address.WorksheetIx > -1 && context.CurrentWorksheet != null)
28+
{
29+
ws = context.CurrentWorksheet.Workbook.GetWorksheetByIndexInList(range.Address.WorksheetIx);
30+
}
31+
else
32+
{
33+
ws = context.CurrentWorksheet;
34+
}
35+
var dimension = ws?.Dimension;
36+
if (dimension == null)
37+
{
38+
error = CompileResult.GetErrorResult(eErrorType.Ref);
39+
return new InMemoryRange(1, 1);
40+
}
41+
42+
int rangeFromRow = range.Address.FromRow;
43+
int rangeFromCol = range.Address.FromCol;
44+
45+
int scanFirstRow = Math.Max(0, dimension.Start.Row - rangeFromRow);
46+
int scanLastRow = Math.Min(nRows - 1, dimension.End.Row - rangeFromRow);
47+
int scanFirstCol = Math.Max(0, dimension.Start.Column - rangeFromCol);
48+
int scanLastCol = Math.Min(nCols - 1, dimension.End.Column - rangeFromCol);
49+
50+
if (scanFirstRow > scanLastRow || scanFirstCol > scanLastCol)
51+
{
52+
error = CompileResult.GetErrorResult(eErrorType.Ref);
53+
return new InMemoryRange(1, 1);
54+
}
55+
int firstRow = 0, lastRow = nRows - 1;
56+
int firstCol = 0, lastCol = nCols - 1;
57+
58+
if (rowMode == TrimMode.Leading || rowMode == TrimMode.Both)
59+
firstRow = FindFirstNonEmptyRow(range, scanFirstRow, scanLastRow, scanFirstCol, scanLastCol);
60+
61+
if (rowMode == TrimMode.Trailing || rowMode == TrimMode.Both)
62+
lastRow = FindLastNonEmptyRow(range, scanFirstRow, scanLastRow, scanFirstCol, scanLastCol);
63+
64+
if (colMode == TrimMode.Leading || colMode == TrimMode.Both)
65+
firstCol = FindFirstNonEmptyCol(range, scanFirstRow, scanLastRow, scanFirstCol, scanLastCol);
66+
67+
if (colMode == TrimMode.Trailing || colMode == TrimMode.Both)
68+
lastCol = FindLastNonEmptyCol(range, scanFirstRow, scanLastRow, scanFirstCol, scanLastCol);
69+
70+
if (firstRow > lastRow || firstCol > lastCol)
71+
{
72+
error = CompileResult.GetErrorResult(eErrorType.Ref);
73+
return new InMemoryRange(1, 1);
74+
}
75+
76+
int trimmedRows = lastRow - firstRow + 1;
77+
int trimmedCols = lastCol - firstCol + 1;
78+
79+
var result = new InMemoryRange(trimmedRows, (short)trimmedCols);
80+
for (int r = 0; r < trimmedRows; r++)
81+
for (int c = 0; c < trimmedCols; c++)
82+
result.SetValue(r, c, range.GetOffset(firstRow + r, firstCol + c));
83+
84+
return result;
85+
}
86+
87+
private int FindFirstNonEmptyRow(IRangeInfo range, int rowStart, int rowEnd, int colStart, int colEnd)
88+
{
89+
for (int r = rowStart; r <= rowEnd; r++)
90+
for (int c = colStart; c <= colEnd; c++)
91+
if (HasValue(range.GetOffset(r, c))) return r;
92+
return int.MaxValue; // not found, empty
93+
}
94+
95+
private int FindLastNonEmptyRow(IRangeInfo range, int rowStart, int rowEnd, int colStart, int colEnd)
96+
{
97+
for (int r = rowEnd; r >= rowStart; r--)
98+
for (int c = colStart; c <= colEnd; c++)
99+
if (HasValue(range.GetOffset(r, c))) return r;
100+
return -1;
101+
}
102+
103+
private int FindFirstNonEmptyCol(IRangeInfo range, int rowStart, int rowEnd, int colStart, int colEnd)
104+
{
105+
for (int c = colStart; c <= colEnd; c++)
106+
for (int r = rowStart; r <= rowEnd; r++)
107+
if (HasValue(range.GetOffset(r, c))) return c;
108+
return int.MaxValue; // not found, empty
109+
}
110+
111+
private int FindLastNonEmptyCol(IRangeInfo range, int rowStart, int rowEnd, int colStart, int colEnd)
112+
{
113+
for (int c = colEnd; c >= colStart; c--)
114+
for (int r = rowStart; r <= rowEnd; r++)
115+
if (HasValue(range.GetOffset(r, c))) return c;
116+
return -1;
117+
}
118+
119+
protected bool HasValue(object val)
120+
{
121+
if (val == null) return false;
122+
if (val.GetType().Name == "RowInternal") return false;
123+
return true;
124+
}
125+
}
126+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions
14+
{
15+
internal enum TrimMode
16+
{
17+
None = 0,
18+
Leading = 1,
19+
Trailing = 2,
20+
Both = 3
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions
14+
{
15+
internal enum TrimRows
16+
{
17+
None = 0,
18+
TrimLeading = 1,
19+
TrimTrailing = 2,
20+
TrimAll = 3
21+
}
22+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
14+
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions;
15+
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
16+
using System.Collections.Generic;
17+
18+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
19+
{
20+
[FunctionMetadata(
21+
Category = ExcelFunctionCategory.LookupAndReference,
22+
EPPlusVersion = "8",
23+
Description = "Excludes all empty rows and/or columns from the outer edges of a range")]
24+
internal class TrimRange : TrimFunctionsBase
25+
{
26+
public override string NamespacePrefix => "_xlfn.";
27+
28+
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
29+
{
30+
var rowMode = TrimMode.Both;
31+
var colMode = TrimMode.Both;
32+
33+
if (arguments.Count > 1)
34+
{
35+
var v = ArgToInt(arguments, 1, RoundingMethod.Convert);
36+
if (v < 0 || v > 3) return CompileResult.GetErrorResult(eErrorType.Value);
37+
rowMode = (TrimMode)v;
38+
}
39+
40+
if (arguments.Count > 2)
41+
{
42+
var v = ArgToInt(arguments, 2, RoundingMethod.Convert);
43+
if (v < 0 || v > 3) return CompileResult.GetErrorResult(eErrorType.Value);
44+
colMode = (TrimMode)v;
45+
}
46+
47+
return ExecuteTrim(arguments, rowMode, colMode, context);
48+
}
49+
}
50+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
14+
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
15+
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions;
16+
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
17+
using System.Collections.Generic;
18+
19+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
20+
{
21+
[FunctionMetadata(
22+
Category = ExcelFunctionCategory.LookupAndReference,
23+
EPPlusVersion = "8.6",
24+
Description = "Used internally by Excel for the trim-range operators. Trims all leading and trailing empty rows and columns from a range.")]
25+
internal class TroAll : TrimFunctionsBase
26+
{
27+
public override string NamespacePrefix => "_xlfn.";
28+
29+
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
30+
=> ExecuteTrim(arguments, TrimMode.Both, TrimMode.Both, context);
31+
}
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
14+
using System.Collections.Generic;
15+
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
16+
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions;
17+
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
18+
19+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
20+
{
21+
[FunctionMetadata(
22+
Category = ExcelFunctionCategory.LookupAndReference,
23+
EPPlusVersion = "8.6",
24+
Description = "Used internally by Excel for the trim-range operators. Trims leading empty rows and columns from a range.")]
25+
26+
internal class TroLeading : TrimFunctionsBase
27+
{
28+
public override string NamespacePrefix => "_xlfn.";
29+
30+
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
31+
=> ExecuteTrim(arguments, TrimMode.Leading, TrimMode.Leading, context);
32+
}
33+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
25/5/2026 EPPlus Software AB EPPlus v8.6
12+
*************************************************************************************************/
13+
14+
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
15+
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.TrimFunctions;
16+
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
17+
using System.Collections.Generic;
18+
19+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
20+
{
21+
[FunctionMetadata(
22+
Category = ExcelFunctionCategory.LookupAndReference,
23+
EPPlusVersion = "8.6",
24+
Description = "Used internally by Excel for the trim-range operators. Trims trailing empty rows and columns from a range.")]
25+
internal class TroTrailing : TrimFunctionsBase
26+
{
27+
public override string NamespacePrefix => "_xlfn.";
28+
29+
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
30+
=> ExecuteTrim(arguments, TrimMode.Trailing, TrimMode.Trailing, context);
31+
}
32+
}

src/EPPlusTest/EPPlus.Test.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@
270270
<None Update="Workbooks\ToDataTableNullValues.xlsx">
271271
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
272272
</None>
273+
<None Update="Workbooks\Trimfunctions.xlsx">
274+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
275+
</None>
273276
<None Update="Workbooks\VBADecompressBug.xlsm">
274277
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
275278
</None>

0 commit comments

Comments
 (0)