
7 changed files with 182 additions and 28 deletions
-
1main/SS/Formula/Functions/CeilingMath.cs
-
40main/SS/Formula/Functions/DoublePrecisionHelper.cs
-
48main/SS/Formula/Functions/FloorCeilingMathBase.cs
-
1main/SS/Formula/Functions/FloorMath.cs
-
1test.runsettings
-
119testcases/ooxml/SS/Formula/Functions/TestFloorCeilingMath.cs
-
BINtestcases/test-data/functions/FloorCeilingMath.xlsx
@ -0,0 +1,40 @@ |
|||
using NPOI.SS.Formula.UDF; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Security.Permissions; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace NPOI.SS.Formula.Functions |
|||
{ |
|||
internal static class DoublePrecisionHelper |
|||
{ |
|||
public static double GetFractionPart(double number) |
|||
{ |
|||
return Math.Abs(number - Math.Truncate(number)); |
|||
} |
|||
public static double DropDigitsAfterSignificantOnes(double number, int digits) |
|||
{ |
|||
if (number == 0.0) return 0.0; |
|||
|
|||
var isNegative = number < 0; |
|||
var positiveNumber = isNegative ? -number : number; |
|||
|
|||
var mostSignificantDigit = Math.Floor(Math.Log10(positiveNumber)); |
|||
var multiplier = Math.Pow(10, digits - mostSignificantDigit - 1); |
|||
|
|||
var newNumber = positiveNumber * multiplier; |
|||
newNumber = GetFractionPart(newNumber) >= 0.5 ? Math.Truncate(newNumber) + 1 : Math.Truncate(newNumber); |
|||
|
|||
newNumber /= multiplier; |
|||
return isNegative ? -newNumber : newNumber; |
|||
} |
|||
|
|||
public static bool IsIntegerWithDigitsDropped(double number, int significantDigits) |
|||
{ |
|||
return Math.Abs(GetFractionPart(DropDigitsAfterSignificantOnes(number, significantDigits))) == 0.0; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,119 @@ |
|||
using NUnit.Framework; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using NPOI.SS.UserModel; |
|||
using NPOI.XSSF.UserModel; |
|||
using NPOI.SS.Formula.Functions; |
|||
using NUnit.Framework.Constraints; |
|||
using NPOI.SS.Util; |
|||
|
|||
namespace TestCases.SS.Formula.Functions |
|||
{ |
|||
/// <summary>
|
|||
/// Testing FLOOR.MATH & CEILING.MATH
|
|||
/// </summary>
|
|||
[TestFixture] |
|||
public class TestFloorCeilingMath |
|||
{ |
|||
// In real-world Excel's tolerance control is more complicated.
|
|||
// Save it for now.
|
|||
private const double Tolerance = 1e-7; |
|||
public enum FunctionTested |
|||
{ |
|||
Ceiling, |
|||
Floor |
|||
} |
|||
public sealed class TestFunction |
|||
{ |
|||
public TestFunction(FunctionTested func, bool mode) |
|||
{ |
|||
Function = func; |
|||
Mode = mode; |
|||
} |
|||
public FunctionTested Function { get; set; } |
|||
public bool Mode { get; set; } |
|||
public string SheetName |
|||
=> (Function == FunctionTested.Ceiling ? "CEILING" : "FLOOR") + "," + Mode.ToString().ToUpperInvariant(); |
|||
|
|||
public FloorCeilingMathBase Evaluator |
|||
=> Function == FunctionTested.Ceiling ? CeilingMath.Instance : (FloorCeilingMathBase)FloorMath.Instance; |
|||
|
|||
public double Evaluate(double number, double significance) |
|||
=> Evaluator.Evaluate(number, significance, Mode); |
|||
public override string ToString() |
|||
=> SheetName; |
|||
} |
|||
|
|||
private XSSFWorkbook _workbook; |
|||
[OneTimeSetUp] |
|||
public void LoadData() |
|||
{ |
|||
var fldr = Path.Combine(TestContext.CurrentContext.TestDirectory, TestContext.Parameters["function"]); |
|||
const string filename = "FloorCeilingMath.xlsx"; |
|||
var file = Path.Combine(fldr, filename); |
|||
|
|||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) |
|||
{ |
|||
_workbook = new XSSFWorkbook(fs); |
|||
} |
|||
} |
|||
|
|||
[OneTimeTearDown] |
|||
public void Dispose() |
|||
{ |
|||
_workbook?.Close(); |
|||
} |
|||
public static TestFunction[] FunctionVariants => new[] |
|||
{ |
|||
new TestFunction(FunctionTested.Ceiling, false), |
|||
new TestFunction(FunctionTested.Ceiling, true), |
|||
new TestFunction(FunctionTested.Floor, false), |
|||
new TestFunction(FunctionTested.Floor, true), |
|||
}; |
|||
|
|||
[Test, Order(1)] |
|||
[TestCaseSource(nameof(FunctionVariants))] |
|||
public void TestEvaluate(TestFunction function) |
|||
{ |
|||
const int StartRowIndex = 1; |
|||
const int StartColumnIndex = 0; |
|||
const int Count = 34; |
|||
|
|||
Assert.Multiple(() => |
|||
{ |
|||
var sheet = _workbook.GetSheet(function.SheetName); |
|||
for (var i = 1; i <= Count; i++) |
|||
{ |
|||
var row = sheet.GetRow(i + StartRowIndex); |
|||
var significance = row.GetCell(StartColumnIndex).NumericCellValue; |
|||
|
|||
for (var j = 1; j <= Count; j++) |
|||
{ |
|||
var number = sheet.GetRow(StartRowIndex).GetCell(j + StartColumnIndex).NumericCellValue; |
|||
var expected = row.GetCell(j + StartColumnIndex).NumericCellValue; |
|||
|
|||
var functionResult = function.Evaluate(number, significance); |
|||
|
|||
// Excel also has bugs on =FLOOR.MATH(4, -2, FALSE|TRUE)
|
|||
// as it recognizes auto-filled 4 as 3.99999999999999.
|
|||
// See the cell of AF13 in the test data file.
|
|||
// So the specific pair is skipped.
|
|||
|
|||
if (Math.Abs(number - (4)) < Tolerance && Math.Abs(significance - (-2)) < Tolerance) |
|||
continue; |
|||
|
|||
Assert.AreEqual(expected, functionResult, Tolerance, $"{function}, {number}, {significance}"); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
[Test, Order(2), NonParallelizable] |
|||
public void EvaluateAllFormulas() |
|||
{ |
|||
var evaluator = new XSSFFormulaEvaluator(_workbook); |
|||
evaluator.ClearAllCachedResultValues(); |
|||
Assert.DoesNotThrow(() => evaluator.EvaluateAll()); |
|||
} |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue