Browse Source

improved FLOOR|CEILING.MATH function

pull/1135/head
Gan Keyu 2 years ago
parent
commit
299a81d244
  1. 1
      main/SS/Formula/Functions/CeilingMath.cs
  2. 40
      main/SS/Formula/Functions/DoublePrecisionHelper.cs
  3. 48
      main/SS/Formula/Functions/FloorCeilingMathBase.cs
  4. 1
      main/SS/Formula/Functions/FloorMath.cs
  5. 1
      test.runsettings
  6. 119
      testcases/ooxml/SS/Formula/Functions/TestFloorCeilingMath.cs
  7. BIN
      testcases/test-data/functions/FloorCeilingMath.xlsx

1
main/SS/Formula/Functions/CeilingMath.cs

@ -12,7 +12,6 @@ namespace NPOI.SS.Formula.Functions
{
}
public static readonly CeilingMath Instance = new();
protected override double EvaluateMajorDirection(double number)
=> Math.Ceiling(number);

40
main/SS/Formula/Functions/DoublePrecisionHelper.cs

@ -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;
}
}
}

48
main/SS/Formula/Functions/FloorCeilingMathBase.cs

@ -1,6 +1,7 @@
using NPOI.SS.Formula.Eval;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -9,12 +10,15 @@ namespace NPOI.SS.Formula.Functions
{
public abstract class FloorCeilingMathBase : FreeRefFunction
{
// Excel has an internal precision of 15 significant digits
private const int SignificantThreshold = 15;
public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec)
=> args.Length switch
{
1 => Evaluate(0, 0, args[0]),
2 => Evaluate(0, 0, args[0], args[1]),
3 => Evaluate(0, 0, args[0], args[1], args[2]),
1 => Evaluate(ec.RowIndex, ec.ColumnIndex, args[0]),
2 => Evaluate(ec.RowIndex, ec.ColumnIndex, args[0], args[1]),
3 => Evaluate(ec.RowIndex, ec.ColumnIndex, args[0], args[1], args[2]),
_ => ErrorEval.VALUE_INVALID
};
private ValueEval Evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0)
@ -25,14 +29,12 @@ namespace NPOI.SS.Formula.Functions
{
return Evaluate(srcRowIndex, srcColumnIndex, arg0, arg1, null);
}
private ValueEval Evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1, ValueEval arg2)
{
try
{
double number = NumericFunction.SingleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex);
double significance = arg1 is null ? 1.0 :
NumericFunction.SingleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex);
var number = NumericFunction.SingleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex);
var significance = arg1 is null ? 1.0 : NumericFunction.SingleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex);
bool? method = null;
@ -43,7 +45,6 @@ namespace NPOI.SS.Formula.Functions
}
var result = Evaluate(number, significance, method ?? false);
return result == 0.0 ? NumberEval.ZERO : new NumberEval(result);
}
catch (EvaluationException e)
@ -58,7 +59,6 @@ namespace NPOI.SS.Formula.Functions
{
// FLOOR|CEILING.MATH 's behavior is different from FLOOR|CEILING
// when significance is zero & number isn't 0, the MATH one returns 0 instead of #DIV/0.
return 0.0;
}
@ -68,36 +68,32 @@ namespace NPOI.SS.Formula.Functions
significance = -significance;
}
return EvaluateMath(number, significance, mode);
}
// Workaround without BigDecimal
var numberToTest = number / significance;
if (DoublePrecisionHelper.IsIntegerWithDigitsDropped(numberToTest, SignificantThreshold))
return number;
protected abstract double EvaluateMajorDirection(double number);
protected abstract double EvaluateAlternativeDirection(double number);
private double EvaluateMath(double number, double significance, bool mode)
{
if (number >= 0)
if (number > 0)
{
// number is positive
return EvaluateMajorDirection(number / significance) * significance;
// mode is meaningless when number is positive
return EvaluateMajorDirection(numberToTest) * significance;
}
else
{
// number is negative
if (mode)
{
// Towards zero for FLOOR && Away from zero for CEILING
return EvaluateAlternativeDirection(number / -significance) * -significance;
return EvaluateAlternativeDirection(-numberToTest) * -significance;
}
else
{
// vice versa
return EvaluateMajorDirection(number / -significance) * -significance;
// Vice versa
return EvaluateMajorDirection(-numberToTest) * -significance;
}
}
}
protected abstract double EvaluateMajorDirection(double number);
protected abstract double EvaluateAlternativeDirection(double number);
}
}

1
main/SS/Formula/Functions/FloorMath.cs

@ -13,7 +13,6 @@ namespace NPOI.SS.Formula.Functions
}
public static readonly FloorMath Instance = new();
protected override double EvaluateMajorDirection(double number)
=> Math.Floor(number);

1
test.runsettings

@ -13,5 +13,6 @@
<Parameter name="poi.keep.tmp.files" value="../../../testcases/test-data/"/>
<Parameter name="font.metrics.filename" value="../../../testcases/test-data/font_metrics.properties"/>
<Parameter name="escher.data.path" value="../../../testcases/test-data/ddf/"/>
<Parameter name="function" value="../../../testcases/test-data/functions"/>
</TestRunParameters>
</RunSettings>

119
testcases/ooxml/SS/Formula/Functions/TestFloorCeilingMath.cs

@ -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());
}
}
}

BIN
testcases/test-data/functions/FloorCeilingMath.xlsx

Loading…
Cancel
Save