Skip to content

Commit

Permalink
Merge pull request #1324 from swyfft-insurance/feature/ks/20240425_Im…
Browse files Browse the repository at this point in the history
…plementForecastFunction

Implement forecast function
  • Loading branch information
tonyqus committed May 12, 2024
2 parents 75ee81d + ea2c0d3 commit 27fe304
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 2 deletions.
5 changes: 4 additions & 1 deletion build/_build.csproj.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f9fce829_002De6f4_002D4cb2_002D80f1_002D5497c44f51df/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
Expand All @@ -24,4 +26,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2 changes: 1 addition & 1 deletion main/SS/Formula/Eval/FunctionEval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ private static Function[] ProduceFunctions()
retval[306] = new NotImplementedFunction("CHITEST"); // CHITEST
retval[307] = new NotImplementedFunction("CORREL"); // CORREL
retval[308] = new NotImplementedFunction("COVAR"); // COVAR
retval[309] = new NotImplementedFunction("FORECAST"); // FORECAST
retval[309] = new Forecast(); // FORECAST
retval[310] = new NotImplementedFunction("FTEST"); // FTEST
retval[311] = new Intercept(); // INTERCEPT
retval[312] = new NotImplementedFunction("PEARSON"); // PEARSON
Expand Down
150 changes: 150 additions & 0 deletions main/SS/Formula/Functions/Forecast.cs
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);
}
}
}
}
193 changes: 193 additions & 0 deletions testcases/main/SS/Formula/Functions/TestForecast.cs
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 added testcases/test-data/spreadsheet/Forecast.xls
Binary file not shown.

0 comments on commit 27fe304

Please sign in to comment.