diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs b/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs index b73b4e4a0..ce990f339 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs @@ -64,6 +64,9 @@ public BuiltInFunctions() Functions["unichar"] = new Unichar(); Functions["numbervalue"] = new NumberValue(); Functions["dollar"] = new Dollar(); + Functions["usdollar"] = new UsDollar(); + Functions["encodeurl"] = new EncodeUrl(); + Functions["code"] = new CodeFunction(); Functions["textsplit"] = new TextSplit(); Functions["textbefore"] = new TextBefore(DelimiterFunction.TextBefore); Functions["textafter"] = new TextAfter(DelimiterFunction.TextAfter); @@ -374,6 +377,8 @@ public BuiltInFunctions() Functions["hstack"] = new Hstack(); Functions["getpivotdata"] = new GetPivotData(); Functions["image"] = new ImageFunction(); + Functions["wraprows"] = new WrapRows(); + Functions["wrapcols"] = new WrapCols(); // Date Functions["date"] = new Date(); Functions["datedif"] = new DateDif(); diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapCols.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapCols.cs new file mode 100644 index 000000000..4e7b5566f --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapCols.cs @@ -0,0 +1,60 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + XX/XX/XXXX EPPlus Software AB EPPlus vX + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata; +using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using OfficeOpenXml.FormulaParsing.Ranges; +using System.Collections.Generic; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup +{ + [FunctionMetadata( + Category = ExcelFunctionCategory.LookupAndReference, + EPPlusVersion = "8.6", + Description = "Wraps a row or column vector into a 2D array of the specified number of rows per column.", + SupportsArrays = true)] + internal class WrapCols : WrapFunctionBase + { + public override CompileResult Execute(IList arguments, ParsingContext context) + { + int wrapCount; + object padValue; + List items; + ParseArguments(arguments, out wrapCount, out padValue, out items, out CompileResult error); + if (error != null) return error; + + // Output shape: wrap_count rows, ceil(n / wrap_count) columns. + var itemCount = items.Count; + var resultCols = (itemCount + wrapCount - 1) / wrapCount; + var resultRange = new InMemoryRange(new RangeDefinition(wrapCount, (short)resultCols)); + + // Fill column-major: column 0 top-to-bottom, then column 1, etc. + for (var i = 0; i < wrapCount * resultCols; i++) + { + var col = i / wrapCount; + var row = i % wrapCount; + object value; + if (i < itemCount) + { + value = items[i]; + } + else + { + value = padValue; + } + resultRange.SetValue(row, col, value); + } + + return CreateDynamicArrayResult(resultRange, DataType.ExcelRange); + } + } +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapFunctionBase.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapFunctionBase.cs new file mode 100644 index 000000000..ca6c371d9 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapFunctionBase.cs @@ -0,0 +1,119 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + XX/XX/XXXX EPPlus Software AB EPPlus vX + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using System.Collections.Generic; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup +{ + /// + /// Base class for the WRAPROWS and WRAPCOLS functions. Provides shared + /// argument parsing, vector flattening and validation. + /// + internal abstract class WrapFunctionBase : ExcelFunction + { + public override string NamespacePrefix => "_xlfn."; + public override int ArgumentMinLength => 2; + + /// + /// If the function is allowed in a pivot table calculated field. + /// + public override bool IsAllowedInCalculatedPivotTableField => false; + + /// + /// Parses and validates the common WRAPROWS/WRAPCOLS arguments and flattens + /// the source vector. Returns null on success; otherwise a CompileResult + /// holding the error to return. + /// + /// The function arguments. + /// Out: the validated wrap count. + /// Out: the pad value (defaults to #N/A). + /// Out: the flattened source items. + /// Out: if an error occurs during parsing + protected void ParseArguments( + IList arguments, + out int wrapCount, + out object padValue, + out List items, + out CompileResult errResult) + { + wrapCount = 0; + padValue = ExcelErrorValue.Create(eErrorType.NA); + items = null; + errResult = default; + + // wrap_count must be present and a positive integer + wrapCount = ArgToInt(arguments, 1, out ExcelErrorValue wrapErr); + if (wrapErr != null) + { + errResult = CompileResult.GetDynamicArrayResultError(wrapErr.Type); + return; + } + if (wrapCount < 1) + { + errResult = CompileResult.GetDynamicArrayResultError(eErrorType.Num); + return; + } + + // Optional pad value + if (arguments.Count > 2 && arguments[2].Value != null) + { + padValue = arguments[2].Value; + } + + // Collect the source values as a flat list. Input must be a 1D vector + // (single row or single column). A 2D range yields #VALUE!. + var firstArg = arguments[0]; + if (firstArg.IsExcelRange) + { + var range = firstArg.ValueAsRangeInfo; + var rows = range.Size.NumberOfRows; + var cols = range.Size.NumberOfCols; + if (rows > 1 && cols > 1) + { + errResult = CompileResult.GetDynamicArrayResultError(eErrorType.Value); + return; + } + items = FlattenVector(range, rows, cols); + } + else + { + // Scalar input behaves as a single-element vector. + items = new List(); + items.Add(firstArg.Value); + } + } + + /// + /// Flattens a 1D range (single row or single column) into a list of values. + /// + private static List FlattenVector(IRangeInfo range, int rows, int cols) + { + var result = new List(rows * cols); + if (cols == 1) + { + for (var r = 0; r < rows; r++) + { + result.Add(range.GetOffset(r, 0)); + } + } + else + { + for (var c = 0; c < cols; c++) + { + result.Add(range.GetOffset(0, c)); + } + } + return result; + } + } +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapRows.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapRows.cs new file mode 100644 index 000000000..15966e03a --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/WrapRows.cs @@ -0,0 +1,60 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + XX/XX/XXXX EPPlus Software AB EPPlus vX + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata; +using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using OfficeOpenXml.FormulaParsing.Ranges; +using System.Collections.Generic; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup +{ + [FunctionMetadata( + Category = ExcelFunctionCategory.LookupAndReference, + EPPlusVersion = "8.6", + Description = "Wraps a row or column vector into a 2D array of the specified number of columns per row.", + SupportsArrays = true)] + internal class WrapRows : WrapFunctionBase + { + public override CompileResult Execute(IList arguments, ParsingContext context) + { + int wrapCount; + object padValue; + List items; + ParseArguments(arguments, out wrapCount, out padValue, out items, out CompileResult error); + if (error != null) return error; + + // Output shape: ceil(n / wrap_count) rows, wrap_count columns. + var itemCount = items.Count; + var resultRows = (itemCount + wrapCount - 1) / wrapCount; + var resultRange = new InMemoryRange(new RangeDefinition(resultRows, (short)wrapCount)); + + // Fill row-major: row 0 left-to-right, then row 1, etc. + for (var i = 0; i < resultRows * wrapCount; i++) + { + var row = i / wrapCount; + var col = i % wrapCount; + object value; + if (i < itemCount) + { + value = items[i]; + } + else + { + value = padValue; + } + resultRange.SetValue(row, col, value); + } + + return CreateDynamicArrayResult(resultRange, DataType.ExcelRange); + } + } +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/Text/CodeFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/Text/CodeFunction.cs new file mode 100644 index 000000000..99563c747 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/Text/CodeFunction.cs @@ -0,0 +1,42 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + XX/XX/XXXX EPPlus Software AB EPPlus vX + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata; +using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using System.Collections.Generic; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Text +{ + [FunctionMetadata( + Category = ExcelFunctionCategory.Text, + EPPlusVersion = "8.6", + Description = "Returns the numeric code for the first character of a text string.", SupportsArrays = true)] + internal class CodeFunction : ExcelFunction + { + public override ExcelFunctionArrayBehaviour ArrayBehaviour => ExcelFunctionArrayBehaviour.FirstArgCouldBeARange; + + public override int ArgumentMinLength => 1; + + public override CompileResult Execute(IList arguments, ParsingContext context) + { + var text = ArgToString(arguments, 0); + if (string.IsNullOrEmpty(text)) + { + return CompileResult.GetErrorResult(eErrorType.Value); + } + // Return the numeric code (Unicode code unit) of the first character. + // For surrogate pairs we return the leading high surrogate, matching Excel's behavior. + int code = text[0]; + return CreateResult((double)code, DataType.Decimal); + } + } +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/Text/Dollar.cs b/src/EPPlus/FormulaParsing/Excel/Functions/Text/Dollar.cs index d1a43cf69..f5ac3a1fd 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/Text/Dollar.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/Text/Dollar.cs @@ -26,6 +26,13 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Text Description = "Converts a supplied number into text, using a currency format")] internal class Dollar : ExcelFunction { + public override ExcelFunctionArrayBehaviour ArrayBehaviour => ExcelFunctionArrayBehaviour.Custom; + + public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config) + { + config.SetArrayParameterIndexes(0, 1); + } + public override int ArgumentMinLength => 1; public override CompileResult Execute(IList arguments, ParsingContext context) { @@ -38,15 +45,18 @@ public override CompileResult Execute(IList arguments, Parsing if (e2 != null) return CompileResult.GetErrorResult(e2.Type); } double result; - if(decimals >= 0) + if (decimals >= 0) { - result = Math.Round(number, decimals); + result = Math.Round(number, decimals, MidpointRounding.AwayFromZero); } else { - result = Math.Round(number * System.Math.Pow(10, decimals)) / System.Math.Pow(10, decimals); + var factor = Math.Pow(10, decimals); + result = Math.Round(number * factor, MidpointRounding.AwayFromZero) / factor; } - return CreateResult(result.ToString(GetFormatString(decimals), CultureInfo.CurrentCulture), DataType.String); + var formatCulture = (CultureInfo)CultureInfo.CurrentCulture.Clone(); + formatCulture.NumberFormat.CurrencyNegativePattern = 0; + return CreateResult(result.ToString(GetFormatString(decimals), formatCulture), DataType.String); } private string GetFormatString(int decimals) diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/Text/EncodeUrl.cs b/src/EPPlus/FormulaParsing/Excel/Functions/Text/EncodeUrl.cs new file mode 100644 index 000000000..2148f7106 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/Text/EncodeUrl.cs @@ -0,0 +1,41 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + XX/XX/XXXX EPPlus Software AB EPPlus vX + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata; +using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using System; +using System.Collections.Generic; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Text +{ + [FunctionMetadata( + Category = ExcelFunctionCategory.Text, + EPPlusVersion = "8.6", + Description = "Returns a URL-encoded string, replacing characters that are not allowed in URLs with their percent-encoded equivalents.", + SupportsArrays = true)] + internal class EncodeUrl : ExcelFunction + { + public override ExcelFunctionArrayBehaviour ArrayBehaviour => ExcelFunctionArrayBehaviour.FirstArgCouldBeARange; + + public override string NamespacePrefix => "_xlfn."; + public override int ArgumentMinLength => 1; + + public override CompileResult Execute(IList arguments, ParsingContext context) + { + var text = ArgToString(arguments, 0); + if (text == null) text = string.Empty; + // Uri.EscapeDataString uses UTF-8 percent-encoding, matching Excel's behavior. + var encoded = Uri.EscapeDataString(text); + return CreateResult(encoded, DataType.String); + } + } +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/Text/UsDollar.cs b/src/EPPlus/FormulaParsing/Excel/Functions/Text/UsDollar.cs new file mode 100644 index 000000000..0b516ec15 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/Text/UsDollar.cs @@ -0,0 +1,22 @@ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata; + +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; + +/// +/// USDOLLAR is a legacy function preserved for Lotus 1-2-3 compatibility. +/// +/// Microsoft's documentation claims this function "always shows U.S. currency", +/// but in practice Excel desktop renders it using the system's current locale, +/// identical to DOLLAR. For example, USDOLLAR(3) on a Swedish system produces +/// "3,00 kr", not "$3.00". Verified against Excel desktop output. +/// +/// We match the actual Excel behavior, not the documentation. +/// +[FunctionMetadata( + Category = ExcelFunctionCategory.Text, + EPPlusVersion = "8.6", + Description = "Legacy Lotus 1-2-3 compatibility function. Behaves identically to DOLLAR.", + SupportsArrays = true)] +internal class UsDollar : Dollar +{ +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/WrapColsTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/WrapColsTests.cs new file mode 100644 index 000000000..4a28c4d6e --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/WrapColsTests.cs @@ -0,0 +1,179 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using System; + +namespace EPPlusTest.FormulaParsing.Excel.Functions.RefAndLookup +{ + [TestClass] + public class WrapColsTests + { + private ExcelWorksheet _sheet; + private ExcelPackage _package; + + [TestInitialize] + public void TestInitialize() + { + _package = new ExcelPackage(); + _sheet = _package.Workbook.Worksheets.Add("test"); + } + + [TestCleanup] + public void TestCleanup() + { + _package.Dispose(); + } + + private void AddRowVector() + { + // A1:F1 = 1..6 + for (var col = 1; col <= 6; col++) + { + _sheet.Cells[1, col].Value = col; + } + } + + private void AddColumnVector() + { + // A1:A6 = 1..6 + for (var row = 1; row <= 6; row++) + { + _sheet.Cells[row, 1].Value = row; + } + } + + [TestMethod] + public void ShouldWrapRowVectorIntoColumnsOfThree() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:F1,3)"; + _sheet.Calculate(); + // Expected layout at A10:B12 (3 rows, 2 cols) + // 1 4 + // 2 5 + // 3 6 + Assert.AreEqual(1, _sheet.Cells["A10"].Value); + Assert.AreEqual(2, _sheet.Cells["A11"].Value); + Assert.AreEqual(3, _sheet.Cells["A12"].Value); + Assert.AreEqual(4, _sheet.Cells["B10"].Value); + Assert.AreEqual(5, _sheet.Cells["B11"].Value); + Assert.AreEqual(6, _sheet.Cells["B12"].Value); + } + + [TestMethod] + public void ShouldWrapColumnVectorIntoColumnsOfTwo() + { + AddColumnVector(); + _sheet.Cells["C10"].Formula = "WRAPCOLS(A1:A6,2)"; + _sheet.Calculate(); + // Expected layout at C10:E11 (2 rows, 3 cols) + // 1 3 5 + // 2 4 6 + Assert.AreEqual(1, _sheet.Cells["C10"].Value); + Assert.AreEqual(2, _sheet.Cells["C11"].Value); + Assert.AreEqual(3, _sheet.Cells["D10"].Value); + Assert.AreEqual(4, _sheet.Cells["D11"].Value); + Assert.AreEqual(5, _sheet.Cells["E10"].Value); + Assert.AreEqual(6, _sheet.Cells["E11"].Value); + } + + [TestMethod] + public void ShouldPadLastColumnWithNAByDefault() + { + AddRowVector(); + // 6 items, wrap_count = 4 -> 4 rows, 2 cols, last col has 2 padded cells + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:F1,4)"; + _sheet.Calculate(); + // Expected + // 1 5 + // 2 6 + // 3 #N/A + // 4 #N/A + Assert.AreEqual(1, _sheet.Cells["A10"].Value); + Assert.AreEqual(2, _sheet.Cells["A11"].Value); + Assert.AreEqual(3, _sheet.Cells["A12"].Value); + Assert.AreEqual(4, _sheet.Cells["A13"].Value); + Assert.AreEqual(5, _sheet.Cells["B10"].Value); + Assert.AreEqual(6, _sheet.Cells["B11"].Value); + var b12 = _sheet.Cells["B12"].Value as ExcelErrorValue; + var b13 = _sheet.Cells["B13"].Value as ExcelErrorValue; + Assert.IsNotNull(b12); + Assert.IsNotNull(b13); + Assert.AreEqual(eErrorType.NA, b12.Type); + Assert.AreEqual(eErrorType.NA, b13.Type); + } + + [TestMethod] + public void ShouldUseSuppliedPadValue() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:F1,4,0)"; + _sheet.Calculate(); + Assert.AreEqual(1, _sheet.Cells["A10"].Value); + Assert.AreEqual(4, _sheet.Cells["A13"].Value); + Assert.AreEqual(5, _sheet.Cells["B10"].Value); + Assert.AreEqual(6, _sheet.Cells["B11"].Value); + Assert.AreEqual(0D, _sheet.Cells["B12"].Value); + Assert.AreEqual(0D, _sheet.Cells["B13"].Value); + } + + [TestMethod] + public void ShouldReturnExactFitWithoutPadding() + { + AddRowVector(); + // 6 items, wrap_count = 3 -> exactly 2 cols, no padding + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:F1,3,\"X\")"; + _sheet.Calculate(); + Assert.AreEqual(6, _sheet.Cells["B12"].Value); + // Make sure the cell to the right of the spill is untouched + Assert.IsNull(_sheet.Cells["C10"].Value); + } + + [TestMethod] + public void ShouldReturnValueErrorFor2dRange() + { + // 2x3 range is not a vector + _sheet.Cells["A1"].Value = 1; + _sheet.Cells["B1"].Value = 2; + _sheet.Cells["C1"].Value = 3; + _sheet.Cells["A2"].Value = 4; + _sheet.Cells["B2"].Value = 5; + _sheet.Cells["C2"].Value = 6; + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:C2,2)"; + _sheet.Calculate(); + var err = _sheet.Cells["A10"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Value, err.Type); + } + + [TestMethod] + public void ShouldReturnNumErrorWhenWrapCountIsZero() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:F1,0)"; + _sheet.Calculate(); + var err = _sheet.Cells["A10"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Num, err.Type); + } + + [TestMethod] + public void ShouldReturnNumErrorWhenWrapCountIsNegative() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1:F1,-1)"; + _sheet.Calculate(); + var err = _sheet.Cells["A10"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Num, err.Type); + } + + [TestMethod] + public void ShouldWrapSingleCellAsOneByOne() + { + _sheet.Cells["A1"].Value = 42; + _sheet.Cells["A10"].Formula = "WRAPCOLS(A1,1)"; + _sheet.Calculate(); + Assert.AreEqual(42, _sheet.Cells["A10"].Value); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/WrapRowsTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/WrapRowsTests.cs new file mode 100644 index 000000000..7f221b618 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/WrapRowsTests.cs @@ -0,0 +1,174 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using System; + +namespace EPPlusTest.FormulaParsing.Excel.Functions.RefAndLookup +{ + [TestClass] + public class WrapRowsTests + { + private ExcelWorksheet _sheet; + private ExcelPackage _package; + + [TestInitialize] + public void TestInitialize() + { + _package = new ExcelPackage(); + _sheet = _package.Workbook.Worksheets.Add("test"); + } + + [TestCleanup] + public void TestCleanup() + { + _package.Dispose(); + } + + private void AddRowVector() + { + // A1:F1 = 1..6 + for (var col = 1; col <= 6; col++) + { + _sheet.Cells[1, col].Value = col; + } + } + + private void AddColumnVector() + { + // A1:A6 = 1..6 + for (var row = 1; row <= 6; row++) + { + _sheet.Cells[row, 1].Value = row; + } + } + + [TestMethod] + public void ShouldWrapRowVectorIntoRowsOfThree() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:F1,3)"; + _sheet.Calculate(); + // Expected layout at A10:C11 + // 1 2 3 + // 4 5 6 + Assert.AreEqual(1, _sheet.Cells["A10"].Value); + Assert.AreEqual(2, _sheet.Cells["B10"].Value); + Assert.AreEqual(3, _sheet.Cells["C10"].Value); + Assert.AreEqual(4, _sheet.Cells["A11"].Value); + Assert.AreEqual(5, _sheet.Cells["B11"].Value); + Assert.AreEqual(6, _sheet.Cells["C11"].Value); + } + + [TestMethod] + public void ShouldWrapColumnVectorIntoRowsOfTwo() + { + AddColumnVector(); + _sheet.Cells["C10"].Formula = "WRAPROWS(A1:A6,2)"; + _sheet.Calculate(); + // Expected layout at C10:D12 + // 1 2 + // 3 4 + // 5 6 + Assert.AreEqual(1, _sheet.Cells["C10"].Value); + Assert.AreEqual(2, _sheet.Cells["D10"].Value); + Assert.AreEqual(3, _sheet.Cells["C11"].Value); + Assert.AreEqual(4, _sheet.Cells["D11"].Value); + Assert.AreEqual(5, _sheet.Cells["C12"].Value); + Assert.AreEqual(6, _sheet.Cells["D12"].Value); + } + + [TestMethod] + public void ShouldPadLastRowWithNAByDefault() + { + AddRowVector(); + // 6 items, wrap_count = 4 -> last row has 2 padded cells (#N/A by default) + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:F1,4)"; + _sheet.Calculate(); + Assert.AreEqual(1, _sheet.Cells["A10"].Value); + Assert.AreEqual(2, _sheet.Cells["B10"].Value); + Assert.AreEqual(3, _sheet.Cells["C10"].Value); + Assert.AreEqual(4, _sheet.Cells["D10"].Value); + Assert.AreEqual(5, _sheet.Cells["A11"].Value); + Assert.AreEqual(6, _sheet.Cells["B11"].Value); + var c11 = _sheet.Cells["C11"].Value as ExcelErrorValue; + var d11 = _sheet.Cells["D11"].Value as ExcelErrorValue; + Assert.IsNotNull(c11); + Assert.IsNotNull(d11); + Assert.AreEqual(eErrorType.NA, c11.Type); + Assert.AreEqual(eErrorType.NA, d11.Type); + } + + [TestMethod] + public void ShouldUseSuppliedPadValue() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:F1,4,0)"; + _sheet.Calculate(); + Assert.AreEqual(1, _sheet.Cells["A10"].Value); + Assert.AreEqual(4, _sheet.Cells["D10"].Value); + Assert.AreEqual(5, _sheet.Cells["A11"].Value); + Assert.AreEqual(6, _sheet.Cells["B11"].Value); + Assert.AreEqual(0D, _sheet.Cells["C11"].Value); + Assert.AreEqual(0D, _sheet.Cells["D11"].Value); + } + + [TestMethod] + public void ShouldReturnExactFitWithoutPadding() + { + AddRowVector(); + // 6 items, wrap_count = 3 -> exactly 2 rows, no padding + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:F1,3,\"X\")"; + _sheet.Calculate(); + Assert.AreEqual(6, _sheet.Cells["C11"].Value); + // Make sure the cell below the spill is untouched + Assert.IsNull(_sheet.Cells["A12"].Value); + } + + [TestMethod] + public void ShouldReturnValueErrorFor2dRange() + { + // 2x3 range is not a vector + _sheet.Cells["A1"].Value = 1; + _sheet.Cells["B1"].Value = 2; + _sheet.Cells["C1"].Value = 3; + _sheet.Cells["A2"].Value = 4; + _sheet.Cells["B2"].Value = 5; + _sheet.Cells["C2"].Value = 6; + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:C2,2)"; + _sheet.Calculate(); + var err = _sheet.Cells["A10"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Value, err.Type); + } + + [TestMethod] + public void ShouldReturnNumErrorWhenWrapCountIsZero() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:F1,0)"; + _sheet.Calculate(); + var err = _sheet.Cells["A10"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Num, err.Type); + } + + [TestMethod] + public void ShouldReturnNumErrorWhenWrapCountIsNegative() + { + AddRowVector(); + _sheet.Cells["A10"].Formula = "WRAPROWS(A1:F1,-1)"; + _sheet.Calculate(); + var err = _sheet.Cells["A10"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Num, err.Type); + } + + [TestMethod] + public void ShouldWrapSingleCellAsOneByOne() + { + _sheet.Cells["A1"].Value = 42; + _sheet.Cells["A10"].Formula = "WRAPROWS(A1,1)"; + _sheet.Calculate(); + Assert.AreEqual(42, _sheet.Cells["A10"].Value); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/CodeTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/CodeTests.cs new file mode 100644 index 000000000..35235b513 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/CodeTests.cs @@ -0,0 +1,94 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; + +namespace EPPlusTest.FormulaParsing.Excel.Functions.Text +{ + [TestClass] + public class CodeTests + { + private ExcelWorksheet _sheet; + private ExcelPackage _package; + + [TestInitialize] + public void TestInitialize() + { + _package = new ExcelPackage(); + _sheet = _package.Workbook.Worksheets.Add("test"); + } + + [TestCleanup] + public void TestCleanup() + { + _package.Dispose(); + } + + [TestMethod] + public void ShouldReturnCodeForUppercaseA() + { + _sheet.Cells["A1"].Formula = "CODE(\"A\")"; + _sheet.Calculate(); + Assert.AreEqual(65d, _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnCodeForLowercaseA() + { + _sheet.Cells["A1"].Formula = "CODE(\"a\")"; + _sheet.Calculate(); + Assert.AreEqual(97d, _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnCodeForFirstCharacterOnly() + { + _sheet.Cells["A1"].Formula = "CODE(\"Hello\")"; + _sheet.Calculate(); + // H = 72 + Assert.AreEqual(72d, _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnCodeForDigit() + { + _sheet.Cells["A1"].Formula = "CODE(\"0\")"; + _sheet.Calculate(); + Assert.AreEqual(48d, _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnCodeForUnicodeCharacter() + { + // Swedish å = U+00E5 = 229 + _sheet.Cells["A1"].Formula = "CODE(\"\u00e5\")"; + _sheet.Calculate(); + Assert.AreEqual(229d, _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnCodeForSpace() + { + _sheet.Cells["A1"].Formula = "CODE(\" \")"; + _sheet.Calculate(); + Assert.AreEqual(32d, _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnValueErrorForEmptyString() + { + _sheet.Cells["A1"].Formula = "CODE(\"\")"; + _sheet.Calculate(); + var err = _sheet.Cells["A1"].Value as ExcelErrorValue; + Assert.IsNotNull(err); + Assert.AreEqual(eErrorType.Value, err.Type); + } + + [TestMethod] + public void ShouldReturnCodeFromCellReference() + { + _sheet.Cells["B1"].Value = "Z"; + _sheet.Cells["A1"].Formula = "CODE(B1)"; + _sheet.Calculate(); + Assert.AreEqual(90d, _sheet.Cells["A1"].Value); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/EncodeUrlTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/EncodeUrlTests.cs new file mode 100644 index 000000000..0bde14cee --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/EncodeUrlTests.cs @@ -0,0 +1,76 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; + +namespace EPPlusTest.FormulaParsing.Excel.Functions.Text +{ + [TestClass] + public class EncodeUrlTests + { + private ExcelWorksheet _sheet; + private ExcelPackage _package; + + [TestInitialize] + public void TestInitialize() + { + _package = new ExcelPackage(); + _sheet = _package.Workbook.Worksheets.Add("test"); + } + + [TestCleanup] + public void TestCleanup() + { + _package.Dispose(); + } + + [TestMethod] + public void ShouldEncodeSpacesAsPercent20() + { + _sheet.Cells["A1"].Formula = "ENCODEURL(\"hello world\")"; + _sheet.Calculate(); + Assert.AreEqual("hello%20world", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldEncodeSpecialCharacters() + { + _sheet.Cells["A1"].Formula = "ENCODEURL(\"a&b=c?d\")"; + _sheet.Calculate(); + Assert.AreEqual("a%26b%3Dc%3Fd", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldNotEncodeUnreservedAscii() + { + _sheet.Cells["A1"].Formula = "ENCODEURL(\"abc-XYZ_123.~\")"; + _sheet.Calculate(); + // Letters, digits, hyphen, underscore, period and tilde are unreserved per RFC 3986. + Assert.AreEqual("abc-XYZ_123.~", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldEncodeUnicodeAsUtf8() + { + _sheet.Cells["A1"].Formula = "ENCODEURL(\"\u00e5\u00e4\u00f6\")"; + _sheet.Calculate(); + // å = C3 A5, ä = C3 A4, ö = C3 B6 in UTF-8 + Assert.AreEqual("%C3%A5%C3%A4%C3%B6", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldReturnEmptyStringForEmptyInput() + { + _sheet.Cells["A1"].Formula = "ENCODEURL(\"\")"; + _sheet.Calculate(); + Assert.AreEqual("", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldEncodeCellReference() + { + _sheet.Cells["B1"].Value = "test value"; + _sheet.Cells["A1"].Formula = "ENCODEURL(B1)"; + _sheet.Calculate(); + Assert.AreEqual("test%20value", _sheet.Cells["A1"].Value); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/UsDollarTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/UsDollarTests.cs new file mode 100644 index 000000000..1fe2cef9f --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/TextFunctions/UsDollarTests.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using System.Globalization; +using System.Threading; + +namespace EPPlusTest.FormulaParsing.Excel.Functions.Text +{ + [TestClass] + public class UsDollarTests : TestBase + { + private ExcelWorksheet _sheet; + private ExcelPackage _package; + private CultureInfo _originalCulture; + + [TestInitialize] + public void TestInitialize() + { + _package = new ExcelPackage(); + _sheet = _package.Workbook.Worksheets.Add("test"); + SwitchToCulture("en-US"); + } + + [TestCleanup] + public void TestCleanup() + { + SwitchBackToCurrentCulture(); + _package.Dispose(); + } + + [TestMethod] + public void ShouldFormatPositiveNumberWithDefaultDecimals() + { + _sheet.Cells["A1"].Formula = "USDOLLAR(1234.567)"; + _sheet.Calculate(); + Assert.AreEqual("$1,234.57", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldFormatNegativeNumberWithParentheses() + { + _sheet.Cells["A1"].Formula = "USDOLLAR(-1234.567)"; + _sheet.Calculate(); + // en-US currency format renders negatives as ($1,234.57) + Assert.AreEqual("($1,234.57)", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldHonorExplicitDecimals() + { + _sheet.Cells["A1"].Formula = "USDOLLAR(1234.5678,3)"; + _sheet.Calculate(); + Assert.AreEqual("$1,234.568", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldFormatWithZeroDecimals() + { + _sheet.Cells["A1"].Formula = "USDOLLAR(1234.5,0)"; + _sheet.Calculate(); + Assert.AreEqual("$1,235", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldRoundToNegativeDecimals() + { + // -2 decimals rounds to nearest hundred + _sheet.Cells["A1"].Formula = "USDOLLAR(1234.567,-2)"; + _sheet.Calculate(); + Assert.AreEqual("$1,200", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldHandleZero() + { + _sheet.Cells["A1"].Formula = "USDOLLAR(0)"; + _sheet.Calculate(); + Assert.AreEqual("$0.00", _sheet.Cells["A1"].Value); + } + + [TestMethod] + public void ShouldSpillOverRangeFirstArg() + { + _sheet.Cells["B1"].Value = 1234.5; + _sheet.Cells["B2"].Value = 99.9; + _sheet.Cells["A1"].Formula = "USDOLLAR(B1:B2)"; + _sheet.Calculate(); + Assert.AreEqual("$1,234.50", _sheet.Cells["A1"].Value); + Assert.AreEqual("$99.90", _sheet.Cells["A2"].Value); + } + + [TestMethod] + public void ShouldSpillWithBothArgsAsRanges() + { + _sheet.Cells["B1"].Value = 1234.5; + _sheet.Cells["B2"].Value = 99.9; + _sheet.Cells["C1"].Value = 2; + _sheet.Cells["C2"].Value = 0; + _sheet.Cells["A1"].Formula = "USDOLLAR(B1:B2,C1:C2)"; + _sheet.Calculate(); + Assert.AreEqual("$1,234.50", _sheet.Cells["A1"].Value); + Assert.AreEqual("$100", _sheet.Cells["A2"].Value); + } + } +} \ No newline at end of file