forked from nissl-lab/npoi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request nissl-lab#1324 from swyfft-insurance/feature/ks/20…
…240425_ImplementForecastFunction Implement forecast function
- Loading branch information
Showing
5 changed files
with
348 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
using System; | ||
using NPOI.SS.Formula.Eval; | ||
|
||
namespace NPOI.SS.Formula.Functions | ||
{ | ||
/// <summary> | ||
/// The Forecast class is a representation of the Excel FORECAST function. | ||
/// This function predicts a future value along a linear trend line based on existing historical data. | ||
/// The class inherits from the Fixed3ArgFunction class and overrides the Evaluate method. | ||
/// The Evaluate method takes three arguments: the x-value for which we want to forecast a y-value, | ||
/// and two arrays of x-values and y-values representing historical data. | ||
/// The method calculates the slope and intercept of the line of best fit for the historical data | ||
/// and uses these to calculate the forecast y-value. | ||
/// The class also includes methods for converting ValueEval objects to numeric arrays and for creating ValueVectors. | ||
/// </summary> | ||
public class Forecast : Fixed3ArgFunction | ||
{ | ||
public override ValueEval Evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1, | ||
ValueEval arg2) | ||
{ | ||
try | ||
{ | ||
if(arg0 is ErrorEval arg0Error) | ||
{ | ||
return arg0Error; | ||
} | ||
|
||
if(arg1 is ErrorEval arg1Error) | ||
{ | ||
return arg1Error; | ||
} | ||
|
||
if(arg2 is ErrorEval arg2Error) | ||
{ | ||
return arg2Error; | ||
} | ||
|
||
double x = NumericFunction.SingleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); | ||
double[] yValues = GetNumericArray(arg1); | ||
double[] xValues = GetNumericArray(arg2); | ||
|
||
if(yValues.Length != xValues.Length) | ||
{ | ||
return ErrorEval.NA; | ||
} | ||
|
||
double xSum = 0, ySum = 0, xySum = 0, xSquareSum = 0; | ||
int n = xValues.Length; | ||
|
||
for(int i = 0; i < n; i++) | ||
{ | ||
xSum += xValues[i]; | ||
ySum += yValues[i]; | ||
xySum += xValues[i] * yValues[i]; | ||
xSquareSum += Math.Pow(xValues[i], 2); | ||
} | ||
|
||
double slope = (n * xySum - xSum * ySum) / (n * xSquareSum - Math.Pow(xSum, 2)); | ||
double intercept = (ySum - slope * xSum) / n; | ||
|
||
double forecastY = slope * x + intercept; | ||
|
||
return new NumberEval(forecastY); | ||
} | ||
catch(EvaluationException e) | ||
{ | ||
return e.GetErrorEval(); | ||
} | ||
} | ||
|
||
private static double[] GetNumericArray(ValueEval arg) | ||
{ | ||
ValueVector vv = CreateValueVector(arg); | ||
double[] result = new double[vv.Size]; | ||
for(int i = 0; i < vv.Size; i++) | ||
{ | ||
ValueEval v = vv.GetItem(i); | ||
if(v is ErrorEval errorEval) | ||
{ | ||
throw new EvaluationException(errorEval); | ||
} | ||
|
||
if(v is NumberEval numberEval) | ||
{ | ||
result[i] = numberEval.NumberValue; | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
|
||
private static ValueVector CreateValueVector(ValueEval arg) | ||
{ | ||
return arg switch { | ||
ErrorEval eval => throw new EvaluationException(eval), | ||
TwoDEval dEval => new AreaValueArray(dEval), | ||
RefEval refEval => new RefValueArray(refEval), | ||
_ => new SingleCellValueArray(arg) | ||
}; | ||
} | ||
|
||
private abstract class ValueArray(int size) : ValueVector | ||
{ | ||
public ValueEval GetItem(int index) | ||
{ | ||
if(index < 0 || index > size) | ||
{ | ||
throw new ArgumentException($"Specified index {index} is outside range (0..{(size - 1)})"); | ||
} | ||
|
||
return GetItemInternal(index); | ||
} | ||
|
||
protected abstract ValueEval GetItemInternal(int index); | ||
|
||
public int Size => size; | ||
} | ||
|
||
private class SingleCellValueArray(ValueEval value) : ValueArray(1) | ||
{ | ||
protected override ValueEval GetItemInternal(int index) | ||
{ | ||
return value; | ||
} | ||
} | ||
|
||
private class RefValueArray(RefEval ref1) : ValueArray(ref1.NumberOfSheets) | ||
{ | ||
private readonly int _width = ref1.NumberOfSheets; | ||
|
||
protected override ValueEval GetItemInternal(int index) | ||
{ | ||
int sIx = (index % _width) + ref1.FirstSheetIndex; | ||
return ref1.GetInnerValueEval(sIx); | ||
} | ||
} | ||
|
||
private class AreaValueArray(TwoDEval ae) : ValueArray(ae.Width * ae.Height) | ||
{ | ||
private readonly int _width = ae.Width; | ||
|
||
protected override ValueEval GetItemInternal(int index) | ||
{ | ||
int rowIx = index / _width; | ||
int colIx = index % _width; | ||
return ae.GetValue(rowIx, colIx); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/* | ||
* ==================================================================== | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for Additional information regarding copyright ownership. | ||
* The ASF 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. | ||
* ==================================================================== | ||
*/ | ||
|
||
namespace TestCases.SS.Formula.Functions | ||
{ | ||
using NUnit.Framework; | ||
using System; | ||
using HSSF; | ||
using NPOI.SS.Formula.Eval; | ||
using NPOI.HSSF.UserModel; | ||
using NPOI.SS.UserModel; | ||
using NPOI.SS.Formula.Functions; | ||
|
||
/** | ||
* Test for Excel function FORECAST() | ||
* | ||
* @author Ken Smith | ||
*/ | ||
[TestFixture] | ||
public class TestForecast | ||
{ | ||
private static readonly Function FORECAST = new Forecast(); | ||
|
||
/// <summary> | ||
/// This test is replicated in the "TestBasic" tab of the "Forecast.xls" file. | ||
/// </summary> | ||
[Test] | ||
public void TestBasic() | ||
{ | ||
ValueEval x = new NumberEval(100); | ||
ValueEval[] yValues = [ | ||
new NumberEval(1), | ||
new NumberEval(2), | ||
new NumberEval(3), | ||
new NumberEval(4), | ||
new NumberEval(5), | ||
new NumberEval(6) | ||
]; | ||
|
||
ValueEval[] xValues = [ | ||
new NumberEval(2), | ||
new NumberEval(4), | ||
new NumberEval(6), | ||
new NumberEval(8), | ||
new NumberEval(10), | ||
new NumberEval(12) | ||
]; | ||
Confirm(x, CreateAreaEval(yValues), CreateAreaEval(xValues), 50.0); | ||
// Excel 365 build 2402 gives 50.0 | ||
} | ||
|
||
/// <summary> | ||
/// This test is replicated in the "TestLargeNumbers" tab of the "Forecast.xls" file. | ||
/// </summary> | ||
[Test] | ||
public void TestLargeNumbers() | ||
{ | ||
double exp = Math.Pow(10, 7.5); | ||
ValueEval x = new NumberEval(100); | ||
ValueEval[] yValues = [ | ||
new NumberEval(3 + exp), | ||
new NumberEval(4 + exp), | ||
new NumberEval(2 + exp), | ||
new NumberEval(5 + exp), | ||
new NumberEval(4 + exp), | ||
new NumberEval(7 + exp) | ||
]; | ||
|
||
ValueEval[] xValues = [ | ||
new NumberEval(1), | ||
new NumberEval(2), | ||
new NumberEval(3), | ||
new NumberEval(4), | ||
new NumberEval(5), | ||
new NumberEval(6) | ||
]; | ||
Confirm(x, CreateAreaEval(yValues), CreateAreaEval(xValues), 31622844.1826363); | ||
// Excel 365 build 2402 gives 31622844.1826363 | ||
} | ||
|
||
/// <summary> | ||
/// This test is replicated in the "TestLargeArrays" tab of the "Forecast.xls" file. | ||
/// </summary> | ||
[Test] | ||
public void TestLargeArrays() | ||
{ | ||
ValueEval x = new NumberEval(100); | ||
ValueEval[] yValues = CreateMockNumberArray(100, 3); // [2,2,0,1,2,0,...,0,1] | ||
yValues[0] = new NumberEval(2.0); // Changes first element to 2 | ||
ValueEval[] xValues = CreateMockNumberArray(100, 101); // [1,2,3,4,...,99,100] | ||
|
||
Confirm(x, CreateAreaEval(yValues), CreateAreaEval(xValues), 0.960990099); | ||
// Excel 365 build 2402 gives 0.960990099 | ||
} | ||
|
||
[Test] | ||
public void TestErrors() | ||
{ | ||
NumberEval x = new(100); | ||
|
||
ValueEval areaEval1 = CreateAreaEval([new NumberEval(2)]); | ||
ValueEval areaEval2 = CreateAreaEval([new NumberEval(2), new NumberEval(2)]); // different size | ||
|
||
ValueEval areaEvalWithNullError = CreateAreaEval([new NumberEval(2), ErrorEval.NULL_INTERSECTION]); | ||
ValueEval areaEvalWithRefError = CreateAreaEval([ErrorEval.REF_INVALID, new NumberEval(2)]); | ||
|
||
// if either arg is an error, that error propagates | ||
ConfirmError(x, ErrorEval.REF_INVALID, ErrorEval.NAME_INVALID, ErrorEval.REF_INVALID); | ||
ConfirmError(x, areaEvalWithRefError, ErrorEval.NAME_INVALID, ErrorEval.NAME_INVALID); | ||
ConfirmError(x, ErrorEval.NAME_INVALID, areaEvalWithRefError, ErrorEval.NAME_INVALID); | ||
|
||
// array sizes must match | ||
ConfirmError(x, areaEval1, areaEval2, ErrorEval.NA); | ||
|
||
// any error in an array item propagates up | ||
ConfirmError(x, areaEvalWithRefError, areaEvalWithRefError, ErrorEval.REF_INVALID); | ||
|
||
// search for errors array by array, not pair by pair | ||
ConfirmError(x, areaEvalWithRefError, areaEvalWithNullError, ErrorEval.REF_INVALID); | ||
ConfirmError(x, areaEvalWithNullError, areaEvalWithRefError, ErrorEval.NULL_INTERSECTION); | ||
} | ||
|
||
/** | ||
* Example from | ||
* https://support.microsoft.com/en-us/office/forecast-and-forecast-linear-functions-50ca49c9-7b40-4892-94e4-7ad38bbeda99 | ||
*/ | ||
[Test] | ||
public void TestFromFile() | ||
{ | ||
IWorkbook wb = HSSFTestDataSamples.OpenSampleWorkbook("Forecast.xls"); | ||
HSSFFormulaEvaluator fe = new(wb); | ||
|
||
ISheet example1 = wb.GetSheet("TestFromFile"); | ||
ICell a8 = example1.GetRow(7).GetCell(0); | ||
Assert.AreEqual("FORECAST(30,A2:A6,B2:B6)", a8.CellFormula); | ||
fe.Evaluate(a8); | ||
Assert.AreEqual(10.60725309, a8.NumericCellValue, 0.00000001); | ||
} | ||
|
||
private static ValueEval Invoke(ValueEval x, ValueEval yArray, ValueEval xArray) | ||
{ | ||
ValueEval[] args = [x, yArray, xArray]; | ||
return FORECAST.Evaluate(args, -1, (short) -1); | ||
} | ||
|
||
private static void Confirm(ValueEval x, ValueEval yArray, ValueEval xArray, double expected) | ||
{ | ||
ValueEval result = Invoke(x, yArray, xArray); | ||
Assert.AreEqual(typeof(NumberEval), result.GetType()); | ||
Assert.AreEqual(expected, ((NumberEval) result).NumberValue, expected * .000000001); | ||
} | ||
|
||
private static void ConfirmError(ValueEval x, ValueEval yArray, ValueEval xArray, ErrorEval expectedError) | ||
{ | ||
ValueEval result = Invoke(x, yArray, xArray); | ||
Assert.AreEqual(typeof(ErrorEval), result.GetType()); | ||
Assert.AreEqual(expectedError, (ErrorEval) result); | ||
} | ||
|
||
private static ValueEval[] CreateMockNumberArray(int size, double value) | ||
{ | ||
ValueEval[] result = new ValueEval[size]; | ||
for(int i = 0; i < result.Length; i++) | ||
{ | ||
result[i] = new NumberEval((i + 1) % value); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
private static ValueEval CreateAreaEval(ValueEval[] values) | ||
{ | ||
string refStr = "A1:A" + values.Length; | ||
return EvalFactory.CreateAreaEval(refStr, values); | ||
} | ||
} | ||
} |
Binary file not shown.