diff --git a/main/NPOI.Core.csproj b/main/NPOI.Core.csproj index a7def2ca6..62ecf7a79 100644 --- a/main/NPOI.Core.csproj +++ b/main/NPOI.Core.csproj @@ -15,6 +15,7 @@ + diff --git a/main/SS/Formula/Atp/AnalysisToolPak.cs b/main/SS/Formula/Atp/AnalysisToolPak.cs index 449f646f3..34b65d4c5 100644 --- a/main/SS/Formula/Atp/AnalysisToolPak.cs +++ b/main/SS/Formula/Atp/AnalysisToolPak.cs @@ -19,7 +19,8 @@ limitations under the License. using System.Collections.ObjectModel; using NPOI.SS.Formula.Function; -namespace NPOI.SS.Formula.Atp { +namespace NPOI.SS.Formula.Atp +{ using System; using System.Collections; using NPOI.SS.Formula; @@ -192,6 +193,15 @@ private static Dictionary CreateFunctionsMap() r(m, "YIELD", null); r(m, "YIELDDISC", null); r(m, "YIELDMAT", null); + + r(m, "CEILING.MATH", CeilingMath.Instance); + r(m, "FLOOR.MATH", FloorMath.Instance); + + r(m, "STDEV.S", StdevS.Instance); + r(m, "STDEV.P", StdevP.Instance); + r(m, "VAR.S", VarS.Instance); + r(m, "VAR.P", VarP.Instance); + return m; } diff --git a/main/SS/Formula/Functions/CeilingMath.cs b/main/SS/Formula/Functions/CeilingMath.cs new file mode 100644 index 000000000..d2e4615d8 --- /dev/null +++ b/main/SS/Formula/Functions/CeilingMath.cs @@ -0,0 +1,39 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public sealed class CeilingMath : FloorCeilingMathBase + { + private CeilingMath() + { + } + public static readonly CeilingMath Instance = new(); + protected override double EvaluateMajorDirection(double number) + => Math.Ceiling(number); + + protected override double EvaluateAlternativeDirection(double number) + => Math.Floor(number); + } +} diff --git a/main/SS/Formula/Functions/FloorCeilingMathBase.cs b/main/SS/Formula/Functions/FloorCeilingMathBase.cs new file mode 100644 index 000000000..eb62b882c --- /dev/null +++ b/main/SS/Formula/Functions/FloorCeilingMathBase.cs @@ -0,0 +1,128 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using ExtendedNumerics; +using NPOI.SS.Formula.Eval; +using NPOI.SS.Util; +using System; +using System.Collections.Generic; + +namespace NPOI.SS.Formula.Functions +{ + public abstract class FloorCeilingMathBase : FreeRefFunction + { + // Excel has an internal precision of 15 significant digits + private const int SignificantDigits = 15; + // Use high-precision decimal calculations or customized double workaround + private const bool UseHighPrecisionCalculation = true; + private const int SignificantDigitsForHighPrecision = SignificantDigits + 2; + + public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) + => args.Length switch + { + 1 => Evaluate(ec.RowIndex, ec.ColumnIndex, args[0], null, null), + 2 => Evaluate(ec.RowIndex, ec.ColumnIndex, args[0], args[1], null), + 3 => Evaluate(ec.RowIndex, ec.ColumnIndex, args[0], args[1], args[2]), + _ => ErrorEval.VALUE_INVALID + }; + private ValueEval Evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1, ValueEval arg2) + { + try + { + var number = NumericFunction.SingleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); + var significance = arg1 is null ? 1.0 : NumericFunction.SingleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex); + + bool? method = null; + + if (arg2 is not null) + { + ValueEval ve = OperandResolver.GetSingleValue(arg2, srcRowIndex, srcColumnIndex); + method = OperandResolver.CoerceValueToBoolean(ve, false); + } + + var result = Evaluate(number, significance, method ?? false); + return result == 0.0 ? NumberEval.ZERO : new NumberEval(result); + } + catch (EvaluationException e) + { + return e.GetErrorEval(); + } + } + + public double Evaluate(double number, double significance, bool mode) + { + if (significance == 0.0 || number == 0.0) + { + // 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; + } + + if (number > 0 && significance < 0 || number < 0 && significance > 0) + { + // This is how Excel behaves + significance = -significance; + } + + double numberToTest = number / significance; + + if (UseHighPrecisionCalculation) + { + BigDecimal.Precision = SignificantDigitsForHighPrecision; + + var bigNumber = new BigDecimal(number); + var bigSignificance = new BigDecimal(significance); + + BigDecimal bigNumberToTest = bigNumber / bigSignificance; + + if (bigNumberToTest.IsIntegerWithDigitsDropped(SignificantDigits)) + return number; + + // High-precision number is only for integer determination. We don't need it later. + } + else + { + // Workaround without BigDecimal + if (numberToTest.IsIntegerWithDigitsDropped(SignificantDigits)) + return number; + } + + if (number > 0) + { + // mode is meaningless when number is positive + return EvaluateMajorDirection(numberToTest) * significance; + } + else + { + if (mode) + { + // Towards zero for FLOOR && Away from zero for CEILING + return EvaluateAlternativeDirection(-numberToTest) * -significance; + } + else + { + // Vice versa + return EvaluateMajorDirection(-numberToTest) * -significance; + } + } + } + + protected abstract double EvaluateMajorDirection(double number); + protected abstract double EvaluateAlternativeDirection(double number); + } +} diff --git a/main/SS/Formula/Functions/FloorMath.cs b/main/SS/Formula/Functions/FloorMath.cs new file mode 100644 index 000000000..e04736bcd --- /dev/null +++ b/main/SS/Formula/Functions/FloorMath.cs @@ -0,0 +1,40 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public sealed class FloorMath : FloorCeilingMathBase + { + private FloorMath() + { + + } + public static readonly FloorMath Instance = new(); + protected override double EvaluateMajorDirection(double number) + => Math.Floor(number); + + protected override double EvaluateAlternativeDirection(double number) + => Math.Ceiling(number); + } +} diff --git a/main/SS/Formula/Functions/NumberListFuncBase.cs b/main/SS/Formula/Functions/NumberListFuncBase.cs new file mode 100644 index 000000000..73464148b --- /dev/null +++ b/main/SS/Formula/Functions/NumberListFuncBase.cs @@ -0,0 +1,73 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using NPOI.SS.Formula.Eval; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public abstract class NumberListFuncBase : FreeRefFunction + { + public bool AllowEmptyList { get; set; } = false; + public ValueEval ErrorOnEmptyList { get; set; } = ErrorEval.DIV_ZERO; + public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) + { + if (args.Length == 0) + return ErrorEval.VALUE_INVALID; + + try + { + var list = new List(); + + foreach (var arg in args) + { + switch (arg) + { + case AreaEval ae: + ValueEvaluationHelper.GetArrayValues(ae, list); + break; + case NumericValueEval: + case RefEval: + var val = ValueEvaluationHelper.GetScalarValue(arg); + if (val.HasValue) + list.Add(val.Value); + break; + default: + return ErrorEval.VALUE_INVALID; + } + } + + if (!AllowEmptyList && list.Count == 0) + return ErrorOnEmptyList; + + var result = CalculateFromNumberList(list); + return result == 0.0 ? NumberEval.ZERO : new NumberEval(result); + } + catch (EvaluationException e) + { + return e.GetErrorEval(); + } + } + + public abstract double CalculateFromNumberList(List list); + } +} diff --git a/main/SS/Formula/Functions/StdevP.cs b/main/SS/Formula/Functions/StdevP.cs new file mode 100644 index 000000000..9a0e1d909 --- /dev/null +++ b/main/SS/Formula/Functions/StdevP.cs @@ -0,0 +1,36 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public sealed class StdevP : NumberListFuncBase + { + public static readonly StdevP Instance = new(); + private StdevP() + { + } + public override double CalculateFromNumberList(List list) + => Math.Sqrt(VarP.Instance.CalculateFromNumberList(list)); + } +} diff --git a/main/SS/Formula/Functions/StdevS.cs b/main/SS/Formula/Functions/StdevS.cs new file mode 100644 index 000000000..bbd3fb439 --- /dev/null +++ b/main/SS/Formula/Functions/StdevS.cs @@ -0,0 +1,36 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public sealed class StdevS : NumberListFuncBase + { + public static readonly StdevS Instance = new(); + private StdevS() + { + } + public override double CalculateFromNumberList(List list) + => Math.Sqrt(VarS.Instance.CalculateFromNumberList(list)); + } +} diff --git a/main/SS/Formula/Functions/ValueEvaluationHelper.cs b/main/SS/Formula/Functions/ValueEvaluationHelper.cs new file mode 100644 index 000000000..91c9e9496 --- /dev/null +++ b/main/SS/Formula/Functions/ValueEvaluationHelper.cs @@ -0,0 +1,88 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using NPOI.SS.Formula.Eval; +using NPOI.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + internal static class ValueEvaluationHelper + { + public static double? GetScalarValue(ValueEval arg) + { + + ValueEval eval; + if (arg is RefEval re) + { + if (re.NumberOfSheets > 1) + { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + eval = re.GetInnerValueEval(re.FirstSheetIndex); + } + else + { + eval = arg; + } + + if (eval is AreaEval ae) + { + // an area ref can work as a scalar value if it is 1x1 + if (!ae.IsColumn || !ae.IsRow) + { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + eval = ae.GetRelativeValue(0, 0); + } + + if (eval is null) + { + throw new ArgumentException("parameter (eval) may not be null"); + } + + return GetSingleValue(eval); + } + public static void GetArrayValues(AreaEval evalArg, in List numList) + { + int height = evalArg.LastRow - evalArg.FirstRow + 1; + int width = evalArg.LastColumn - evalArg.FirstColumn + 1; // TODO - junit + + for (int rrIx = 0; rrIx < height; rrIx++) + { + for (int rcIx = 0; rcIx < width; rcIx++) + { + var val = GetSingleValue(evalArg.GetRelativeValue(rrIx, rcIx)); + if (val.HasValue) + numList.Add(val.Value); + } + } + } + private static double? GetSingleValue(ValueEval ve) => ve switch + { + NumericValueEval nve => nve.NumberValue, + null or BlankEval or StringEval => null, + ErrorEval ev => throw new EvaluationException(ev), + _ => throw new RuntimeException($"Unexpected value eval class ({ve.GetType().Name})") + }; + } +} diff --git a/main/SS/Formula/Functions/VarP.cs b/main/SS/Formula/Functions/VarP.cs new file mode 100644 index 000000000..751fd463a --- /dev/null +++ b/main/SS/Formula/Functions/VarP.cs @@ -0,0 +1,47 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public sealed class VarP : NumberListFuncBase + { + public static readonly VarP Instance = new(); + private VarP() + { + } + public override double CalculateFromNumberList(List list) + { + if (list.Count == 1) return 0.0; + var average = list.Average(); + var sum = 0.0; + + foreach (var item in list) + { + sum += Math.Pow(item - average, 2); + } + + return sum / list.Count; + } + } +} diff --git a/main/SS/Formula/Functions/VarS.cs b/main/SS/Formula/Functions/VarS.cs new file mode 100644 index 000000000..34af38a40 --- /dev/null +++ b/main/SS/Formula/Functions/VarS.cs @@ -0,0 +1,48 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using NPOI.SS.Formula.Eval; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NPOI.SS.Formula.Functions +{ + public sealed class VarS : NumberListFuncBase + { + public static readonly VarS Instance = new(); + private VarS() + { + } + public override double CalculateFromNumberList(List list) + { + if (list.Count == 1) throw new EvaluationException(ErrorEval.DIV_ZERO); + var average = list.Average(); + var sum = 0.0; + + foreach (var item in list) + { + sum += Math.Pow(item - average, 2); + } + + return sum / (list.Count - 1); + } + } +} diff --git a/main/SS/Util/DoublePrecisionHelper.cs b/main/SS/Util/DoublePrecisionHelper.cs new file mode 100644 index 000000000..182943765 --- /dev/null +++ b/main/SS/Util/DoublePrecisionHelper.cs @@ -0,0 +1,92 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using ExtendedNumerics; +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.Util +{ + 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(this double number, int significantDigits) + { + return Math.Abs(GetFractionPart(DropDigitsAfterSignificantOnes(number, significantDigits))) == 0.0; + } + + public static bool IsIntegerWithDigitsDropped(this BigDecimal number, int significantDigits) + { + if (number.IsZero()) + return true; + + if (number.IsNegative()) + number = -number; + + int decimalPlaces = number.DecimalPlaces; + int realSigDigits = number.SignifigantDigits; + int integerPlaces = realSigDigits - decimalPlaces; + + if (integerPlaces >= significantDigits) + { + return true; + } + + BigDecimal fracPart = number.GetFractionalPart(); + + if (fracPart.IsZero()) + { + return true; + } + + decimalPlaces = Math.Min(decimalPlaces, significantDigits - integerPlaces); + + BigDecimal exp = BigDecimal.Pow(10, decimalPlaces); + fracPart *= exp; + fracPart = BigDecimal.Round(fracPart); + + return fracPart.IsZero() || fracPart == exp; + } + } +} diff --git a/test.runsettings b/test.runsettings index 0f970a486..1b7ebee7f 100644 --- a/test.runsettings +++ b/test.runsettings @@ -13,5 +13,6 @@ + diff --git a/testcases/main/Util/TestDoublePrecisionHelper.cs b/testcases/main/Util/TestDoublePrecisionHelper.cs new file mode 100644 index 000000000..4c77a8815 --- /dev/null +++ b/testcases/main/Util/TestDoublePrecisionHelper.cs @@ -0,0 +1,92 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using NPOI.SS.UserModel; +using NUnit.Framework.Constraints; +using NPOI.SS.Util; +using ExtendedNumerics; +using MathNet.Numerics; + +namespace TestCases.SS.Util +{ + [TestFixture] + public class TestDoublePrecisionHelper + { + public const int Precision = 15; + + private static void BigDecimalPrecisionTest(bool expected, string repre) + { + Assert.AreEqual(expected, BigDecimal.Parse(repre).IsIntegerWithDigitsDropped(Precision), repre); + } + private static void BigDecimalPrecisionTest(bool expected, double repre) + { + Assert.AreEqual(expected, new BigDecimal(repre).IsIntegerWithDigitsDropped(Precision), repre.ToString()); + } + private static void DoublePrecisionTest(bool expected, string repre) + { + Assert.AreEqual(expected, double.Parse(repre).IsIntegerWithDigitsDropped(Precision), repre); + } + private static void DoublePrecisionTest(bool expected, double repre) + { + Assert.AreEqual(expected, repre.IsIntegerWithDigitsDropped(Precision), repre.ToString()); + } + + [Test] + public void IsBigDecimalAlmostInteger() + { + Assert.Multiple(() => + { + BigDecimalPrecisionTest(true, "4.9999999999999999"); + BigDecimalPrecisionTest(true, "4.999999999999999"); + BigDecimalPrecisionTest(false, "4.99999999999999"); + BigDecimalPrecisionTest(false, "4.9999999999999"); + BigDecimalPrecisionTest(false, "12345678901234.5"); + BigDecimalPrecisionTest(false, "12345678901234.9"); + BigDecimalPrecisionTest(true, "12345678901234.99"); + BigDecimalPrecisionTest(true, "123456789012345"); + BigDecimalPrecisionTest(true, "123456789012345.25"); + + BigDecimalPrecisionTest(true, 1 / 0.0999999999999996); + }); + } + + [Test] + public void IsDoubleAlmostInteger() + { + Assert.Multiple(() => + { + DoublePrecisionTest(true, "4.9999999999999999"); + DoublePrecisionTest(true, "4.999999999999999"); + DoublePrecisionTest(false, "4.99999999999999"); + DoublePrecisionTest(false, "4.9999999999999"); + DoublePrecisionTest(false, "12345678901234.5"); + DoublePrecisionTest(false, "12345678901234.9"); + DoublePrecisionTest(true, "12345678901234.99"); + DoublePrecisionTest(true, "123456789012345"); + DoublePrecisionTest(true, "123456789012345.25"); + + DoublePrecisionTest(true, 1 / 0.0999999999999996); + }); + + } + } +} diff --git a/testcases/ooxml/SS/Formula/Functions/CompareNumericFuncEvalTestBase.cs b/testcases/ooxml/SS/Formula/Functions/CompareNumericFuncEvalTestBase.cs new file mode 100644 index 000000000..0eb3c98f5 --- /dev/null +++ b/testcases/ooxml/SS/Formula/Functions/CompareNumericFuncEvalTestBase.cs @@ -0,0 +1,113 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; + +namespace TestCases.SS.Formula.Functions +{ + /// + /// This class provides a simple comparison test + /// to show if the cached formula result in an Excel file + /// is still same after being recalculated by NPOI. + /// The specific class is for numeric and error values. + /// + [TestFixture] + public abstract class CompareNumericFuncEvalTestBase + { + public abstract string TestFileName { get; } + // In real-world Excel's tolerance control is more complicated. + // Save it for now. + private const double Tolerance = 1e-7; + private XSSFWorkbook _workbook; + [OneTimeSetUp] + public void LoadData() + { + var fldr = Path.Combine(TestContext.CurrentContext.TestDirectory, TestContext.Parameters["function"]); + var file = Path.Combine(fldr, TestFileName); + + using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + _workbook = new XSSFWorkbook(fs); + } + } + + [OneTimeTearDown] + public void Dispose() + { + _workbook?.Close(); + } + + internal static SortedList GetSpreadsheetContent(IWorkbook workbook) + { + var list = new SortedList(); + + var sheet = workbook.GetSheetAt(0); + + for (var rowId = sheet.FirstRowNum; rowId <= sheet.LastRowNum; rowId++) + { + var row = sheet.GetRow(rowId); + if (row is null || row.FirstCellNum < 0) + continue; + + for (var colId = row.FirstCellNum; colId <= row.LastCellNum; colId++) + { + var cell = row.GetCell(colId); + if (cell is null) continue; + if (cell.CellType != CellType.Formula) continue; + if (cell.CachedFormulaResultType != CellType.Numeric + && cell.CachedFormulaResultType != CellType.Error) continue; + + list[cell.Address.FormatAsString()] = + cell.CachedFormulaResultType == CellType.Numeric ? + cell.NumericCellValue : cell.ErrorCellValue; + } + } + return list; + } + + [Test] + public void TestEvaluate() + { + var originalData = GetSpreadsheetContent(_workbook); + + var evaluator = new XSSFFormulaEvaluator(_workbook); + evaluator.ClearAllCachedResultValues(); + Assert.DoesNotThrow(() => evaluator.EvaluateAll()); + + var evaluatedData = GetSpreadsheetContent(_workbook); + + Assert.Multiple(() => + { + foreach (var kv in evaluatedData) + { + if (!originalData.TryGetValue(kv.Key, out var val)) + { + Assert.Fail($"Spreadsheet structure changed! No {kv.Key} cell in the original spreadsheet."); + break; + } + + Assert.AreEqual(val, kv.Value, Tolerance, kv.Key); + } + }); + } + } +} diff --git a/testcases/ooxml/SS/Formula/Functions/TestFloorCeilingMath.cs b/testcases/ooxml/SS/Formula/Functions/TestFloorCeilingMath.cs new file mode 100644 index 000000000..332b6044b --- /dev/null +++ b/testcases/ooxml/SS/Formula/Functions/TestFloorCeilingMath.cs @@ -0,0 +1,129 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +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 +{ + /// + /// Testing FLOOR.MATH & CEILING.MATH + /// + [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); + + 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()); + } + } +} diff --git a/testcases/ooxml/SS/Formula/Functions/TestStdevAndVarVariants.cs b/testcases/ooxml/SS/Formula/Functions/TestStdevAndVarVariants.cs new file mode 100644 index 000000000..20d454d50 --- /dev/null +++ b/testcases/ooxml/SS/Formula/Functions/TestStdevAndVarVariants.cs @@ -0,0 +1,31 @@ +/* + * ==================================================================== + * Licensed to the collaborators of the NPOI project under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The collaborators license this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ +using NUnit.Framework; + +namespace TestCases.SS.Formula.Functions +{ + /// + /// Test (STDEV|VAR).(P|S) + /// + [TestFixture] + public class TestStdevAndVarVariants : CompareNumericFuncEvalTestBase + { + public override string TestFileName => "StDevAndVar.xlsx"; + } +} diff --git a/testcases/test-data/functions/FloorCeilingMath.xlsx b/testcases/test-data/functions/FloorCeilingMath.xlsx new file mode 100644 index 000000000..e15ac584c Binary files /dev/null and b/testcases/test-data/functions/FloorCeilingMath.xlsx differ diff --git a/testcases/test-data/functions/StDevAndVar.xlsx b/testcases/test-data/functions/StDevAndVar.xlsx new file mode 100644 index 000000000..9bc982de1 Binary files /dev/null and b/testcases/test-data/functions/StDevAndVar.xlsx differ